Compare commits
293 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
51a43d1ff0 | ||
|
04a798b229 | ||
|
0799090a1a | ||
|
8c51ea15b2 | ||
|
f796387e7e | ||
|
9f1c306920 | ||
|
2f85e50c6f | ||
|
eb4cae4e6d | ||
|
e4e6882f12 | ||
|
15a288b63d | ||
|
620cd6dfc2 | ||
|
a5475e7752 | ||
|
bddfd5763b | ||
|
21edb655d3 | ||
|
51f0d1f33e | ||
|
da31b6fda8 | ||
|
2b071bed90 | ||
|
1cf935eaf3 | ||
|
b33b5264e5 | ||
|
8ca50098d5 | ||
|
d82ed50fa4 | ||
|
f894237a12 | ||
|
9d8ebd7bd2 | ||
|
5c552a3d53 | ||
|
300b68177b | ||
|
83f79c1466 | ||
|
bc94e3992f | ||
|
1c44df8079 | ||
|
b6b1df6a7c | ||
|
b4aa7831e7 | ||
|
d1cdd60883 | ||
|
a850352eab | ||
|
d9d76ba16d | ||
|
993284f9c1 | ||
|
a7d3130f9a | ||
|
e0df5783f8 | ||
|
e4de6da5b8 | ||
|
87219f897e | ||
|
73cf58826f | ||
|
be4637a3a0 | ||
|
6ac6c7cfda | ||
|
94e9b8f4b1 | ||
|
bc6149deeb | ||
|
a0d975c3c0 | ||
|
d51b155e52 | ||
|
fb1b327f9a | ||
|
754cdc4d58 | ||
|
a73cb9ad3d | ||
|
58ecc0dc0d | ||
|
3821e91be0 | ||
|
de2bb7938a | ||
|
61e2877c4b | ||
|
d7ade487b8 | ||
|
6d04e93f34 | ||
|
d7a7af756a | ||
|
0c5fe3d637 | ||
|
eb0a116cc7 | ||
|
e08a21ebe7 | ||
|
49074effce | ||
|
ffe8b3c909 | ||
|
7856afee92 | ||
|
fe533b7c7f | ||
|
fc158ca176 | ||
|
f632888b4c | ||
|
8324632e4e | ||
|
be4b20af97 | ||
|
5a4e0a06e6 | ||
|
fb71d3b562 | ||
|
b96d1e79a0 | ||
|
0d310c434d | ||
|
b111834122 | ||
|
2847b5ee45 | ||
|
943906d8a3 | ||
|
cbedfa4664 | ||
|
01ccc32274 | ||
|
3b153a6c9b | ||
|
1bcdc2652c | ||
|
ea050b98ef | ||
|
b30d69b2a6 | ||
|
60e099e852 | ||
|
c49b37f968 | ||
|
404d9db359 | ||
|
5ac0390446 | ||
|
6e98fb1c5e | ||
|
053d7f9eaa | ||
|
5dcfda0514 | ||
|
b42125a654 | ||
|
413cec8a9f | ||
|
8e7ffab793 | ||
|
770aee4953 | ||
|
f479901c87 | ||
|
1dbe7897d4 | ||
|
c95956766e | ||
|
e92c0db6a2 | ||
|
3a8b8ed639 | ||
|
3a78d69b5b | ||
|
2e562d187a | ||
|
4521dde455 | ||
|
b64b0e3362 | ||
|
f8ca73265b | ||
|
1f7614af33 | ||
|
a48a9318c1 | ||
|
dcb042681d | ||
|
7df2f7e752 | ||
|
c3578d2cda | ||
|
8db39a58fb | ||
|
bbdbb08301 | ||
|
b06e09c030 | ||
|
bb2bcdbf61 | ||
|
2e278e7323 | ||
|
4c9d52422b | ||
|
f4ba1f68ef | ||
|
12497e8fb1 | ||
|
8153e747ef | ||
|
63b597beb8 | ||
|
cdbb0b21da | ||
|
b2f40e490b | ||
|
a96e1903a3 | ||
|
be7eb8b2b5 | ||
|
3b6372431a | ||
|
389ee7917f | ||
|
212e61d2a1 | ||
|
1b60e4a013 | ||
|
93cd93ada3 | ||
|
babb4cb57b | ||
|
dbcc75471f | ||
|
2a0497ca9e | ||
|
2d0767306e | ||
|
8ca83bb255 | ||
|
80a6406062 | ||
|
ff9345a843 | ||
|
fe663c4f04 | ||
|
9fbb012697 | ||
|
0070950911 | ||
|
62cf611fdc | ||
|
75814433a6 | ||
|
e59a5b4449 | ||
|
161e512805 | ||
|
3adb955a14 | ||
|
305afb3713 | ||
|
1acbef1890 | ||
|
f90f370fed | ||
|
d34a0ee20e | ||
|
01e3964232 | ||
|
153638c2cd | ||
|
4bb719359c | ||
|
847eb60806 | ||
|
e799bd3920 | ||
|
2b1aee9e71 | ||
|
d65f068310 | ||
|
b1c199e650 | ||
|
51014e7a8d | ||
|
530bf81940 | ||
|
2bba186c9e | ||
|
61241df0d4 | ||
|
b6b9b542d7 | ||
|
71f41d5233 | ||
|
a421af9ea9 | ||
|
75372ad0cc | ||
|
d1f292f462 | ||
|
890cf81627 | ||
|
b9f31d5066 | ||
|
d97f0a4c4d | ||
|
4370db6bdc | ||
|
770f3e5da3 | ||
|
0f0895f345 | ||
|
6d1933c8f3 | ||
|
776260c85a | ||
|
5a5463bd5d | ||
|
2f45f50d37 | ||
|
41ad7c5d26 | ||
|
c5dff7b5d4 | ||
|
df93d43c36 | ||
|
bc9c70556e | ||
|
f75daba6c0 | ||
|
67e57ffd58 | ||
|
80ff8383fe | ||
|
5fd6202e60 | ||
|
ef5d505de3 | ||
|
5992c34fb5 | ||
|
bae74fbbd7 | ||
|
4264d170e2 | ||
|
ca89be8930 | ||
|
c2256c2ac7 | ||
|
78ce23750e | ||
|
e6ddbd1418 | ||
|
344146d837 | ||
|
15f0e491bf | ||
|
70c4e82b89 | ||
|
db78346bef | ||
|
08f6f8c405 | ||
|
b3bbec83b6 | ||
|
78d1d48ea9 | ||
|
3ff5884112 | ||
|
0d4cbf7da6 | ||
|
a3f6ce52e4 | ||
|
19ea1ee56c | ||
|
09d43403b2 | ||
|
dee8f45986 | ||
|
9d6a791443 | ||
|
d6fd351330 | ||
|
80de3fdd4c | ||
|
25cb598694 | ||
|
b69493d252 | ||
|
2eafa2a212 | ||
|
15a8adb0b9 | ||
|
8434f6e6cf | ||
|
fa66f39790 | ||
|
16785ae005 | ||
|
3822ae9356 | ||
|
f4f0a59e90 | ||
|
59bb04f1b3 | ||
|
47a06c14d9 | ||
|
88637adfe2 | ||
|
9a1ea7f226 | ||
|
4665db62f4 | ||
|
ab5b497562 | ||
|
5a614b5173 | ||
|
8546490bcc | ||
|
3dcea41c4e | ||
|
f947c1304a | ||
|
57314443ed | ||
|
242d57c14b | ||
|
71041ec764 | ||
|
2b12d3f8e8 | ||
|
022c180b62 | ||
|
0263be8c1f | ||
|
a8c118fd4a | ||
|
ddc55c7c22 | ||
|
0ad4f78a51 | ||
|
4e1a9da840 | ||
|
e8e3834fc0 | ||
|
790185f9e9 | ||
|
d02f81974c | ||
|
b340863d52 | ||
|
1a372abaff | ||
|
10aa268ea2 | ||
|
59657766b5 | ||
|
716d1fc988 | ||
|
e82fc8d617 | ||
|
2661f00dd4 | ||
|
afad169118 | ||
|
dcd89f2295 | ||
|
53386b35c9 | ||
|
2e14132a20 | ||
|
2fbcbe86d2 | ||
|
3f65051bd4 | ||
|
7183416d1f | ||
|
0662b5b4ae | ||
|
dcbeab0aef | ||
|
3e1ff9bc25 | ||
|
58359c9682 | ||
|
a3da248e3c | ||
|
396eeca73a | ||
|
d8092ec3eb | ||
|
31ba3cf039 | ||
|
9cef0d8346 | ||
|
ed14be08b9 | ||
|
1990a2d9bd | ||
|
c92df1168d | ||
|
01a4aa51f7 | ||
|
2306a4e34d | ||
|
bd4d8847ce | ||
|
2b29e14e9f | ||
|
14d7665072 | ||
|
09b44075ed | ||
|
8f1d2e0163 | ||
|
accebd00f5 | ||
|
4f3780979e | ||
|
56a72eea5c | ||
|
189d096834 | ||
|
31cc6e51b5 | ||
|
411a8ef8a7 | ||
|
497f38111f | ||
|
72792ae9f9 | ||
|
3f3e23420d | ||
|
af8627b999 | ||
|
6ad0364ace | ||
|
0230177d27 | ||
|
f8bf70f0cb | ||
|
f0aa0c5540 | ||
|
6be86be0a7 | ||
|
5ad498f3ca | ||
|
c0264f1cd6 | ||
|
0f105e0300 | ||
|
c6ffe4502a | ||
|
b07aef02c7 | ||
|
11193896b2 | ||
|
b07a6a9a78 | ||
|
cd5aec7368 | ||
|
b3b7017bf2 | ||
|
59daeeb37a | ||
|
ec896b8a12 |
@@ -4,6 +4,7 @@ npm-debug.log
|
||||
readme*
|
||||
Start.bat
|
||||
/dist
|
||||
/backups/
|
||||
/backups
|
||||
cloudflared.exe
|
||||
access.log
|
||||
/data
|
||||
|
12
.eslintrc.js
@@ -42,11 +42,21 @@ module.exports = {
|
||||
showdownKatex: 'readonly',
|
||||
SVGInject: 'readonly',
|
||||
toastr: 'readonly',
|
||||
Readability: 'readonly',
|
||||
isProbablyReaderable: 'readonly',
|
||||
},
|
||||
},
|
||||
],
|
||||
// There are various vendored libraries that shouldn't be linted
|
||||
ignorePatterns: ['public/lib/**/*', '*.min.js', 'src/ai_horde/**/*'],
|
||||
ignorePatterns: [
|
||||
'public/lib/**/*',
|
||||
'*.min.js',
|
||||
'src/ai_horde/**/*',
|
||||
'plugins/**/*',
|
||||
'data/**/*',
|
||||
'backups/**/*',
|
||||
'node_modules/**/*',
|
||||
],
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'no-control-regex': 'off',
|
||||
|
1
.github/workflows/check-merge-conflicts.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- staging
|
||||
jobs:
|
||||
check-conflicts:
|
||||
if: github.repository == 'SillyTavern/SillyTavern'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mschilde/auto-label-merge-conflicts@master
|
||||
|
1
.github/workflows/docker-publish.yml
vendored
@@ -21,6 +21,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'SillyTavern/SillyTavern'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
43
.github/workflows/update-docs.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Update SillyTavern-Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
update_docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout current repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Checkout SillyTavern-Docs repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: SillyTavern/SillyTavern-Docs
|
||||
path: SillyTavern-Docs
|
||||
|
||||
- name: Clone SillyTavern wiki into SillyTavern-Docs/extensions
|
||||
run: rm -rf SillyTavern-Docs/extensions && git clone https://github.com/SillyTavern/SillyTavern.wiki.git SillyTavern-Docs/extensions && rm -rf SillyTavern-Docs/extensions/.git
|
||||
|
||||
- name: Copy files
|
||||
run: |
|
||||
cp public/notes/content.md SillyTavern-Docs/guidebook.md
|
||||
cp faq.md SillyTavern-Docs/faq.md
|
||||
cp readme.md SillyTavern-Docs/readme.md
|
||||
cp public/notes/update.md SillyTavern-Docs/update.md
|
||||
|
||||
- name: Deploy to external repository
|
||||
uses: cpina/github-action-push-to-another-repository@main
|
||||
env:
|
||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
with:
|
||||
# GitHub Action output files
|
||||
source-directory: SillyTavern-Docs/
|
||||
destination-github-username: SillyTavern
|
||||
destination-repository-name: SillyTavern-Docs
|
||||
user-email: github-actions[bot]@users.noreply.github.com
|
||||
user-name: "GitHub Actions"
|
||||
target-branch: "main"
|
1
.gitignore
vendored
@@ -25,6 +25,7 @@ public/stats.json
|
||||
/docker/config
|
||||
/docker/user
|
||||
/docker/extensions
|
||||
/docker/data
|
||||
.DS_Store
|
||||
public/settings.json
|
||||
/thumbnails
|
||||
|
16
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:19.1.0-alpine3.16
|
||||
FROM node:lts-alpine3.18
|
||||
|
||||
# Arguments
|
||||
ARG APP_HOME=/home/node/app
|
||||
@@ -26,19 +26,9 @@ COPY . ./
|
||||
|
||||
# Copy default chats, characters and user avatars to <folder>.default folder
|
||||
RUN \
|
||||
IFS="," RESOURCES="assets,backgrounds,user,context,instruct,QuickReplies,movingUI,themes,characters,chats,groups,group chats,User Avatars,worlds,OpenAI Settings,NovelAI Settings,KoboldAI Settings,TextGen Settings" && \
|
||||
\
|
||||
echo "*** Store default $RESOURCES in <folder>.default ***" && \
|
||||
for R in $RESOURCES; do mv "public/$R" "public/$R.default"; done || true && \
|
||||
\
|
||||
echo "*** Create symbolic links to config directory ***" && \
|
||||
for R in $RESOURCES; do ln -s "../config/$R" "public/$R"; done || true && \
|
||||
\
|
||||
rm -f "config.yaml" "public/settings.json" || true && \
|
||||
rm -f "config.yaml" || true && \
|
||||
ln -s "./config/config.yaml" "config.yaml" || true && \
|
||||
ln -s "../config/settings.json" "public/settings.json" || true && \
|
||||
mkdir "config" || true && \
|
||||
mkdir -p "public/user" || true
|
||||
mkdir "config" || true
|
||||
|
||||
# Cleanup unnecessary files
|
||||
RUN \
|
||||
|
@@ -33,7 +33,14 @@ If you insist on installing via a zip, here is the tedious process for doing the
|
||||
2. Unzip it into a folder OUTSIDE of your current ST installation.
|
||||
3. Do the usual setup procedure for your OS to install the NodeJS requirements.
|
||||
|
||||
4. Copy the following files/folders as necessary(*) from your old ST installation:
|
||||
4a. Updating 1.12.0 and above
|
||||
|
||||
Copy the user data directory from your data root into the data root of the new install.
|
||||
|
||||
By default: /data/default-user
|
||||
|
||||
4a. Migrating from <1.12.0 to >=1.20.0
|
||||
Copy the following files/folders as necessary(*) from your old ST installation:
|
||||
|
||||
- Assets
|
||||
- Backgrounds
|
||||
@@ -54,16 +61,15 @@ If you insist on installing via a zip, here is the tedious process for doing the
|
||||
- Worlds
|
||||
- User
|
||||
- settings.json
|
||||
- secrets.json <---- this one is in the base folder, not /public/
|
||||
- secrets.json <---- This one is in the base folder, not /public/
|
||||
|
||||
(*) 'As necessary' = "If you made any custom content related to those folders".
|
||||
None of the folders are mandatory, so only copy what you need.
|
||||
|
||||
**NB: DO NOT COPY THE ENTIRE /PUBLIC/ FOLDER.**
|
||||
Doing so could break the new install and prevent new features from being present.
|
||||
Paste those items into the /data/default-user folder of the new install.
|
||||
|
||||
5. Paste those items into the /Public/ folder of the new install.
|
||||
5. Start SillyTavern once again with the method appropriate to your OS, and pray you got it right.
|
||||
|
||||
6. Start SillyTavern once again with the method appropriate to your OS, and pray you got it right.
|
||||
|
||||
7. If everything shows up, you can safely delete the old ST folder.
|
||||
6. If everything shows up, you can safely delete the old ST folder.
|
||||
|
@@ -1,10 +1,16 @@
|
||||
# -- NETWORK CONFIGURATION --
|
||||
# -- DATA CONFIGURATION --
|
||||
# Root directory for user data storage
|
||||
dataRoot: ./data
|
||||
# -- SERVER CONFIGURATION --
|
||||
# Listen for incoming connections
|
||||
listen: false
|
||||
# Server port
|
||||
port: 8000
|
||||
# -- SECURITY CONFIGURATION --
|
||||
# Toggle whitelist mode
|
||||
whitelistMode: true
|
||||
# Whitelist will also verify IP in X-Forwarded-For / X-Real-IP headers
|
||||
enableForwardedWhitelist: true
|
||||
# Whitelist of allowed IP addresses
|
||||
whitelist:
|
||||
- 127.0.0.1
|
||||
@@ -16,7 +22,15 @@ basicAuthUser:
|
||||
password: "password"
|
||||
# Enables CORS proxy middleware
|
||||
enableCorsProxy: false
|
||||
# Disable security checks - NOT RECOMMENDED
|
||||
# Enable multi-user mode
|
||||
enableUserAccounts: false
|
||||
# Enable discreet login mode: hides user list on the login screen
|
||||
enableDiscreetLogin: false
|
||||
# Used to sign session cookies. Will be auto-generated if not set
|
||||
cookieSecret: ''
|
||||
# Disable CSRF protection - NOT RECOMMENDED
|
||||
disableCsrfProtection: false
|
||||
# Disable startup security checks - NOT RECOMMENDED
|
||||
securityOverride: false
|
||||
# -- ADVANCED CONFIGURATION --
|
||||
# Open the browser automatically
|
||||
@@ -34,6 +48,12 @@ allowKeysExposure: false
|
||||
skipContentCheck: false
|
||||
# Disable automatic chats backup
|
||||
disableChatBackup: false
|
||||
# Allowed hosts for card downloads
|
||||
whitelistImportDomains:
|
||||
- localhost
|
||||
- cdn.discordapp.com
|
||||
- files.catbox.moe
|
||||
- raw.githubusercontent.com
|
||||
# API request overrides (for KoboldAI and Text Completion APIs)
|
||||
## Note: host includes the port number if it's not the default (80 or 443)
|
||||
## Format is an array of objects:
|
||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 384 KiB |
Before Width: | Height: | Size: 487 KiB After Width: | Height: | Size: 487 KiB |
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 307 KiB |
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 318 KiB |
Before Width: | Height: | Size: 581 KiB After Width: | Height: | Size: 581 KiB |
Before Width: | Height: | Size: 561 KiB After Width: | Height: | Size: 561 KiB |
Before Width: | Height: | Size: 505 KiB After Width: | Height: | Size: 505 KiB |
Before Width: | Height: | Size: 501 KiB After Width: | Height: | Size: 501 KiB |
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 443 KiB |
Before Width: | Height: | Size: 480 KiB After Width: | Height: | Size: 480 KiB |
Before Width: | Height: | Size: 660 KiB After Width: | Height: | Size: 660 KiB |
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 371 KiB |
Before Width: | Height: | Size: 616 KiB After Width: | Height: | Size: 616 KiB |
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 305 KiB |
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 436 KiB |
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
Before Width: | Height: | Size: 629 KiB After Width: | Height: | Size: 629 KiB |
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 656 KiB |
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 528 KiB |
@@ -1,4 +1,108 @@
|
||||
[
|
||||
{
|
||||
"filename": "settings.json",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"filename": "themes/Dark Lite.json",
|
||||
"type": "theme"
|
||||
},
|
||||
{
|
||||
"filename": "themes/Cappuccino.json",
|
||||
"type": "theme"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/__transparent.png",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/_black.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/_white.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/bedroom clean.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/bedroom cyberpunk.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/bedroom red.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/bedroom tatami.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/cityscape medieval market.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/cityscape medieval night.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/cityscape postapoc.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/forest treehouse fireworks air baloons (by kallmeflocc).jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/japan classroom side.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/japan classroom.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/japan path cherry blossom.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/japan university.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape autumn great tree.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape beach day.png",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape beach night.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape mountain lake.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape postapoc.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape winter lake house.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/royal.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/tavern day.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "default_Seraphina.png",
|
||||
"type": "character"
|
||||
@@ -211,7 +315,6 @@
|
||||
"filename": "presets/novel/Writers-Daemon-Kayra.json",
|
||||
"type": "novel_preset"
|
||||
},
|
||||
|
||||
{
|
||||
"filename": "presets/textgen/Asterism.json",
|
||||
"type": "textgen_preset"
|
||||
@@ -436,6 +539,10 @@
|
||||
"filename": "presets/context/Llama 3 Instruct.json",
|
||||
"type": "context"
|
||||
},
|
||||
{
|
||||
"filename": "presets/context/Phi.json",
|
||||
"type": "context"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/Adventure.json",
|
||||
"type": "instruct"
|
||||
@@ -527,5 +634,37 @@
|
||||
{
|
||||
"filename": "presets/instruct/Llama 3 Instruct.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/Phi.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/moving-ui/Default.json",
|
||||
"type": "moving_ui"
|
||||
},
|
||||
{
|
||||
"filename": "presets/moving-ui/Black Magic Time.json",
|
||||
"type": "moving_ui"
|
||||
},
|
||||
{
|
||||
"filename": "presets/quick-replies/Default.json",
|
||||
"type": "quick_replies"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/Llama-3-Instruct-Names.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/ChatML-Names.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/context/Llama-3-Instruct-Names.json",
|
||||
"type": "context"
|
||||
},
|
||||
{
|
||||
"filename": "presets/context/ChatML-Names.json",
|
||||
"type": "context"
|
||||
}
|
||||
]
|
||||
|
12
default/content/presets/context/ChatML-Names.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"story_string": "<|im_start|>system\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|im_end|>",
|
||||
"example_separator": "",
|
||||
"chat_start": "",
|
||||
"use_stop_strings": false,
|
||||
"allow_jailbreak": false,
|
||||
"always_force_name2": true,
|
||||
"trim_sentences": false,
|
||||
"include_newline": false,
|
||||
"single_line": false,
|
||||
"name": "ChatML-Names"
|
||||
}
|
12
default/content/presets/context/Llama-3-Instruct-Names.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"story_string": "<|start_header_id|>system<|end_header_id|>\n\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|eot_id|>",
|
||||
"example_separator": "",
|
||||
"chat_start": "",
|
||||
"use_stop_strings": false,
|
||||
"allow_jailbreak": false,
|
||||
"always_force_name2": true,
|
||||
"trim_sentences": false,
|
||||
"include_newline": false,
|
||||
"single_line": false,
|
||||
"name": "Llama-3-Instruct-Names"
|
||||
}
|
12
default/content/presets/context/Phi.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"story_string": "<|system|>\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|end|>\n",
|
||||
"example_separator": "",
|
||||
"chat_start": "",
|
||||
"use_stop_strings": false,
|
||||
"allow_jailbreak": false,
|
||||
"always_force_name2": true,
|
||||
"trim_sentences": false,
|
||||
"include_newline": false,
|
||||
"single_line": false,
|
||||
"name": "Phi"
|
||||
}
|
24
default/content/presets/instruct/ChatML-Names.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.",
|
||||
"input_sequence": "<|im_start|>[{{name}}]",
|
||||
"output_sequence": "<|im_start|>[{{name}}]",
|
||||
"last_output_sequence": "",
|
||||
"system_sequence": "<|im_start|>system",
|
||||
"stop_sequence": "<|im_end|>",
|
||||
"wrap": true,
|
||||
"macro": true,
|
||||
"names": false,
|
||||
"names_force_groups": false,
|
||||
"activation_regex": "",
|
||||
"system_sequence_prefix": "",
|
||||
"system_sequence_suffix": "",
|
||||
"first_output_sequence": "",
|
||||
"skip_examples": false,
|
||||
"output_suffix": "<|im_end|>\n",
|
||||
"input_suffix": "<|im_end|>\n",
|
||||
"system_suffix": "<|im_end|>\n",
|
||||
"user_alignment_message": "",
|
||||
"system_same_as_user": false,
|
||||
"last_system_sequence": "",
|
||||
"name": "ChatML-Names"
|
||||
}
|
24
default/content/presets/instruct/Llama-3-Instruct-Names.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"system_prompt": "You are an expert actor that can fully immerse yourself into any role given. You do not break character for any reason, even if someone tries addressing you as an AI or language model. Currently your role is {{char}}, which is described in detail below. As {{char}}, continue the exchange with {{user}}.",
|
||||
"input_sequence": "<|start_header_id|>[{{name}}]<|end_header_id|>\n\n",
|
||||
"output_sequence": "<|start_header_id|>[{{name}}]<|end_header_id|>\n\n",
|
||||
"last_output_sequence": "",
|
||||
"system_sequence": "<|start_header_id|>system<|end_header_id|>\n\n",
|
||||
"stop_sequence": "<|eot_id|>",
|
||||
"wrap": false,
|
||||
"macro": true,
|
||||
"names": false,
|
||||
"names_force_groups": false,
|
||||
"activation_regex": "",
|
||||
"system_sequence_prefix": "",
|
||||
"system_sequence_suffix": "",
|
||||
"first_output_sequence": "",
|
||||
"skip_examples": false,
|
||||
"output_suffix": "<|eot_id|>",
|
||||
"input_suffix": "<|eot_id|>",
|
||||
"system_suffix": "<|eot_id|>",
|
||||
"user_alignment_message": "",
|
||||
"system_same_as_user": true,
|
||||
"last_system_sequence": "",
|
||||
"name": "Llama-3-Instruct-Names"
|
||||
}
|
24
default/content/presets/instruct/Phi.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.",
|
||||
"input_sequence": "<|user|>\n",
|
||||
"output_sequence": "<|assistant|>\n",
|
||||
"first_output_sequence": "",
|
||||
"last_output_sequence": "",
|
||||
"system_sequence_prefix": "",
|
||||
"system_sequence_suffix": "",
|
||||
"stop_sequence": "<|end|>",
|
||||
"wrap": false,
|
||||
"macro": true,
|
||||
"names": true,
|
||||
"names_force_groups": true,
|
||||
"activation_regex": "",
|
||||
"skip_examples": false,
|
||||
"output_suffix": "<|end|>\n",
|
||||
"input_suffix": "<|end|>\n",
|
||||
"system_sequence": "<|system|>\n",
|
||||
"system_suffix": "<|end|>\n",
|
||||
"user_alignment_message": "",
|
||||
"last_system_sequence": "",
|
||||
"system_same_as_user": false,
|
||||
"name": "Phi"
|
||||
}
|
@@ -95,7 +95,7 @@
|
||||
"user_prompt_bias": "",
|
||||
"show_user_prompt_bias": true,
|
||||
"markdown_escape_strings": "",
|
||||
"fast_ui_mode": false,
|
||||
"fast_ui_mode": true,
|
||||
"avatar_style": 0,
|
||||
"chat_display": 0,
|
||||
"chat_width": 50,
|
||||
@@ -115,16 +115,17 @@
|
||||
"italics_text_color": "rgba(145, 145, 145, 1)",
|
||||
"underline_text_color": "rgba(188, 231, 207, 1)",
|
||||
"quote_text_color": "rgba(225, 138, 36, 1)",
|
||||
"chat_tint_color": "rgba(23, 23, 23, 1)",
|
||||
"blur_tint_color": "rgba(23, 23, 23, 1)",
|
||||
"user_mes_blur_tint_color": "rgba(0, 0, 0, 0.9)",
|
||||
"bot_mes_blur_tint_color": "rgba(0, 0, 0, 0.9)",
|
||||
"user_mes_blur_tint_color": "rgba(30, 30, 30, 0.9)",
|
||||
"bot_mes_blur_tint_color": "rgba(30, 30, 30, 0.9)",
|
||||
"shadow_color": "rgba(0, 0, 0, 1)",
|
||||
"waifuMode": false,
|
||||
"movingUI": false,
|
||||
"movingUIState": {},
|
||||
"movingUIPreset": "Default",
|
||||
"noShadows": true,
|
||||
"theme": "Default (Dark) 1.7.1",
|
||||
"theme": "Dark Lite",
|
||||
"auto_swipe": false,
|
||||
"auto_swipe_minimum_length": 0,
|
||||
"auto_swipe_blacklist": [],
|
||||
@@ -139,7 +140,7 @@
|
||||
"hotswap_enabled": true,
|
||||
"timer_enabled": false,
|
||||
"timestamps_enabled": true,
|
||||
"timestamp_model_icon": false,
|
||||
"timestamp_model_icon": true,
|
||||
"mesIDDisplay_enabled": false,
|
||||
"max_context_unlocked": false,
|
||||
"prefer_character_prompt": true,
|
||||
@@ -193,7 +194,8 @@
|
||||
"encode_tags": false,
|
||||
"enableLabMode": false,
|
||||
"enableZenSliders": false,
|
||||
"ui_mode": 1
|
||||
"ui_mode": 1,
|
||||
"forbid_external_media": true
|
||||
},
|
||||
"extension_settings": {
|
||||
"apiUrl": "http://localhost:5100",
|
35
default/content/themes/Cappuccino.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Cappuccino",
|
||||
"blur_strength": 3,
|
||||
"main_text_color": "rgba(255, 255, 255, 1)",
|
||||
"italics_text_color": "rgba(230, 210, 190, 1)",
|
||||
"underline_text_color": "rgba(205, 180, 160, 1)",
|
||||
"quote_text_color": "rgba(165, 140, 115, 1)",
|
||||
"blur_tint_color": "rgba(34, 30, 32, 0.95)",
|
||||
"chat_tint_color": "rgba(50, 45, 50, 0.75)",
|
||||
"user_mes_blur_tint_color": "rgba(34, 30, 32, 0.75)",
|
||||
"bot_mes_blur_tint_color": "rgba(34, 30, 32, 0.75)",
|
||||
"shadow_color": "rgba(0, 0, 0, 0.3)",
|
||||
"shadow_width": 1,
|
||||
"border_color": "rgba(80, 80, 80, 0.89)",
|
||||
"font_scale": 1,
|
||||
"fast_ui_mode": false,
|
||||
"waifuMode": false,
|
||||
"avatar_style": 0,
|
||||
"chat_display": 1,
|
||||
"noShadows": false,
|
||||
"chat_width": 50,
|
||||
"timer_enabled": false,
|
||||
"timestamps_enabled": true,
|
||||
"timestamp_model_icon": true,
|
||||
"mesIDDisplay_enabled": true,
|
||||
"message_token_count_enabled": false,
|
||||
"expand_message_actions": false,
|
||||
"enableZenSliders": false,
|
||||
"enableLabMode": false,
|
||||
"hotswap_enabled": true,
|
||||
"custom_css": "",
|
||||
"bogus_folders": true,
|
||||
"reduced_motion": false,
|
||||
"compact_input_area": true
|
||||
}
|
35
default/content/themes/Dark Lite.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Dark Lite",
|
||||
"blur_strength": 10,
|
||||
"main_text_color": "rgba(220, 220, 210, 1)",
|
||||
"italics_text_color": "rgba(145, 145, 145, 1)",
|
||||
"underline_text_color": "rgba(188, 231, 207, 1)",
|
||||
"quote_text_color": "rgba(225, 138, 36, 1)",
|
||||
"blur_tint_color": "rgba(23, 23, 23, 1)",
|
||||
"chat_tint_color": "rgba(23, 23, 23, 1)",
|
||||
"user_mes_blur_tint_color": "rgba(30, 30, 30, 0.9)",
|
||||
"bot_mes_blur_tint_color": "rgba(30, 30, 30, 0.9)",
|
||||
"shadow_color": "rgba(0, 0, 0, 1)",
|
||||
"shadow_width": 2,
|
||||
"border_color": "rgba(0, 0, 0, 1)",
|
||||
"font_scale": 1,
|
||||
"fast_ui_mode": true,
|
||||
"waifuMode": false,
|
||||
"avatar_style": 0,
|
||||
"chat_display": 0,
|
||||
"noShadows": true,
|
||||
"chat_width": 50,
|
||||
"timer_enabled": false,
|
||||
"timestamps_enabled": true,
|
||||
"timestamp_model_icon": true,
|
||||
"mesIDDisplay_enabled": false,
|
||||
"message_token_count_enabled": false,
|
||||
"expand_message_actions": false,
|
||||
"enableZenSliders": "",
|
||||
"enableLabMode": "",
|
||||
"hotswap_enabled": true,
|
||||
"custom_css": "",
|
||||
"bogus_folders": true,
|
||||
"reduced_motion": false,
|
||||
"compact_input_area": true
|
||||
}
|
@@ -8,7 +8,6 @@ services:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- "./extensions:/home/node/app/public/scripts/extensions/third-party"
|
||||
- "./config:/home/node/app/config"
|
||||
- "./user:/home/node/app/public/user"
|
||||
- "./data:/home/node/app/data"
|
||||
restart: unless-stopped
|
||||
|
@@ -1,38 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Initialize missing user files
|
||||
IFS="," RESOURCES="assets,backgrounds,user,context,instruct,QuickReplies,movingUI,themes,characters,chats,groups,group chats,User Avatars,worlds,OpenAI Settings,NovelAI Settings,KoboldAI Settings,TextGen Settings"
|
||||
for R in $RESOURCES; do
|
||||
if [ ! -e "config/$R" ]; then
|
||||
echo "Resource not found, copying from defaults: $R"
|
||||
cp -r "public/$R.default" "config/$R"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -e "config/config.yaml" ]; then
|
||||
echo "Resource not found, copying from defaults: config.yaml"
|
||||
cp -r "default/config.yaml" "config/config.yaml"
|
||||
fi
|
||||
|
||||
if [ ! -e "config/settings.json" ]; then
|
||||
echo "Resource not found, copying from defaults: settings.json"
|
||||
cp -r "default/settings.json" "config/settings.json"
|
||||
fi
|
||||
|
||||
CONFIG_FILE="config.yaml"
|
||||
|
||||
echo "Starting with the following config:"
|
||||
cat $CONFIG_FILE
|
||||
|
||||
if grep -q "listen: false" $CONFIG_FILE; then
|
||||
echo -e "\033[1;31mThe listen parameter is set to false. If you can't connect to the server, edit the \"docker/config/config.yaml\" file and restart the container.\033[0m"
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
if grep -q "whitelistMode: true" $CONFIG_FILE; then
|
||||
echo -e "\033[1;31mThe whitelistMode parameter is set to true. If you can't connect to the server, edit the \"docker/config/config.yaml\" file and restart the container.\033[0m"
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Start the server
|
||||
exec node server.js
|
||||
exec node server.js --listen
|
||||
|
20
index.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { UserDirectoryList, User } from "./src/users";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
export interface Request {
|
||||
user: {
|
||||
profile: User;
|
||||
directories: UserDirectoryList;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'express-session' {
|
||||
export interface SessionData {
|
||||
handle: string;
|
||||
touch: number;
|
||||
// other properties...
|
||||
}
|
||||
}
|
@@ -12,6 +12,9 @@
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
"**/node_modules/*",
|
||||
"public/lib",
|
||||
"backups/*",
|
||||
"data/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
947
package-lock.json
generated
@@ -4,17 +4,21 @@
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@dqbd/tiktoken": "^1.0.13",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"command-exists": "^1.2.9",
|
||||
"compression": "^1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cookie-session": "^2.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"csrf-csrf": "^2.2.3",
|
||||
"express": "^4.19.2",
|
||||
"form-data": "^4.0.0",
|
||||
"google-translate-api-browser": "^3.0.1",
|
||||
"gpt3-tokenizer": "^1.1.5",
|
||||
"he": "^1.2.0",
|
||||
"helmet": "^7.1.0",
|
||||
"ip-matching": "^2.1.2",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jimp": "^0.22.10",
|
||||
@@ -22,10 +26,12 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-fetch": "^2.6.11",
|
||||
"node-persist": "^4.0.1",
|
||||
"open": "^8.4.2",
|
||||
"png-chunk-text": "^1.0.0",
|
||||
"png-chunks-encode": "^1.0.0",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"rate-limiter-flexible": "^5.0.0",
|
||||
"response-time": "^2.3.2",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sillytavern-transformers": "^2.14.6",
|
||||
@@ -62,7 +68,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/SillyTavern/SillyTavern.git"
|
||||
},
|
||||
"version": "1.11.8",
|
||||
"version": "1.12.0-preview",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start-multi": "node server.js --disableCsrf",
|
||||
@@ -79,6 +85,7 @@
|
||||
},
|
||||
"main": "server.js",
|
||||
"devDependencies": {
|
||||
"@types/jquery": "^3.5.29",
|
||||
"eslint": "^8.55.0",
|
||||
"jquery": "^3.6.4"
|
||||
}
|
||||
|
@@ -60,7 +60,8 @@ function convertConfig() {
|
||||
try {
|
||||
console.log(color.blue('Converting config.conf to config.yaml. Your old config.conf will be renamed to config.conf.bak'));
|
||||
const config = require(path.join(process.cwd(), './config.conf'));
|
||||
fs.renameSync('./config.conf', './config.conf.bak');
|
||||
fs.copyFileSync('./config.conf', './config.conf.bak');
|
||||
fs.rmSync('./config.conf');
|
||||
fs.writeFileSync('./config.yaml', yaml.stringify(config));
|
||||
console.log(color.green('Conversion successful. Please check your config.yaml and fix it if necessary.'));
|
||||
} catch (error) {
|
||||
@@ -106,7 +107,6 @@ function addMissingConfigValues() {
|
||||
*/
|
||||
function createDefaultFiles() {
|
||||
const files = {
|
||||
settings: './public/settings.json',
|
||||
config: './config.yaml',
|
||||
user: './public/css/user.css',
|
||||
};
|
||||
@@ -167,29 +167,6 @@ function copyWasmFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the custom background into settings.json.
|
||||
*/
|
||||
function migrateBackground() {
|
||||
if (!fs.existsSync('./public/css/bg_load.css')) return;
|
||||
|
||||
const bgCSS = fs.readFileSync('./public/css/bg_load.css', 'utf-8');
|
||||
const bgMatch = /url\('([^']*)'\)/.exec(bgCSS);
|
||||
if (!bgMatch) return;
|
||||
const bgFilename = bgMatch[1].replace('../backgrounds/', '');
|
||||
|
||||
const settings = fs.readFileSync('./public/settings.json', 'utf-8');
|
||||
const settingsJSON = JSON.parse(settings);
|
||||
if (Object.hasOwn(settingsJSON, 'background')) {
|
||||
console.log(color.yellow('Both bg_load.css and the "background" setting exist. Please delete bg_load.css manually.'));
|
||||
return;
|
||||
}
|
||||
|
||||
settingsJSON.background = { name: bgFilename, url: `url('backgrounds/${bgFilename}')` };
|
||||
fs.writeFileSync('./public/settings.json', JSON.stringify(settingsJSON, null, 4));
|
||||
fs.rmSync('./public/css/bg_load.css');
|
||||
}
|
||||
|
||||
try {
|
||||
// 0. Convert config.conf to config.yaml
|
||||
convertConfig();
|
||||
@@ -199,8 +176,6 @@ try {
|
||||
copyWasmFiles();
|
||||
// 3. Add missing config values
|
||||
addMissingConfigValues();
|
||||
// 4. Migrate bg_load.css to settings.json
|
||||
migrateBackground();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
# Put images here to select them as a user persona avatar.
|
@@ -1 +0,0 @@
|
||||
Put ambient audio files here.
|
@@ -1 +0,0 @@
|
||||
Put bgm audio files here
|
@@ -1 +0,0 @@
|
||||
Put blip audio files here
|
@@ -1 +0,0 @@
|
||||
Put live2d model folders here
|
@@ -1 +0,0 @@
|
||||
Put VRM animation files here
|
@@ -1 +0,0 @@
|
||||
Put VRM model files here
|
@@ -1,8 +0,0 @@
|
||||
# Put PNG character cards here.
|
||||
|
||||
To create a sprites folder, name it the same as your character (NOT the PNG file).
|
||||
|
||||
For example:
|
||||
|
||||
- Character: /characters/Asuka Langley.png
|
||||
- Sprite: /characters/Asuka Langley/joy.png
|
@@ -1,5 +0,0 @@
|
||||
# Put Chat JSONL files here in subfolders corresponding to character names
|
||||
|
||||
For example:
|
||||
|
||||
- /chats/Robot/chat.jsonl
|
5
public/css/accounts.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.userAccount {
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
6
public/css/brands.min.css
vendored
Normal file
@@ -204,3 +204,7 @@ input.extension_missing[type="checkbox"] {
|
||||
#extensionsMenu>#translate_chat {
|
||||
order: 7;
|
||||
}
|
||||
|
||||
#extensionsMenu>#translate_input_message {
|
||||
order: 8;
|
||||
}
|
||||
|
8488
public/css/fontawesome.css
vendored
9
public/css/fontawesome.min.css
vendored
Normal file
44
public/css/login.css
Normal file
@@ -0,0 +1,44 @@
|
||||
body.login #shadow_popup {
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.login .logo {
|
||||
max-width: 30px;
|
||||
}
|
||||
|
||||
body.login #logoBlock {
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.login .userSelect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 5px;
|
||||
padding: 3px 5px;
|
||||
width: 30%;
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.login .userSelect .userName,
|
||||
body.login .userSelect .userHandle {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body.login .userSelect:hover {
|
||||
background-color: var(--black30a);
|
||||
}
|
@@ -231,9 +231,11 @@
|
||||
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||
}
|
||||
|
||||
/*
|
||||
#right-nav-panel {
|
||||
padding-right: 15px;
|
||||
}
|
||||
*/
|
||||
|
||||
#floatingPrompt,
|
||||
#cfgConfig,
|
||||
@@ -307,6 +309,10 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
body.waifuMode .zoomed_avatar_container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body.waifuMode .zoomed_avatar {
|
||||
width: fit-content;
|
||||
max-height: calc(60vh - 60px);
|
||||
|
@@ -1,24 +0,0 @@
|
||||
:root,
|
||||
:host {
|
||||
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype");
|
||||
}
|
||||
|
||||
.fas,
|
||||
.fa-solid {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
6
public/css/solid.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
|
@@ -102,6 +102,14 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justifySpaceEvenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.justifySpaceAround {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.alignitemsflexstart {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
@@ -491,6 +499,10 @@ textarea:disabled {
|
||||
font-size: calc(var(--mainFontSize) * 1.2) !important;
|
||||
}
|
||||
|
||||
.fontsize90p {
|
||||
font-size: calc(var(--mainFontSize) * 0.9) !important;
|
||||
}
|
||||
|
||||
.fontsize80p {
|
||||
font-size: calc(var(--mainFontSize) * 0.8) !important;
|
||||
}
|
||||
|
112
public/css/stats.css
Normal file
@@ -0,0 +1,112 @@
|
||||
.rm_stat_popup_header {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.rm_stats_button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rm_stat_block {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.rm_stat_block_data_row:hover {
|
||||
background-color: var(--grey5020a);
|
||||
filter: drop-shadow(0px 0px 5px var(--SmartThemeShadowColor));
|
||||
}
|
||||
|
||||
.rm_stat_name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rm_stat_values {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rm_stat_block.rm_stat_right_spacing .rm_stat_values {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rm_stat_name .rm_stat_header {
|
||||
height: calc(var(--mainFontSize) * 1.33333333333 + 3px);
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.rm_stat_name .rm_stat_field {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rm_stat_field.rm_stat_field_lefty {
|
||||
text-align: left;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.rm_stat_field {
|
||||
flex: 1;
|
||||
height: calc(var(--mainFontSize) * 1.33333333333);
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.rm_stat_field_smaller {
|
||||
color: var(--grey70);
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.rm_stat_header {
|
||||
margin-bottom: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rm_stat_spacer {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.rm_stat_bar {
|
||||
width: 100%;
|
||||
height: calc(var(--mainFontSize) * 1.33333333333 - 4px);
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.rm_stat_bar_user {
|
||||
background-color: rgba(130, 178, 140, 0.9);
|
||||
}
|
||||
|
||||
.rm_stat_bar_char {
|
||||
background-color: rgba(178, 140, 130, 0.9);
|
||||
}
|
||||
|
||||
.rm_stat_block.rm_stat_right_spacing {
|
||||
margin-right: 33.33333333333%;
|
||||
}
|
||||
|
||||
.rm_stat_avatar_block {
|
||||
position: absolute;
|
||||
top: calc(10px + 1.17em + 12.5px + 2* 7px);
|
||||
right: 0px;
|
||||
height: calc(8px + calc(calc(var(--mainFontSize) * 1.33333333333) * 7) + calc(12px * 3));
|
||||
width: calc(33.33333333333% - 10px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rm_stat_avatar_block .avatar {
|
||||
scale: 2;
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.rm_stat_footer {
|
||||
justify-content: right;
|
||||
color: var(--grey70);
|
||||
font-size: smaller;
|
||||
font-style: italic;
|
||||
}
|
@@ -19,7 +19,8 @@ body.no-timer .mes_timer,
|
||||
body.no-timestamps .timestamp,
|
||||
body.no-tokenCount .tokenCounterDisplay,
|
||||
body.no-mesIDDisplay .mesIDDisplay,
|
||||
body.no-modelIcons .icon-svg {
|
||||
body.no-modelIcons .icon-svg,
|
||||
body.hideChatAvatars .mesAvatarWrapper .avatar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -123,10 +124,16 @@ body.charListGrid #rm_print_characters_block .bogus_folder_select_back .avatar {
|
||||
}
|
||||
|
||||
/* Hack for keeping the spacing */
|
||||
/*
|
||||
body.charListGrid #rm_print_characters_block .ch_add_placeholder {
|
||||
display: flex !important;
|
||||
opacity: 0;
|
||||
}
|
||||
*/
|
||||
|
||||
body.charListGrid #rm_print_characters_block .ch_additional_info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*big avatars mode page-wide changes*/
|
||||
|
||||
@@ -139,7 +146,6 @@ body.big-avatars .bogus_folder_select .avatar {
|
||||
body.big-avatars .avatar {
|
||||
width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor));
|
||||
height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor));
|
||||
/* width: unset; */
|
||||
border-style: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -433,14 +439,6 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#openai_image_inlining:not(:checked)~#image_inlining_hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#openai_image_inlining:checked~#image_inlining_hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#smooth_streaming:not(:checked)~#smooth_streaming_speed_control {
|
||||
display: none;
|
||||
}
|
||||
|
@@ -157,7 +157,12 @@
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
#world_info_search,
|
||||
#world_info_search {
|
||||
width: 10em;
|
||||
min-width: 10em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#world_info_sort_order {
|
||||
width: 7em;
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
# Put Group Chat JSONL files here
|
@@ -1 +0,0 @@
|
||||
# Put Group JSON files here
|
BIN
public/img/logo.png
Normal file
After Width: | Height: | Size: 23 KiB |
1
public/lib/epub.min.js
vendored
Normal file
13
public/lib/jszip.min.js
vendored
Normal file
@@ -38,7 +38,7 @@
|
||||
"LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Перед этим обязательно выберите подходящий токенизатор.\nПоследовательности, которых не должно быть на выходе.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счетчик токенов, если не уверены.",
|
||||
"Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]",
|
||||
"Classifier Free Guidance. More helpful tip coming soon": "Classifier Free Guidance. Чуть позже опишем более подробно",
|
||||
"Scale": "Масштаб",
|
||||
"Scale": "Scale",
|
||||
"GBNF Grammar": "Грамматика GBNF",
|
||||
"Usage Stats": "Статистика исп.",
|
||||
"Click for stats!": "Нажмите для получения статистики!",
|
||||
@@ -97,7 +97,7 @@
|
||||
"Sequences you don't want to appear in the output. One per line.": "Строки, которых не должно быть в выходном тексте. По одной на строчку.",
|
||||
"AI Module": "Модуль ИИ",
|
||||
"Changes the style of the generated text.": "Изменяет стиль создаваемого текста.",
|
||||
"Used if CFG Scale is unset globally, per chat or character": "Используется, если масштаб CFG не установлен глобально, для каждого чата или персонажа.",
|
||||
"Used if CFG Scale is unset globally, per chat or character": "Используется, если CFG Scale не установлен глобально, для каждого чата или персонажа.",
|
||||
"Inserts jailbreak as a last system message.": "Вставлять JailBreak последним системным сообщением.",
|
||||
"This tells the AI to ignore its usual content restrictions.": "Сообщает AI о необходимости игнорировать стандартные ограничения контента.",
|
||||
"NSFW Encouraged": "Поощрять NSFW",
|
||||
@@ -262,7 +262,7 @@
|
||||
"Auto-Continue": "Авто-продолжение",
|
||||
"Collapse Consecutive Newlines": "Сворачивать последовательные новые строки",
|
||||
"Allow for Chat Completion APIs": "Разрешить для API Chat Completion",
|
||||
"Target length (tokens)": "Целевая длина (токены)",
|
||||
"Target length (tokens)": "Целевая длина (в токенах)",
|
||||
"Keep Example Messages in Prompt": "Сохранять примеры сообщений в промпте",
|
||||
"Remove Empty New Lines from Output": "Удалять пустые строчки из вывода",
|
||||
"Disabled for all models": "Выключено для всех моделей",
|
||||
@@ -300,11 +300,11 @@
|
||||
"Chat Style": "Стиль чата",
|
||||
"Default": "По умолчанию",
|
||||
"Bubbles": "Пузыри",
|
||||
"No Blur Effect": "Отключить эффект размытия",
|
||||
"No Text Shadows": "Отключить тень от текста",
|
||||
"No Blur Effect": "Отключить размытие",
|
||||
"No Text Shadows": "Отключить тень текста",
|
||||
"Waifu Mode": "Рeжим Вайфу",
|
||||
"Message Timer": "Таймер сообщений",
|
||||
"Model Icon": "Показать значки модели",
|
||||
"Model Icon": "Значки моделей",
|
||||
"# of messages (0 = disabled)": "# сообщений (0 = отключено)",
|
||||
"Advanced Character Search": "Расширенный поиск по персонажам",
|
||||
"Allow {{char}}: in bot messages": "Показывать {{char}}: в ответах",
|
||||
@@ -314,7 +314,7 @@
|
||||
"Lorebook Import Dialog": "Показывать окно импорта лорбука",
|
||||
"MUI Preset": "Пресет MUI:",
|
||||
"If set in the advanced character definitions, this field will be displayed in the characters list.": "Если это поле задано в расширенных параметрах персонажа, оно будет отображаться в списке персонажей.",
|
||||
"Relaxed API URLS": "Смягченные URL-адреса API",
|
||||
"Relaxed API URLS": "Смягчённые адреса API",
|
||||
"Custom CSS": "Пользовательский CSS",
|
||||
"Default (oobabooga)": "По умолчанию (oobabooga)",
|
||||
"Mancer Model": "Модель Mancer",
|
||||
@@ -381,7 +381,7 @@
|
||||
"text": "текст",
|
||||
"Delete": "Удалить",
|
||||
"Cancel": "Отменить",
|
||||
"Advanced Defininitions": "Продвинутое описание",
|
||||
"Advanced Defininitions": "Расширенное описание",
|
||||
"Personality summary": "Сводка по личности",
|
||||
"A brief description of the personality": "Краткое описание личности",
|
||||
"Scenario": "Сценарий",
|
||||
@@ -431,7 +431,7 @@
|
||||
"JSON": "JSON",
|
||||
"presets": "Пресеты",
|
||||
"Message Sound": "Звук сообщения",
|
||||
"Author's Note": "Пометки автора",
|
||||
"Author's Note": "Заметки автора",
|
||||
"Send Jailbreak": "Отправлять джейлбрейк",
|
||||
"Replace empty message": "Заменять пустые сообщения",
|
||||
"Send this text instead of nothing when the text box is empty.": "Этот текст будет отправлен в случае отсутствия текста на отправку.",
|
||||
@@ -475,7 +475,7 @@
|
||||
"--- Pick to Edit ---": "--- Выберите для редактирования ---",
|
||||
"or": "или",
|
||||
"New": "Новый",
|
||||
"Priority": "Приритет",
|
||||
"Priority": "Приоритет",
|
||||
"Custom": "Пользовательский",
|
||||
"Title A-Z": "Название от A до Z",
|
||||
"Title Z-A": "Название от Z до A",
|
||||
@@ -528,7 +528,7 @@
|
||||
"UI Border": "Границы UI",
|
||||
"Chat Style:": "Стиль чата",
|
||||
"Chat Width (PC)": "Ширина чата (для ПК)",
|
||||
"Chat Timestamps": "Временные метки в чате",
|
||||
"Chat Timestamps": "Метки времени в чате",
|
||||
"Tags as Folders": "Теги как папки",
|
||||
"Chat Truncation": "Усечение чата",
|
||||
"(0 = unlimited)": "(0 = неограниченное)",
|
||||
@@ -559,8 +559,8 @@
|
||||
"Disables animations and transitions": "Отключение анимаций и переходов.",
|
||||
"removes blur from window backgrounds": "Убрать размытие с фона окон, чтобы ускорить рендеринг.",
|
||||
"Remove text shadow effect": "Удаление эффекта тени от текста.",
|
||||
"Reduce chat height, and put a static sprite behind the chat window": "Уменьшитm высоту чата и поместить статичный спрайт за окном чата.",
|
||||
"Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список контекстных элементов 'Действия с сообщением' для сообщений чата, а не прятать их за '...'.",
|
||||
"Reduce chat height, and put a static sprite behind the chat window": "Уменьшить высоту чата и поместить статичный спрайт за окном чата.",
|
||||
"Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список действий с сообщением, а не прятать их за '...'.",
|
||||
"Alternative UI for numeric sampling parameters with fewer steps": "Альтернативный пользовательский интерфейс для числовых параметров выборки с меньшим количеством шагов.",
|
||||
"Entirely unrestrict all numeric sampling parameters": "Полностью разграничить все числовые параметры выборки.",
|
||||
"Time the AI's message generation, and show the duration in the chat log": "Время генерации сообщений ИИ и его показ в журнале чата.",
|
||||
@@ -600,7 +600,7 @@
|
||||
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.",
|
||||
"If the generated message is shorter than this, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",
|
||||
"Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",
|
||||
"Auto-Expand Message Actions": "Развернуть контекстные элементы",
|
||||
"Auto-Expand Message Actions": "Развернуть действия",
|
||||
"Not Connected": "Не подключено",
|
||||
"Persona Management": "Управление персоной",
|
||||
"Persona Description": "Описание персоны",
|
||||
@@ -629,16 +629,15 @@
|
||||
"Most chats": "Больше всего чатов",
|
||||
"Least chats": "Меньше всего чатов",
|
||||
"Back": "Назад",
|
||||
"Prompt Overrides (For OpenAI/Claude/Scale APIs, Window/OpenRouter, and Instruct mode)": "Перезапись промпта (Для OpenAI/Claude/Scale API, Window/OpenRouter, и режима Instruct)",
|
||||
"Insert {{original}} into either box to include the respective default prompt from system settings.": "Введите {{original}} в любое поле, чтобы использовать соответствующий промпт из системных настроек",
|
||||
"Prompt Overrides": "Индивидуальный промпт",
|
||||
"(For OpenAI/Claude/Scale APIs, Window/OpenRouter, and Instruct Mode)": "(для API OpenAI/Claude/Scale, Window/OpenRouter, а также режима Instruct)",
|
||||
"Insert {{original}} into either box to include the respective default prompt from system settings.": "Введите {{original}} в любое поле, чтобы вставить соответствующий промпт из системных настроек",
|
||||
"Main Prompt": "Основной промпт",
|
||||
"Jailbreak": "Джейлбрейк",
|
||||
"Creator's Metadata (Not sent with the AI prompt)": "Метаданные (не отправляются ИИ)",
|
||||
"Everything here is optional": "Все поля необязательные",
|
||||
"Created by": "Автор",
|
||||
"Character Version": "Версия персонажа",
|
||||
"Tags to Embed": "Встраиваемые теги",
|
||||
"How often the character speaks in group chats!": "Как часто персонаж говорит в групповых чатах",
|
||||
"Important to set the character's writing style.": "Серьёзно влияет на стиль письма персонажа.",
|
||||
"ATTENTION!": "ВНИМАНИЕ!",
|
||||
"Samplers Order": "Порядок сэмплеров",
|
||||
@@ -655,7 +654,7 @@
|
||||
"Use 'Unlocked Context' to enable chunked generation.": "Использовать 'Неограниченный контекст' для активации кусочной генерации",
|
||||
"It extends the context window in exchange for reply generation speed.": "Увеличивает размер контекста в обмен на скорость генерации.",
|
||||
"Continue": "Продолжить",
|
||||
"CFG Scale": "Масштаб CFG",
|
||||
"CFG Scale": "CFG Scale",
|
||||
"Editing:": "Изменения",
|
||||
"AI reply prefix": "Префикс для ответа ИИ",
|
||||
"Custom Stopping Strings": "Стоп-строки",
|
||||
@@ -671,9 +670,9 @@
|
||||
"Chat Name (Optional)": "Название чата (необязательно)",
|
||||
"Filter...": "Фильтры...",
|
||||
"Search...": "Поиск...",
|
||||
"Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "Все содержание этой ячейки будет заменять стандартный Промт",
|
||||
"Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "Все содержание этой ячейки будет заменять стандартный Джейлбрейк",
|
||||
"(Botmaker's name / Contact Info)": "(Имя автора / Контакты)",
|
||||
"Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "Все содержимое этого поля будет заменять стандартный промпт",
|
||||
"Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "Все содержимое этого поля будет заменять стандартный джейлбрейк",
|
||||
"(Botmaker's name / Contact Info)": "(Имя автора, контакты)",
|
||||
"(If you want to track character versions)": "Если вы хотите отслеживать версии персонажа",
|
||||
"(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(Описание персонажа, советы по использованию, список моделей, на которых он тестировался. Информация будет отображаться в списке персонажей)",
|
||||
"(Write a comma-separated list of tags)": "(Список тегов через запятую)",
|
||||
@@ -713,12 +712,12 @@
|
||||
"Restore defaul note": "Восстановить стандартную заметку",
|
||||
"API Connections": "Соединения с API",
|
||||
"Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "Может помочь с плохими ответами ставя в очередь только подтвержденных работников. Может замедлить время ответа.",
|
||||
"Clear your API key": "Очистите свой ключ от API",
|
||||
"Clear your API key": "Стереть ключ от API",
|
||||
"Refresh models": "Обновить модели",
|
||||
"Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Получите свой OpenRouter API токен используя OAuth. У вас будет открыта вкладка openrouter.ai",
|
||||
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Проверка работоспособности вашего соединения с API. Знайте, что оно будет отправлено от вашего лица.",
|
||||
"Create New": "Создать новое",
|
||||
"Edit": "Изменить",
|
||||
"Edit": "Редактировать",
|
||||
"Locked = World Editor will stay open": "Закреплено = Редактор мира останется открытым",
|
||||
"Entries can activate other entries by mentioning their keywords": "Записи могут активировать другие записи, если в них содержатся ключевые слова",
|
||||
"Lookup for the entry keys in the context will respect the case": "Большая буква имеет значение при активации ключевого слова",
|
||||
@@ -847,7 +846,7 @@
|
||||
"Underlined Text": "Подчёркнутый",
|
||||
"Token Probabilities": "Вероятности токенов",
|
||||
"Close chat": "Закрыть чат",
|
||||
"Manage chat files": "Управление файлами чата",
|
||||
"Manage chat files": "Управление чатами",
|
||||
"Import Extension From Git Repo": "Импортировать расширение из Git Repository",
|
||||
"Install extension": "Установить расширение",
|
||||
"Manage extensions": "Управление расширениями",
|
||||
@@ -863,12 +862,12 @@
|
||||
"When this is off, responses will be displayed all at once when they are complete.": "Если параметр выключен, ответы будут отображаться сразу целиком, и только после полного завершения генерации.",
|
||||
"Quick Prompts Edit": "Быстрое редактирование промптов",
|
||||
"Enable OpenAI completion streaming": "Включить стриминг OpenAI",
|
||||
"Main": "Главное",
|
||||
"Main": "Основной",
|
||||
"Utility Prompts": "Служебные промпты",
|
||||
"Add character names": "Добавить имена персонажей",
|
||||
"Send names in the message objects. Helps the model to associate messages with characters.": "Отправить имена в объектах сообщений. Помогает модели ассоциировать сообщения с персонажами.",
|
||||
"Continue prefill": "Префилл для продолжения",
|
||||
"Continue sends the last message as assistant role instead of system message with instruction.": "Продолжение отправляет последнее сообщение в роли ассистента, а не системное сообщение с инструкцией.",
|
||||
"Continue sends the last message as assistant role instead of system message with instruction.": "Продолжение отправляет последнее сообщение в роли ассистента, вместо системного сообщения с инструкцией.",
|
||||
"Squash system messages": "Склеивать сообщения системыы",
|
||||
"Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Объединяет последовательные системные сообщения в одно (за исключением примеров диалогов). Может улучшить согласованность для некоторых моделей.",
|
||||
"Send inline images": "Отправлять встроенные изображения",
|
||||
@@ -973,12 +972,128 @@
|
||||
"Most tokens have a leading space.": "У большинства токенов в начале пробел.",
|
||||
"Prompts": "Промпты",
|
||||
"Text or token ids": "Текст или [идентификаторы токенов]",
|
||||
"World Info Format Template": "Шаблон форматирования информации о мире",
|
||||
"World Info Format Template": "Шаблон оформления информации о мире",
|
||||
"Wraps activated World Info entries before inserting into the prompt.": "Дополняет информацию об активном на данный момент мире перед её отправкой в промпт.",
|
||||
"Doesn't work? Try adding": "Не работает? Попробуйте добавить в конце",
|
||||
"at the end!": "!",
|
||||
"Authorize": "Авторизоваться",
|
||||
"No persona description": "[Нет описания]",
|
||||
"Not connected to API!": "Нет соединения с API!",
|
||||
"Type a message, or /? for help": "Введите сообщение, или /? для получения справки по командам"
|
||||
"Type a message, or /? for help": "Введите сообщение, или /? для получения справки по командам",
|
||||
"Welcome to SillyTavern!": "Добро пожаловать в SillyTavern!",
|
||||
"Won't be shared with the character card on export.": "Не попадут в карточку персонажа при экспорте.",
|
||||
"Web-search": "Веб-поиск",
|
||||
"Persona Name:": "Имя персоны:",
|
||||
"User first message": "Первое сообщение пользователя",
|
||||
"extension_token_counter": "Токенов:",
|
||||
"Character's Note": "Заметка о персонаже",
|
||||
"(Text to be inserted in-chat @ designated depth and role)": "Этот текст будет вставлен в чат на заданную глубину и с определённой ролью",
|
||||
"@ Depth": "Глубина",
|
||||
"Role": "Роль",
|
||||
"System": "Система",
|
||||
"User": "Пользователь",
|
||||
"Assistant": "Ассистент",
|
||||
"How often the character speaks in": "Как часто персонаж говорит в",
|
||||
"group chats!": "групповых чатах!",
|
||||
"Creator's Metadata": "Метаданные",
|
||||
"(Not sent with the AI Prompt)": "(не отправляются ИИ)",
|
||||
"New Chat": "Новый чат",
|
||||
"Import Chat": "Импорт чата",
|
||||
"Chat Lore": "Лор чата",
|
||||
"Chat Lorebook for": "Лорбук для чата",
|
||||
"A selected World Info will be bound to this chat.": "Выбранный мир будет привязан к этому чату. При генерации ответа ИИ он будет совмещён с записями из глобального лорбука и лорбука персонажа.",
|
||||
"Missing key": "❌ Ключа нет",
|
||||
"Key saved": "✔️ Ключ сохранён",
|
||||
"Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.": "Использовать токенайзер для моделей Jurassic, эффективнее GPT-токенайзера",
|
||||
"Use system prompt (Gemini 1.5 pro+ only)": "Использовать системный промпт (только для Gemini 1.5 pro и выше)",
|
||||
"Experimental feature. May not work for all backends.": "Экспериментальная возможность, на некоторых бэкендах может не работать.",
|
||||
"Avatar Hover Magnification": "Зум аватарки по наведению",
|
||||
"Enable magnification for zoomed avatar display.": "Добавляет возможность приближать увеличенную версию аватарки.",
|
||||
"Unique to this chat": "Только для текущего чата",
|
||||
"Checkpoints inherit the Note from their parent, and can be changed individually after that.": "Чекпоинты наследуют заметки от родительского чата, но впоследствие их всегда можно изменить.",
|
||||
"Include in World Info Scanning": "Учитывать при сканировании Информации о мире",
|
||||
"Before Main Prompt / Story String": "Перед основным промптом / строкой истории",
|
||||
"After Main Prompt / Story String": "После основного промпта / строки истории",
|
||||
"In-chat @ Depth": "Встав. на глуб.",
|
||||
"as": "роль:",
|
||||
"Insertion Frequency": "Частота вставки",
|
||||
"(0 = Disable, 1 = Always)": "(0 = никогда, 1 = всегда)",
|
||||
"User inputs until next insertion:": "Ваших сообщений до след. вставки:",
|
||||
"Character Author's Note (Private)": "Заметки автора персонажа (личные)",
|
||||
"Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.": "Автоматически применятся к этому персонажу в качестве заметок автора. Будут использоваться в группах, но при активном групповом чате к редактированию недоступны.",
|
||||
"Use character author's note": "Использовать заметки автора персонажа",
|
||||
"Replace Author's Note": "Вместо заметок автора",
|
||||
"Top of Author's Note": "Сверху от заметок автора",
|
||||
"Bottom of Author's Note": "Снизу от заметок автора",
|
||||
"Default Author's Note": "Стандартные заметки автора",
|
||||
"Will be automatically added as the Author's Note for all new chats.": "Будут автоматически добавляться во все новые чаты в качестве Заметок автора",
|
||||
"1 = disabled": "1 = откл.",
|
||||
"write short replies, write replies using past tense": "пиши короткие ответы, пиши в настоящем времени",
|
||||
"Positive Prompt": "Положительный промпт",
|
||||
"Character CFG": "CFG для персонажа",
|
||||
"Will be automatically added as the CFG for this character.": "Автоматически применится к персонажу как его CFG.",
|
||||
"Global CFG": "Глобальный CFG",
|
||||
"Will be used as the default CFG options for every chat unless overridden.": "Будет применяться как стандартный CFG для всех чатов, если не указаны индивидуальные настройки.",
|
||||
"CFG Prompt Cascading": "Совмещение CFG-промптов",
|
||||
"Combine positive/negative prompts from other boxes.": "Комбинировать различные положительные и негативные промпты.",
|
||||
"For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "К примеру, если отметить галочки с чатом, персонажем и глобальной настройкой, то все эти негативы соберутся в одну строку, разделённую запятыми.",
|
||||
"Always Include": "Всегда применять",
|
||||
"Chat Negatives": "Негативы от чата",
|
||||
"Character Negatives": "Негативы от персонажа",
|
||||
"Global Negatives": "Глобальные негативы",
|
||||
"Custom Separator:": "Кастомный разделитель:",
|
||||
"Insertion Depth:": "Глубина вставки:",
|
||||
"Chat CFG": "CFG для чата",
|
||||
"Chat backgrounds generated with the": "Здесь будут появляться фоны, сгенерированные расширением",
|
||||
"extension will appear here.": ".",
|
||||
"Prevent further recursion (this entry will not activate others)": "Пресечь дальнейшую рекурсию (эта запись не будет активировать другие)",
|
||||
"Alert if your world info is greater than the allocated budget.": "Оповещать, если ваш мир выходит за выделенный бюджет.",
|
||||
"Convert to Persona": "Преобразовать в персону",
|
||||
"Link to Source": "Ссылка на источник",
|
||||
"Replace / Update": "Заменить / Обновить",
|
||||
"Smoothing Curve": "Кривая сглаживания",
|
||||
"Message Actions": "Действия с сообщением",
|
||||
"SillyTavern is aimed at advanced users.": "SillyTavern рассчитана на продвинутых пользователей.",
|
||||
"If you're new to this, enable the simplified UI mode below.": "Если вы новичок, советуем включить упрощённый UI.",
|
||||
"Enable simple UI mode": "Включить упрощённый UI",
|
||||
"welcome_message_part_1": "Ознакомьтесь с",
|
||||
"welcome_message_part_2": "официальной документацией",
|
||||
"welcome_message_part_3": ".",
|
||||
"welcome_message_part_4": "Введите",
|
||||
"welcome_message_part_5": "в чате, чтобы получить справку по командам и макросам.",
|
||||
"welcome_message_part_6": "Заходите на наш",
|
||||
"Discord server": "Discord-сервер,",
|
||||
"welcome_message_part_7": "там публикуется много разной полезной информации, в том числе анонсы.",
|
||||
"Before you get started, you must select a persona name.": "Для начала вам следует выбрать имя своей персоны.",
|
||||
"welcome_message_part_8": "Его можно будет изменить в любое время через иконку",
|
||||
"welcome_message_part_9": ".",
|
||||
"UI Language:": "Язык интерфейса:",
|
||||
"Ignore EOS Token": "Игнорировать EOS-токен",
|
||||
"Ignore the EOS Token even if it generates.": "Игнорировать EOS-токен, даже если он сгенерировался.",
|
||||
"Hide Muted Member Sprites": "Скрыть спрайты заглушенных участников",
|
||||
"Group generation handling mode": "Генерировать ответы путём...",
|
||||
"Swap character cards": "Подмены карточки персонажа",
|
||||
"Join character cards (exclude muted)": "Совмещения карточек (кроме заглушенных)",
|
||||
"Join character cards (include muted)": "Совмещения карточек (включая заглушенных)",
|
||||
"Click to allow/forbid the use of external media for this group.": "Нажмите, чтобы разрешить/запретить использование внешних медиа в этой группе.",
|
||||
"Scenario Format Template": "Шаблон оформления сценария",
|
||||
"scenario_format_template_part_1": "Используйте",
|
||||
"scenario_format_template_part_2": "чтобы указать, куда именно вставляется основное содержимое.",
|
||||
"Personality Format Template": "Шаблон оформления характера",
|
||||
"Group Nudge Prompt Template": "Шаблон промпта-подсказки для групп",
|
||||
"Sent at the end of the group chat history to force reply from a specific character.": "Добавляется в конец истории сообщений в групповом чате, чтобы запросить ответ от конкретного персонажа.",
|
||||
"Set at the beginning of the chat history to indicate that a new chat is about to start.": "Добавляется в начале истории сообщений в качестве указания на то, что дальше начнётся новый чат.",
|
||||
"New Group Chat": "Новый групповой чат",
|
||||
"Set at the beginning of the chat history to indicate that a new group chat is about to start.": "Добавляется в начале истории сообщений в качестве указания на то, что дальше начнётся новый групповой чат.",
|
||||
"New Example Chat": "Новый образец чата",
|
||||
"Set at the beginning of Dialogue examples to indicate that a new example chat is about to start.": "Добавляется в начале примеров диалогов в качестве указания на то, что дальше начнётся новый чат-пример.",
|
||||
"Continue nudge": "Подсказка для продолжения",
|
||||
"Set at the end of the chat history when the continue button is pressed.": "Добавляется в конец истории чата, когда отправлен запрос на продолжение текущего сообщения.",
|
||||
"Prompts": "Промпты",
|
||||
"Your Persona": "Ваша персона",
|
||||
"Continue Postfix": "Постфикс для продолжения",
|
||||
"Space": "Пробел",
|
||||
"Newline": "Новая строка",
|
||||
"Double Newline": "Две новые строки",
|
||||
"The next chunk of the continued message will be appended using this as a separator.": "Используется в качестве разделителя между уже имеющимся сообщением и его новым отрывком, при генерации продолжения"
|
||||
}
|
||||
|
81
public/login.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<base href="/">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, viewport-fit=cover, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="darkreader-lock">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="img/apple-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="img/apple-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="img/apple-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="img/apple-icon-144x144.png" />
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/st-tailwind.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/login.css">
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
|
||||
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
|
||||
<!-- fontawesome webfonts-->
|
||||
<link href="css/fontawesome.css" rel="stylesheet">
|
||||
<link href="css/solid.css" rel="stylesheet">
|
||||
<link href="css/user.css" rel="stylesheet">
|
||||
<script src="lib/jquery-3.5.1.min.js"></script>
|
||||
<script src="scripts/login.js"></script>
|
||||
<title>SillyTavern</title>
|
||||
</head>
|
||||
|
||||
<body class="login">
|
||||
<div id="shadow_popup" style="opacity: 0;">
|
||||
<div id="dialogue_popup">
|
||||
<div id="dialogue_popup_holder">
|
||||
<div id="dialogue_popup_text">
|
||||
<div id="userSelectBlock" class="flex-container flexFlowColumn alignItemsCenter">
|
||||
<h2 id="logoBlock" class="flex-container">
|
||||
<img src="img/logo.png" alt="SillyTavern" class="logo">
|
||||
<span>Welcome to SillyTavern</span>
|
||||
</h2>
|
||||
<h3 id="normalLoginPrompt">
|
||||
Select an Account
|
||||
</h3>
|
||||
<h3 id="discreetLoginPrompt">
|
||||
Enter Login Details
|
||||
</h3>
|
||||
<div id="userListBlock" class="wide100p">
|
||||
<div id="userList" class="flex-container justifySpaceEvenly"></div>
|
||||
<div id="handleEntryBlock" style="display:none;" class="flex-container flexFlowColumn alignItemsCenter">
|
||||
<input id="userHandle" class="text_pole" type="text" placeholder="User handle" autocomplete="username">
|
||||
</div>
|
||||
<div id="passwordEntryBlock" style="display:none;"
|
||||
class="flex-container flexFlowColumn alignItemsCenter">
|
||||
<input id="userPassword" class="text_pole" type="password" placeholder="Password" autocomplete="current-password">
|
||||
<a id="recoverPassword" href="#" onclick="return false;">Forgot password?</a>
|
||||
<div class="flex-container">
|
||||
<div id="loginButton" class="menu_button">Login</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="passwordRecoveryBlock" style="display:none;"
|
||||
class="flex-container flexFlowColumn alignItemsCenter">
|
||||
<div id="recoverMessage">
|
||||
Recovery code has been posted to the server console.
|
||||
</div>
|
||||
<input id="recoveryCode" class="text_pole" type="text" placeholder="Recovery code">
|
||||
<input id="newPassword" class="text_pole" type="password" placeholder="New password" autocomplete="new-password">
|
||||
<div class="flex-container flexGap10">
|
||||
<div id="sendRecovery" class="menu_button">Send</div>
|
||||
<div id="cancelRecovery" class="menu_button">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="neutral_warning" id="errorMessage">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
1129
public/script.js
@@ -5,6 +5,7 @@ import { is_group_generating } from './group-chats.js';
|
||||
import { Message, TokenHandler } from './openai.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { debounce, waitUntilCondition, escapeHtml } from './utils.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
|
||||
function debouncePromise(func, delay) {
|
||||
let timeoutId;
|
||||
@@ -294,7 +295,7 @@ class PromptManager {
|
||||
this.handleCharacterReset = () => { };
|
||||
|
||||
/** Debounced version of render */
|
||||
this.renderDebounced = debounce(this.render.bind(this), 1000);
|
||||
this.renderDebounced = debounce(this.render.bind(this), debounce_timeout.relaxed);
|
||||
}
|
||||
|
||||
|
||||
@@ -776,7 +777,7 @@ class PromptManager {
|
||||
const promptOrder = this.getPromptOrderForCharacter(character);
|
||||
const index = promptOrder.findIndex(entry => entry.identifier === prompt.identifier);
|
||||
|
||||
if (-1 === index) promptOrder.push({ identifier: prompt.identifier, enabled: false });
|
||||
if (-1 === index) promptOrder.unshift({ identifier: prompt.identifier, enabled: false });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1286,7 +1287,7 @@ class PromptManager {
|
||||
} else if (!entry.enabled && entry.identifier === 'main') {
|
||||
// Some extensions require main prompt to be present for relative inserts.
|
||||
// So we make a GMO-free vegan replacement.
|
||||
const prompt = this.getPromptById(entry.identifier);
|
||||
const prompt = structuredClone(this.getPromptById(entry.identifier));
|
||||
prompt.content = '';
|
||||
if (prompt) promptCollection.add(this.preparePrompt(prompt));
|
||||
}
|
||||
|
@@ -32,10 +32,11 @@ import {
|
||||
SECRET_KEYS,
|
||||
secret_state,
|
||||
} from './secrets.js';
|
||||
import { debounce, delay, getStringHash, isValidUrl } from './utils.js';
|
||||
import { debounce, getStringHash, isValidUrl } from './utils.js';
|
||||
import { chat_completion_sources, oai_settings } from './openai.js';
|
||||
import { getTokenCountAsync } from './tokenizers.js';
|
||||
import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
|
||||
import Bowser from '../lib/bowser.min.js';
|
||||
|
||||
@@ -54,7 +55,7 @@ var retry_delay = 500;
|
||||
let counterNonce = Date.now();
|
||||
|
||||
const observerConfig = { childList: true, subtree: true };
|
||||
const countTokensDebounced = debounce(RA_CountCharTokens, 1000);
|
||||
const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
|
||||
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
@@ -80,19 +81,21 @@ observer.observe(document.documentElement, observerConfig);
|
||||
|
||||
|
||||
/**
|
||||
* Converts generation time from milliseconds to a human-readable format.
|
||||
* Converts a timespan from milliseconds to a human-readable format.
|
||||
*
|
||||
* The function takes total generation time as an input, then converts it to a format
|
||||
* The function takes a total timespan as an input, then converts it to a format
|
||||
* of "_ Days, _ Hours, _ Minutes, _ Seconds". If the generation time does not exceed a
|
||||
* particular measure (like days or hours), that measure will not be included in the output.
|
||||
*
|
||||
* @param {number} total_gen_time - The total generation time in milliseconds.
|
||||
* @returns {string} - A human-readable string that represents the time spent generating characters.
|
||||
* @param {number} timespan - The total timespan in milliseconds.
|
||||
* @param {object} [options] - Optional parameters
|
||||
* @param {boolean} [options.short=false] - Flag indicating whether short form should be used. ('2h' instead of '2 Hours')
|
||||
* @param {number} [options.onlyHighest] - Number of maximum blocks to be returned. (If, and daya is the highest matching unit, only returns days and hours, cutting of minutes and seconds)
|
||||
* @returns {string} - A human-readable string that represents the timespan.
|
||||
*/
|
||||
export function humanizeGenTime(total_gen_time) {
|
||||
|
||||
export function humanizeTimespan(timespan, { short = false, onlyHighest = 2 } = {}) {
|
||||
//convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds
|
||||
let time_spent = total_gen_time || 0;
|
||||
let time_spent = timespan || 0;
|
||||
time_spent = Math.floor(time_spent / 1000);
|
||||
let seconds = time_spent % 60;
|
||||
time_spent = Math.floor(time_spent / 60);
|
||||
@@ -101,12 +104,36 @@ export function humanizeGenTime(total_gen_time) {
|
||||
let hours = time_spent % 24;
|
||||
time_spent = Math.floor(time_spent / 24);
|
||||
let days = time_spent;
|
||||
time_spent = '';
|
||||
if (days > 0) { time_spent += `${days} Days, `; }
|
||||
if (hours > 0) { time_spent += `${hours} Hours, `; }
|
||||
if (minutes > 0) { time_spent += `${minutes} Minutes, `; }
|
||||
time_spent += `${seconds} Seconds`;
|
||||
return time_spent;
|
||||
|
||||
let parts = [
|
||||
{ singular: 'Day', plural: 'Days', short: 'd', value: days },
|
||||
{ singular: 'Hour', plural: 'Hours', short: 'h', value: hours },
|
||||
{ singular: 'Minute', plural: 'Minutes', short: 'm', value: minutes },
|
||||
{ singular: 'Second', plural: 'Seconds', short: 's', value: seconds },
|
||||
];
|
||||
|
||||
// Build the final string based on the highest significant units and respecting zeros
|
||||
let resultParts = [];
|
||||
let count = 0;
|
||||
for (let part of parts) {
|
||||
if (part.value > 0) {
|
||||
resultParts.push(part);
|
||||
}
|
||||
|
||||
// If we got a match, we count from there. Take a maximum of X elements
|
||||
if (resultParts.length) count++;
|
||||
if (count >= onlyHighest) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resultParts.length) {
|
||||
return short ? '<1s' : 'Instant';
|
||||
}
|
||||
|
||||
return resultParts.map(part => {
|
||||
return short ? `${part.value}${part.short}` : `${part.value} ${part.value === 1 ? part.singular : part.plural}`;
|
||||
}).join(short ? ' ' : ', ');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -12,6 +12,7 @@ import { extension_settings, getContext, saveMetadataDebounced } from './extensi
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { getCharaFilename, debounce, delay } from './utils.js';
|
||||
import { getTokenCountAsync } from './tokenizers.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
export { MODULE_NAME as NOTE_MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
|
||||
@@ -84,9 +85,9 @@ function updateSettings() {
|
||||
setFloatingPrompt();
|
||||
}
|
||||
|
||||
const setMainPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_prompt_token_counter').text(await getTokenCountAsync(value)), 1000);
|
||||
const setCharaPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_chara_token_counter').text(await getTokenCountAsync(value)), 1000);
|
||||
const setDefaultPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_default_token_counter').text(await getTokenCountAsync(value)), 1000);
|
||||
const setMainPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_prompt_token_counter').text(await getTokenCountAsync(value)), debounce_timeout.relaxed);
|
||||
const setCharaPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_chara_token_counter').text(await getTokenCountAsync(value)), debounce_timeout.relaxed);
|
||||
const setDefaultPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_default_token_counter').text(await getTokenCountAsync(value)), debounce_timeout.relaxed);
|
||||
|
||||
async function onExtensionFloatingPromptInput() {
|
||||
chat_metadata[metadata_keys.prompt] = $(this).val();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import { flashHighlight, stringFormat } from './utils.js';
|
||||
|
||||
const BG_METADATA_KEY = 'custom_background';
|
||||
const LIST_METADATA_KEY = 'chat_backgrounds';
|
||||
@@ -453,8 +453,7 @@ function highlightNewBackground(bg) {
|
||||
const newBg = $(`.bg_example[bgfile="${bg}"]`);
|
||||
const scrollOffset = newBg.offset().top - newBg.parent().offset().top;
|
||||
$('#Backgrounds').scrollTop(scrollOffset);
|
||||
newBg.addClass('flash animated');
|
||||
setTimeout(() => newBg.removeClass('flash animated'), 2000);
|
||||
flashHighlight(newBg);
|
||||
}
|
||||
|
||||
function onBackgroundFilterInput() {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { characters, getCharacters, handleDeleteCharacter, callPopup, characterGroupOverlay } from '../script.js';
|
||||
import { characterGroupOverlay } from '../script.js';
|
||||
import { BulkEditOverlay, BulkEditOverlayState } from './BulkEditOverlay.js';
|
||||
|
||||
|
||||
@@ -69,15 +69,6 @@ function onSelectAllButtonClick() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the character with the given chid.
|
||||
*
|
||||
* @param {string} this_chid - The chid of the character to delete.
|
||||
*/
|
||||
async function deleteCharacter(this_chid) {
|
||||
await handleDeleteCharacter('del_ch', this_chid, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all characters that have been selected via the bulk checkboxes.
|
||||
*/
|
||||
|
@@ -18,6 +18,8 @@ import {
|
||||
saveSettingsDebounced,
|
||||
showSwipeButtons,
|
||||
this_chid,
|
||||
saveChatConditional,
|
||||
chat_metadata,
|
||||
} from '../script.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { power_user } from './power-user.js';
|
||||
@@ -25,22 +27,93 @@ import {
|
||||
extractTextFromHTML,
|
||||
extractTextFromMarkdown,
|
||||
extractTextFromPDF,
|
||||
extractTextFromEpub,
|
||||
getBase64Async,
|
||||
getStringHash,
|
||||
humanFileSize,
|
||||
saveBase64AsFile,
|
||||
extractTextFromOffice,
|
||||
} from './utils.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
import { ScraperManager } from './scrapers.js';
|
||||
|
||||
const fileSizeLimit = 1024 * 1024 * 10; // 10 MB
|
||||
/**
|
||||
* @typedef {Object} FileAttachment
|
||||
* @property {string} url File URL
|
||||
* @property {number} size File size
|
||||
* @property {string} name File name
|
||||
* @property {number} created Timestamp
|
||||
* @property {string} [text] File text
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {function} ConverterFunction
|
||||
* @param {File} file File object
|
||||
* @returns {Promise<string>} Converted file text
|
||||
*/
|
||||
|
||||
const fileSizeLimit = 1024 * 1024 * 100; // 100 MB
|
||||
const ATTACHMENT_SOURCE = {
|
||||
GLOBAL: 'global',
|
||||
CHARACTER: 'character',
|
||||
CHAT: 'chat',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {Record<string, ConverterFunction>} File converters
|
||||
*/
|
||||
const converters = {
|
||||
'application/pdf': extractTextFromPDF,
|
||||
'text/html': extractTextFromHTML,
|
||||
'text/markdown': extractTextFromMarkdown,
|
||||
'application/epub+zip': extractTextFromEpub,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': extractTextFromOffice,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': extractTextFromOffice,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.text': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.presentation': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.spreadsheet': extractTextFromOffice,
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a matching key in the converters object.
|
||||
* @param {string} type MIME type
|
||||
* @returns {string} Matching key
|
||||
*/
|
||||
function findConverterKey(type) {
|
||||
return Object.keys(converters).find((key) => {
|
||||
// Match exact type
|
||||
if (type === key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Match wildcards
|
||||
if (key.endsWith('*')) {
|
||||
return type.startsWith(key.substring(0, key.length - 1));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the file type has a converter function.
|
||||
* @param {string} type MIME type
|
||||
* @returns {boolean} True if the file type is convertible, false otherwise.
|
||||
*/
|
||||
function isConvertible(type) {
|
||||
return Object.keys(converters).includes(type);
|
||||
return Boolean(findConverterKey(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the converter function for a file type.
|
||||
* @param {string} type MIME type
|
||||
* @returns {ConverterFunction} Converter function
|
||||
*/
|
||||
function getConverter(type) {
|
||||
const key = findConverterKey(type);
|
||||
return key && converters[key];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +199,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
|
||||
|
||||
if (isConvertible(file.type)) {
|
||||
try {
|
||||
const converter = converters[file.type];
|
||||
const converter = getConverter(file.type);
|
||||
const fileText = await converter(file);
|
||||
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||
} catch (error) {
|
||||
@@ -145,6 +218,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
|
||||
url: fileUrl,
|
||||
size: file.size,
|
||||
name: file.name,
|
||||
created: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -275,9 +349,9 @@ async function onFileAttach() {
|
||||
* @param {number} messageId Message ID
|
||||
*/
|
||||
async function deleteMessageFile(messageId) {
|
||||
const confirm = await callPopup('Are you sure you want to delete this file?', 'confirm');
|
||||
const confirm = await callGenericPopup('Are you sure you want to delete this file?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (!confirm) {
|
||||
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
console.debug('Delete file cancelled');
|
||||
return;
|
||||
}
|
||||
@@ -289,11 +363,15 @@ async function deleteMessageFile(messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = message.extra.file.url;
|
||||
|
||||
delete message.extra.file;
|
||||
$(`.mes[mesid="${messageId}"] .mes_file_container`).remove();
|
||||
saveChatDebounced();
|
||||
await saveChatConditional();
|
||||
await deleteFileFromServer(url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens file from message in a modal.
|
||||
* @param {number} messageId Message ID
|
||||
@@ -306,14 +384,7 @@ async function viewMessageFile(messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileText = messageFile.text || (await getFileAttachment(messageFile.url));
|
||||
|
||||
const modalTemplate = $('<div><pre><code></code></pre></div>');
|
||||
modalTemplate.find('code').addClass('txt').text(fileText);
|
||||
modalTemplate.addClass('file_modal');
|
||||
addCopyToCodeBlocks(modalTemplate);
|
||||
|
||||
callPopup(modalTemplate, 'text', '', { wide: true, large: true });
|
||||
await openFilePopup(messageFile);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,7 +419,7 @@ function embedMessageFile(messageId, messageBlock) {
|
||||
|
||||
await populateFileAttachment(message, 'embed_file_input');
|
||||
appendMediaToMessage(message, messageBlock);
|
||||
saveChatDebounced();
|
||||
await saveChatConditional();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +434,7 @@ export async function appendFileContent(message, messageText) {
|
||||
const fileText = message.extra.file.text || (await getFileAttachment(message.extra.file.url));
|
||||
|
||||
if (fileText) {
|
||||
const fileWrapped = `\`\`\`\n${fileText}\n\`\`\`\n\n`;
|
||||
const fileWrapped = `${fileText}\n\n`;
|
||||
message.extra.fileLength = fileWrapped.length;
|
||||
messageText = fileWrapped + messageText;
|
||||
}
|
||||
@@ -395,7 +466,7 @@ export function decodeStyleTags(text) {
|
||||
|
||||
return text.replaceAll(styleDecodeRegex, (_, style) => {
|
||||
try {
|
||||
let styleCleaned = unescape(style).replaceAll(/<br\/>/g, '');
|
||||
let styleCleaned = unescape(style).replaceAll(/<br\/>/g, '');
|
||||
const ast = css.parse(styleCleaned);
|
||||
const rules = ast?.stylesheet?.rules;
|
||||
if (rules) {
|
||||
@@ -436,8 +507,8 @@ async function openExternalMediaOverridesDialog() {
|
||||
}
|
||||
|
||||
const template = $('#forbid_media_override_template > .forbid_media_override').clone();
|
||||
template.find('.forbid_media_global_state_forbidden').toggle(power_user.forbid_external_images);
|
||||
template.find('.forbid_media_global_state_allowed').toggle(!power_user.forbid_external_images);
|
||||
template.find('.forbid_media_global_state_forbidden').toggle(power_user.forbid_external_media);
|
||||
template.find('.forbid_media_global_state_allowed').toggle(!power_user.forbid_external_media);
|
||||
|
||||
if (power_user.external_media_allowed_overrides.includes(entityId)) {
|
||||
template.find('#forbid_media_override_allowed').prop('checked', true);
|
||||
@@ -463,7 +534,7 @@ export function getCurrentEntityId() {
|
||||
export function isExternalMediaAllowed() {
|
||||
const entityId = getCurrentEntityId();
|
||||
if (!entityId) {
|
||||
return !power_user.forbid_external_images;
|
||||
return !power_user.forbid_external_media;
|
||||
}
|
||||
|
||||
if (power_user.external_media_allowed_overrides.includes(entityId)) {
|
||||
@@ -474,7 +545,736 @@ export function isExternalMediaAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !power_user.forbid_external_images;
|
||||
return !power_user.forbid_external_media;
|
||||
}
|
||||
|
||||
function enlargeMessageImage() {
|
||||
const mesBlock = $(this).closest('.mes');
|
||||
const mesId = mesBlock.attr('mesid');
|
||||
const message = chat[mesId];
|
||||
const imgSrc = message?.extra?.image;
|
||||
const title = message?.extra?.title;
|
||||
|
||||
if (!imgSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.classList.add('img_enlarged');
|
||||
img.src = imgSrc;
|
||||
const imgContainer = $('<div><pre><code></code></pre></div>');
|
||||
imgContainer.prepend(img);
|
||||
imgContainer.addClass('img_enlarged_container');
|
||||
imgContainer.find('code').addClass('txt').text(title);
|
||||
const titleEmpty = !title || title.trim().length === 0;
|
||||
imgContainer.find('pre').toggle(!titleEmpty);
|
||||
addCopyToCodeBlocks(imgContainer);
|
||||
callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
}
|
||||
|
||||
async function deleteMessageImage() {
|
||||
const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (value !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mesBlock = $(this).closest('.mes');
|
||||
const mesId = mesBlock.attr('mesid');
|
||||
const message = chat[mesId];
|
||||
delete message.extra.image;
|
||||
delete message.extra.inline_image;
|
||||
mesBlock.find('.mes_img_container').removeClass('img_extra');
|
||||
mesBlock.find('.mes_img').attr('src', '');
|
||||
await saveChatConditional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes file from the server.
|
||||
* @param {string} url Path to the file on the server
|
||||
* @param {boolean} [silent=false] If true, do not show error messages
|
||||
* @returns {Promise<boolean>} True if file was deleted, false otherwise.
|
||||
*/
|
||||
async function deleteFileFromServer(url, silent = false) {
|
||||
try {
|
||||
const result = await fetch('/api/files/delete', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ path: url }),
|
||||
});
|
||||
|
||||
if (!result.ok && !silent) {
|
||||
const error = await result.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
await eventSource.emit(event_types.FILE_ATTACHMENT_DELETED, url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
toastr.error(String(error), 'Could not delete file');
|
||||
console.error('Could not delete file', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens file attachment in a modal.
|
||||
* @param {FileAttachment} attachment File attachment
|
||||
*/
|
||||
async function openFilePopup(attachment) {
|
||||
const fileText = attachment.text || (await getFileAttachment(attachment.url));
|
||||
|
||||
const modalTemplate = $('<div><pre><code></code></pre></div>');
|
||||
modalTemplate.find('code').addClass('txt').text(fileText);
|
||||
modalTemplate.addClass('file_modal').addClass('textarea_compact').addClass('fontsize90p');
|
||||
addCopyToCodeBlocks(modalTemplate);
|
||||
|
||||
callGenericPopup(modalTemplate, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a file attachment in a notepad-like modal.
|
||||
* @param {FileAttachment} attachment Attachment to edit
|
||||
* @param {string} source Attachment source
|
||||
* @param {function} callback Callback function
|
||||
*/
|
||||
async function editAttachment(attachment, source, callback) {
|
||||
const originalFileText = attachment.text || (await getFileAttachment(attachment.url));
|
||||
const template = $(await renderExtensionTemplateAsync('attachments', 'notepad'));
|
||||
|
||||
let editedFileText = originalFileText;
|
||||
template.find('[name="notepadFileContent"]').val(editedFileText).on('input', function () {
|
||||
editedFileText = String($(this).val());
|
||||
});
|
||||
|
||||
let editedFileName = attachment.name;
|
||||
template.find('[name="notepadFileName"]').val(editedFileName).on('input', function () {
|
||||
editedFileName = String($(this).val());
|
||||
});
|
||||
|
||||
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { wide: true, large: true, okButton: 'Save', cancelButton: 'Cancel' });
|
||||
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editedFileText === originalFileText && editedFileName === attachment.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nullCallback = () => { };
|
||||
await deleteAttachment(attachment, source, nullCallback, false);
|
||||
const file = new File([editedFileText], editedFileName, { type: 'text/plain' });
|
||||
await uploadFileAttachmentToServer(file, source);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an attachment to the user's device.
|
||||
* @param {FileAttachment} attachment Attachment to download
|
||||
*/
|
||||
async function downloadAttachment(attachment) {
|
||||
const fileText = attachment.text || (await getFileAttachment(attachment.url));
|
||||
const blob = new Blob([fileText], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = attachment.name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an attachment from the disabled list.
|
||||
* @param {FileAttachment} attachment Attachment to enable
|
||||
* @param {function} callback Success callback
|
||||
*/
|
||||
function enableAttachment(attachment, callback) {
|
||||
ensureAttachmentsExist();
|
||||
extension_settings.disabled_attachments = extension_settings.disabled_attachments.filter(url => url !== attachment.url);
|
||||
saveSettingsDebounced();
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an attachment to the disabled list.
|
||||
* @param {FileAttachment} attachment Attachment to disable
|
||||
* @param {function} callback Success callback
|
||||
*/
|
||||
function disableAttachment(attachment, callback) {
|
||||
ensureAttachmentsExist();
|
||||
extension_settings.disabled_attachments.push(attachment.url);
|
||||
saveSettingsDebounced();
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a file attachment to a different source.
|
||||
* @param {FileAttachment} attachment Attachment to moves
|
||||
* @param {string} source Source of the attachment
|
||||
* @param {function} callback Success callback
|
||||
* @returns {Promise<void>} A promise that resolves when the attachment is moved.
|
||||
*/
|
||||
async function moveAttachment(attachment, source, callback) {
|
||||
let selectedTarget = source;
|
||||
const targets = getAvailableTargets();
|
||||
const template = $(await renderExtensionTemplateAsync('attachments', 'move-attachment', { name: attachment.name, targets }));
|
||||
template.find('.moveAttachmentTarget').val(source).on('input', function () {
|
||||
selectedTarget = String($(this).val());
|
||||
});
|
||||
|
||||
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Move', cancelButton: 'Cancel' });
|
||||
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
console.debug('Move attachment cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedTarget === source) {
|
||||
console.debug('Move attachment cancelled: same source and target');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await getFileAttachment(attachment.url);
|
||||
const file = new File([content], attachment.name, { type: 'text/plain' });
|
||||
await deleteAttachment(attachment, source, () => { }, false);
|
||||
await uploadFileAttachmentToServer(file, selectedTarget);
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an attachment from the server and the chat.
|
||||
* @param {FileAttachment} attachment Attachment to delete
|
||||
* @param {string} source Source of the attachment
|
||||
* @param {function} callback Callback function
|
||||
* @param {boolean} [confirm=true] If true, show a confirmation dialog
|
||||
* @returns {Promise<void>} A promise that resolves when the attachment is deleted.
|
||||
*/
|
||||
async function deleteAttachment(attachment, source, callback, confirm = true) {
|
||||
if (confirm) {
|
||||
const result = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ensureAttachmentsExist();
|
||||
|
||||
switch (source) {
|
||||
case 'global':
|
||||
extension_settings.attachments = extension_settings.attachments.filter((a) => a.url !== attachment.url);
|
||||
saveSettingsDebounced();
|
||||
break;
|
||||
case 'chat':
|
||||
chat_metadata.attachments = chat_metadata.attachments.filter((a) => a.url !== attachment.url);
|
||||
saveMetadataDebounced();
|
||||
break;
|
||||
case 'character':
|
||||
extension_settings.character_attachments[characters[this_chid]?.avatar] = extension_settings.character_attachments[characters[this_chid]?.avatar].filter((a) => a.url !== attachment.url);
|
||||
break;
|
||||
}
|
||||
|
||||
if (Array.isArray(extension_settings.disabled_attachments) && extension_settings.disabled_attachments.includes(attachment.url)) {
|
||||
extension_settings.disabled_attachments = extension_settings.disabled_attachments.filter(url => url !== attachment.url);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
const silent = confirm === false;
|
||||
await deleteFileFromServer(attachment.url, silent);
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the attachment is disabled.
|
||||
* @param {FileAttachment} attachment Attachment to check
|
||||
* @returns {boolean} True if attachment is disabled, false otherwise.
|
||||
*/
|
||||
function isAttachmentDisabled(attachment) {
|
||||
return extension_settings.disabled_attachments.some(url => url === attachment?.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the attachment manager.
|
||||
*/
|
||||
async function openAttachmentManager() {
|
||||
/**
|
||||
* Renders a list of attachments.
|
||||
* @param {FileAttachment[]} attachments List of attachments
|
||||
* @param {string} source Source of the attachments
|
||||
*/
|
||||
async function renderList(attachments, source) {
|
||||
/**
|
||||
* Sorts attachments by sortField and sortOrder.
|
||||
* @param {FileAttachment} a First attachment
|
||||
* @param {FileAttachment} b Second attachment
|
||||
* @returns {number} Sort order
|
||||
*/
|
||||
function sortFn(a, b) {
|
||||
const sortValueA = a[sortField];
|
||||
const sortValueB = b[sortField];
|
||||
if (typeof sortValueA === 'string' && typeof sortValueB === 'string') {
|
||||
return sortValueA.localeCompare(sortValueB) * (sortOrder === 'asc' ? 1 : -1);
|
||||
}
|
||||
return (sortValueA - sortValueB) * (sortOrder === 'asc' ? 1 : -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters attachments by name.
|
||||
* @param {FileAttachment} a Attachment
|
||||
* @returns {boolean} True if attachment matches the filter, false otherwise.
|
||||
*/
|
||||
function filterFn(a) {
|
||||
if (!filterString) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return a.name.toLowerCase().includes(filterString.toLowerCase());
|
||||
}
|
||||
const sources = {
|
||||
[ATTACHMENT_SOURCE.GLOBAL]: '.globalAttachmentsList',
|
||||
[ATTACHMENT_SOURCE.CHARACTER]: '.characterAttachmentsList',
|
||||
[ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsList',
|
||||
};
|
||||
|
||||
template.find(sources[source]).empty();
|
||||
|
||||
// Sort attachments by sortField and sortOrder, and apply filter
|
||||
const sortedAttachmentList = attachments.slice().filter(filterFn).sort(sortFn);
|
||||
|
||||
for (const attachment of sortedAttachmentList) {
|
||||
const isDisabled = isAttachmentDisabled(attachment);
|
||||
const attachmentTemplate = template.find('.attachmentListItemTemplate .attachmentListItem').clone();
|
||||
attachmentTemplate.toggleClass('disabled', isDisabled);
|
||||
attachmentTemplate.find('.attachmentFileIcon').attr('title', attachment.url);
|
||||
attachmentTemplate.find('.attachmentListItemName').text(attachment.name);
|
||||
attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size));
|
||||
attachmentTemplate.find('.attachmentListItemCreated').text(new Date(attachment.created).toLocaleString());
|
||||
attachmentTemplate.find('.viewAttachmentButton').on('click', () => openFilePopup(attachment));
|
||||
attachmentTemplate.find('.editAttachmentButton').on('click', () => editAttachment(attachment, source, renderAttachments));
|
||||
attachmentTemplate.find('.deleteAttachmentButton').on('click', () => deleteAttachment(attachment, source, renderAttachments));
|
||||
attachmentTemplate.find('.downloadAttachmentButton').on('click', () => downloadAttachment(attachment));
|
||||
attachmentTemplate.find('.moveAttachmentButton').on('click', () => moveAttachment(attachment, source, renderAttachments));
|
||||
attachmentTemplate.find('.enableAttachmentButton').toggle(isDisabled).on('click', () => enableAttachment(attachment, renderAttachments));
|
||||
attachmentTemplate.find('.disableAttachmentButton').toggle(!isDisabled).on('click', () => disableAttachment(attachment, renderAttachments));
|
||||
template.find(sources[source]).append(attachmentTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders buttons for the attachment manager.
|
||||
*/
|
||||
async function renderButtons() {
|
||||
const sources = {
|
||||
[ATTACHMENT_SOURCE.GLOBAL]: '.globalAttachmentsTitle',
|
||||
[ATTACHMENT_SOURCE.CHARACTER]: '.characterAttachmentsTitle',
|
||||
[ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsTitle',
|
||||
};
|
||||
|
||||
const modal = template.find('.actionButtonsModal').hide();
|
||||
const scrapers = ScraperManager.getDataBankScrapers();
|
||||
|
||||
for (const scraper of scrapers) {
|
||||
const isAvailable = await ScraperManager.isScraperAvailable(scraper.id);
|
||||
if (!isAvailable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const buttonTemplate = template.find('.actionButtonTemplate .actionButton').clone();
|
||||
if (scraper.iconAvailable) {
|
||||
buttonTemplate.find('.actionButtonIcon').addClass(scraper.iconClass);
|
||||
buttonTemplate.find('.actionButtonImg').remove();
|
||||
} else {
|
||||
buttonTemplate.find('.actionButtonImg').attr('src', scraper.iconClass);
|
||||
buttonTemplate.find('.actionButtonIcon').remove();
|
||||
}
|
||||
buttonTemplate.find('.actionButtonText').text(scraper.name);
|
||||
buttonTemplate.attr('title', scraper.description);
|
||||
buttonTemplate.on('click', () => {
|
||||
const target = modal.attr('data-attachment-manager-target');
|
||||
runScraper(scraper.id, target, renderAttachments);
|
||||
});
|
||||
modal.append(buttonTemplate);
|
||||
}
|
||||
|
||||
const modalButtonData = Object.entries(sources).map(entry => {
|
||||
const [source, selector] = entry;
|
||||
const button = template.find(selector).find('.openActionModalButton').get(0);
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bodyListener = (e) => {
|
||||
if (modal.is(':visible') && (!$(e.target).closest('.openActionModalButton').length)) {
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
// Replay a click if the modal was already open by another button
|
||||
if ($(e.target).closest('.openActionModalButton').length && !modal.is(':visible')) {
|
||||
modal.show();
|
||||
}
|
||||
};
|
||||
document.body.addEventListener('click', bodyListener);
|
||||
|
||||
const popper = Popper.createPopper(button, modal.get(0), { placement: 'bottom-end' });
|
||||
button.addEventListener('click', () => {
|
||||
modal.attr('data-attachment-manager-target', source);
|
||||
modal.toggle();
|
||||
popper.update();
|
||||
});
|
||||
|
||||
return [popper, bodyListener];
|
||||
}).filter(Boolean);
|
||||
|
||||
return () => {
|
||||
modalButtonData.forEach(p => {
|
||||
const [popper, bodyListener] = p;
|
||||
popper.destroy();
|
||||
document.body.removeEventListener('click', bodyListener);
|
||||
});
|
||||
modal.remove();
|
||||
};
|
||||
}
|
||||
|
||||
async function renderAttachments() {
|
||||
/** @type {FileAttachment[]} */
|
||||
const globalAttachments = extension_settings.attachments ?? [];
|
||||
/** @type {FileAttachment[]} */
|
||||
const chatAttachments = chat_metadata.attachments ?? [];
|
||||
/** @type {FileAttachment[]} */
|
||||
const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
|
||||
await renderList(globalAttachments, ATTACHMENT_SOURCE.GLOBAL);
|
||||
await renderList(chatAttachments, ATTACHMENT_SOURCE.CHAT);
|
||||
await renderList(characterAttachments, ATTACHMENT_SOURCE.CHARACTER);
|
||||
|
||||
const isNotCharacter = this_chid === undefined || selected_group;
|
||||
const isNotInChat = getCurrentChatId() === undefined;
|
||||
template.find('.characterAttachmentsBlock').toggle(!isNotCharacter);
|
||||
template.find('.chatAttachmentsBlock').toggle(!isNotInChat);
|
||||
|
||||
const characterName = characters[this_chid]?.name || 'Anonymous';
|
||||
template.find('.characterAttachmentsName').text(characterName);
|
||||
|
||||
const chatName = getCurrentChatId() || 'Unnamed chat';
|
||||
template.find('.chatAttachmentsName').text(chatName);
|
||||
}
|
||||
|
||||
function addDragAndDrop() {
|
||||
$(document.body).on('dragover', '.dialogue_popup', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.target).closest('.dialogue_popup').addClass('dragover');
|
||||
});
|
||||
|
||||
$(document.body).on('dragleave', '.dialogue_popup', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.target).closest('.dialogue_popup').removeClass('dragover');
|
||||
});
|
||||
|
||||
$(document.body).on('drop', '.dialogue_popup', async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.target).closest('.dialogue_popup').removeClass('dragover');
|
||||
|
||||
const files = Array.from(event.originalEvent.dataTransfer.files);
|
||||
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
|
||||
const targets = getAvailableTargets();
|
||||
|
||||
const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets }));
|
||||
targetSelectTemplate.find('.droppedFilesTarget').on('input', function () {
|
||||
selectedTarget = String($(this).val());
|
||||
});
|
||||
const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' });
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
console.log('File upload cancelled');
|
||||
return;
|
||||
}
|
||||
for (const file of files) {
|
||||
await uploadFileAttachmentToServer(file, selectedTarget);
|
||||
}
|
||||
renderAttachments();
|
||||
});
|
||||
}
|
||||
|
||||
function removeDragAndDrop() {
|
||||
$(document.body).off('dragover', '.shadow_popup');
|
||||
$(document.body).off('dragleave', '.shadow_popup');
|
||||
$(document.body).off('drop', '.shadow_popup');
|
||||
}
|
||||
|
||||
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
|
||||
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
|
||||
let filterString = '';
|
||||
|
||||
const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
|
||||
|
||||
template.find('.attachmentSearch').on('input', function () {
|
||||
filterString = String($(this).val());
|
||||
renderAttachments();
|
||||
});
|
||||
template.find('.attachmentSort').on('change', function () {
|
||||
if (!(this instanceof HTMLSelectElement) || this.selectedOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sortField = this.selectedOptions[0].dataset.sortField;
|
||||
sortOrder = this.selectedOptions[0].dataset.sortOrder;
|
||||
localStorage.setItem('DataBank_sortField', sortField);
|
||||
localStorage.setItem('DataBank_sortOrder', sortOrder);
|
||||
renderAttachments();
|
||||
});
|
||||
|
||||
const cleanupFn = await renderButtons();
|
||||
await verifyAttachments();
|
||||
await renderAttachments();
|
||||
addDragAndDrop();
|
||||
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
|
||||
|
||||
cleanupFn();
|
||||
removeDragAndDrop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of available targets for attachments.
|
||||
* @returns {string[]} List of available targets
|
||||
*/
|
||||
function getAvailableTargets() {
|
||||
const targets = Object.values(ATTACHMENT_SOURCE);
|
||||
|
||||
const isNotCharacter = this_chid === undefined || selected_group;
|
||||
const isNotInChat = getCurrentChatId() === undefined;
|
||||
|
||||
if (isNotCharacter) {
|
||||
targets.splice(targets.indexOf(ATTACHMENT_SOURCE.CHARACTER), 1);
|
||||
}
|
||||
|
||||
if (isNotInChat) {
|
||||
targets.splice(targets.indexOf(ATTACHMENT_SOURCE.CHAT), 1);
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a known scraper on a source and saves the result as an attachment.
|
||||
* @param {string} scraperId Id of the scraper
|
||||
* @param {string} target Target for the attachment
|
||||
* @param {function} callback Callback function
|
||||
* @returns {Promise<void>} A promise that resolves when the source is scraped.
|
||||
*/
|
||||
async function runScraper(scraperId, target, callback) {
|
||||
try {
|
||||
console.log(`Running scraper ${scraperId} for ${target}`);
|
||||
const files = await ScraperManager.runDataBankScraper(scraperId);
|
||||
|
||||
if (!Array.isArray(files)) {
|
||||
console.warn('Scraping returned nothing');
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
console.warn('Scraping returned no files');
|
||||
toastr.info('No files were scraped.', 'Data Bank');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
await uploadFileAttachmentToServer(file, target);
|
||||
}
|
||||
|
||||
toastr.success(`Scraped ${files.length} files from ${scraperId} to ${target}.`, 'Data Bank');
|
||||
callback();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Scraping failed', error);
|
||||
toastr.error('Check browser console for details.', 'Scraping failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file attachment to the server.
|
||||
* @param {File} file File to upload
|
||||
* @param {string} target Target for the attachment
|
||||
* @returns
|
||||
*/
|
||||
export async function uploadFileAttachmentToServer(file, target) {
|
||||
const isValid = await validateFile(file);
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
let base64Data = await getBase64Async(file);
|
||||
const slug = getStringHash(file.name);
|
||||
const uniqueFileName = `${Date.now()}_${slug}.txt`;
|
||||
|
||||
if (isConvertible(file.type)) {
|
||||
try {
|
||||
const converter = getConverter(file.type);
|
||||
const fileText = await converter(file);
|
||||
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||
} catch (error) {
|
||||
toastr.error(String(error), 'Could not convert file');
|
||||
console.error('Could not convert file', error);
|
||||
}
|
||||
} else {
|
||||
const fileText = await file.text();
|
||||
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||
}
|
||||
|
||||
const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data);
|
||||
const convertedSize = Math.round(base64Data.length * 0.75);
|
||||
|
||||
if (!fileUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = {
|
||||
url: fileUrl,
|
||||
size: convertedSize,
|
||||
name: file.name,
|
||||
created: Date.now(),
|
||||
};
|
||||
|
||||
ensureAttachmentsExist();
|
||||
|
||||
switch (target) {
|
||||
case ATTACHMENT_SOURCE.GLOBAL:
|
||||
extension_settings.attachments.push(attachment);
|
||||
saveSettingsDebounced();
|
||||
break;
|
||||
case ATTACHMENT_SOURCE.CHAT:
|
||||
chat_metadata.attachments.push(attachment);
|
||||
saveMetadataDebounced();
|
||||
break;
|
||||
case ATTACHMENT_SOURCE.CHARACTER:
|
||||
extension_settings.character_attachments[characters[this_chid]?.avatar].push(attachment);
|
||||
saveSettingsDebounced();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAttachmentsExist() {
|
||||
if (!Array.isArray(extension_settings.disabled_attachments)) {
|
||||
extension_settings.disabled_attachments = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(extension_settings.attachments)) {
|
||||
extension_settings.attachments = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(chat_metadata.attachments)) {
|
||||
chat_metadata.attachments = [];
|
||||
}
|
||||
|
||||
if (this_chid !== undefined && characters[this_chid]) {
|
||||
if (!extension_settings.character_attachments) {
|
||||
extension_settings.character_attachments = {};
|
||||
}
|
||||
|
||||
if (!Array.isArray(extension_settings.character_attachments[characters[this_chid].avatar])) {
|
||||
extension_settings.character_attachments[characters[this_chid].avatar] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all currently available attachments. Ignores disabled attachments.
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachments() {
|
||||
ensureAttachmentsExist();
|
||||
const globalAttachments = extension_settings.attachments ?? [];
|
||||
const chatAttachments = chat_metadata.attachments ?? [];
|
||||
const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
|
||||
return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => !isAttachmentDisabled(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all attachments for a specific source. Includes disabled attachments.
|
||||
* @param {string} source Attachment source
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachmentsForSource(source) {
|
||||
ensureAttachmentsExist();
|
||||
|
||||
switch (source) {
|
||||
case ATTACHMENT_SOURCE.GLOBAL:
|
||||
return extension_settings.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHAT:
|
||||
return chat_metadata.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHARACTER:
|
||||
return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies all attachments in the Data Bank.
|
||||
* @returns {Promise<void>} A promise that resolves when attachments are verified.
|
||||
*/
|
||||
async function verifyAttachments() {
|
||||
for (const source of Object.values(ATTACHMENT_SOURCE)) {
|
||||
await verifyAttachmentsForSource(source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies all attachments for a specific source.
|
||||
* @param {string} source Attachment source
|
||||
* @returns {Promise<void>} A promise that resolves when attachments are verified.
|
||||
*/
|
||||
async function verifyAttachmentsForSource(source) {
|
||||
try {
|
||||
const attachments = getDataBankAttachmentsForSource(source);
|
||||
const urls = attachments.map(a => a.url);
|
||||
const response = await fetch('/api/files/verify', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ urls }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const verifiedUrls = await response.json();
|
||||
for (const attachment of attachments) {
|
||||
if (verifiedUrls[attachment.url] === false) {
|
||||
console.log('Deleting orphaned attachment', attachment);
|
||||
await deleteAttachment(attachment, source, () => { }, false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Attachment verification failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a file converter function.
|
||||
* @param {string} mimeType MIME type
|
||||
* @param {ConverterFunction} converter Function to convert file
|
||||
* @returns {void}
|
||||
*/
|
||||
export function registerFileConverter(mimeType, converter) {
|
||||
if (typeof mimeType !== 'string' || typeof converter !== 'function') {
|
||||
console.error('Invalid converter registration');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(converters).includes(mimeType)) {
|
||||
console.error('Converter already registered');
|
||||
return;
|
||||
}
|
||||
|
||||
converters[mimeType] = converter;
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
@@ -507,6 +1307,11 @@ jQuery(function () {
|
||||
$('#file_form_input').trigger('click');
|
||||
});
|
||||
|
||||
// Do not change. #manageAttachments is added by extension.
|
||||
$(document).on('click', '#manageAttachments', function () {
|
||||
openAttachmentManager();
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_embed', function () {
|
||||
const messageBlock = $(this).closest('.mes');
|
||||
const messageId = Number(messageBlock.attr('mesid'));
|
||||
@@ -529,6 +1334,7 @@ jQuery(function () {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = String(bro.val());
|
||||
textarea.classList.add('height100p', 'wide100p');
|
||||
bro.hasClass('monospace') && textarea.classList.add('monospace');
|
||||
textarea.addEventListener('input', function () {
|
||||
bro.val(textarea.value).trigger('input');
|
||||
});
|
||||
@@ -598,6 +1404,9 @@ jQuery(function () {
|
||||
reloadCurrentChat();
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
|
||||
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
||||
|
||||
$('#file_form_input').on('change', onFileAttach);
|
||||
$('#file_form').on('reset', function () {
|
||||
$('#file_form').addClass('displayNone');
|
||||
|
14
public/scripts/constants.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Common debounce timeout values to use with `debounce` calls.
|
||||
* @enum {number}
|
||||
*/
|
||||
export const debounce_timeout = {
|
||||
/** [100 ms] For ultra-fast responses, typically for keypresses or executions that might happen multiple times in a loop or recursion. */
|
||||
quick: 100,
|
||||
/** [300 ms] Default time for general use, good balance between responsiveness and performance. */
|
||||
standard: 300,
|
||||
/** [1.000 ms] For situations where the function triggers more intensive tasks. */
|
||||
relaxed: 1000,
|
||||
/** [5 sec] For delayed tasks, like auto-saving or completing batch operations that need a significant pause. */
|
||||
extended: 5000,
|
||||
};
|
@@ -145,6 +145,18 @@ const extension_settings = {
|
||||
variables: {
|
||||
global: {},
|
||||
},
|
||||
/**
|
||||
* @type {import('./chats.js').FileAttachment[]}
|
||||
*/
|
||||
attachments: [],
|
||||
/**
|
||||
* @type {Record<string, import('./chats.js').FileAttachment[]>}
|
||||
*/
|
||||
character_attachments: {},
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
disabled_attachments: [],
|
||||
};
|
||||
|
||||
let modules = [];
|
||||
|
9
public/scripts/extensions/attachments/buttons.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div id="attachFile" class="list-group-item flex-container flexGap5" title="Attach a file or image to a current chat.">
|
||||
<div class="fa-fw fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
|
||||
<span data-i18n="Attach a File">Attach a File</span>
|
||||
</div>
|
||||
|
||||
<div id="manageAttachments" class="list-group-item flex-container flexGap5" title="View global, character, or data files.">
|
||||
<div class="fa-fw fa-solid fa-book-open-reader extensionsMenuExtensionButton"></div>
|
||||
<span data-i18n="Open Data Bank">Open Data Bank</span>
|
||||
</div>
|
51
public/scripts/extensions/attachments/fandom-scrape.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="fandomScrapeInput" data-i18n="Enter a URL or the ID of a Fandom wiki page to scrape:">
|
||||
Enter a URL or the ID of a Fandom wiki page to scrape:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<code>https://harrypotter.fandom.com/</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>harrypotter</code>
|
||||
</small>
|
||||
<input type="text" id="fandomScrapeInput" name="fandomScrapeInput" class="text_pole" placeholder="">
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="fandomScrapeFilter">
|
||||
Optional regex to pick the content by its title:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n="Example:">Example:</span>
|
||||
<code>/(Azkaban|Weasley)/gi</code>
|
||||
</small>
|
||||
<input type="text" id="fandomScrapeFilter" name="fandomScrapeFilter" class="text_pole" placeholder="">
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label>
|
||||
Output format:
|
||||
</label>
|
||||
<label class="checkbox_label justifyLeft" for="fandomScrapeOutputSingle">
|
||||
<input id="fandomScrapeOutputSingle" type="radio" name="fandomScrapeOutput" value="single" checked>
|
||||
<div class="flex-container flexFlowColumn flexNoGap">
|
||||
<span data-i18n="Single file">
|
||||
Single file
|
||||
</span>
|
||||
<small data-i18n="All articles will be concatenated into a single file.">
|
||||
All articles will be concatenated into a single file.
|
||||
</small>
|
||||
</div>
|
||||
</label>
|
||||
<label class="checkbox_label justifyLeft" for="fandomScrapeOutputMulti">
|
||||
<input id="fandomScrapeOutputMulti" type="radio" name="fandomScrapeOutput" value="multi">
|
||||
<div class="flex-container flexFlowColumn flexNoGap">
|
||||
<span data-i18n="File per article">
|
||||
File per article
|
||||
</span>
|
||||
<small data-i18n="Each article will be saved as a separate file.">
|
||||
Not recommended. Each article will be saved as a separate file.
|
||||
</small>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|