mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Compare commits
476 Commits
1.12.0-pre
...
1.12.0-6
Author | SHA1 | Date | |
---|---|---|---|
|
db2c9a9926 | ||
|
1fc88e97f4 | ||
|
3d3914645d | ||
|
68ade7b384 | ||
|
071f232611 | ||
|
dff5ca7e92 | ||
|
ab75680ed3 | ||
|
8c11d7e8e8 | ||
|
8b776491e8 | ||
|
884f26924c | ||
|
5f79579a4d | ||
|
82f56da16b | ||
|
bc2035d362 | ||
|
41f25edb15 | ||
|
cf28d6653c | ||
|
53496d70f1 | ||
|
a17588fb1b | ||
|
dd06fddd17 | ||
|
e9c5618b10 | ||
|
b32330df0f | ||
|
3bd2edf4d2 | ||
|
697949c784 | ||
|
ee913be46b | ||
|
4f5813a6ce | ||
|
4a25821aba | ||
|
c049ff82cd | ||
|
64ee2d77b8 | ||
|
ce4106eb3d | ||
|
974d27ce26 | ||
|
dfe482b37b | ||
|
1f18694aa6 | ||
|
89de668241 | ||
|
963e525e07 | ||
|
94ee7167e0 | ||
|
48c075fb42 | ||
|
581e5f1f04 | ||
|
9637b3fbe1 | ||
|
c7232ae23c | ||
|
01912f5b3e | ||
|
db5e2d95c2 | ||
|
d9b55df883 | ||
|
4e9a113a35 | ||
|
70a35e9b49 | ||
|
bc4a8fbe1d | ||
|
d77a70b25a | ||
|
e49317a73c | ||
|
87cc28ae28 | ||
|
b93131ec7a | ||
|
4219468e20 | ||
|
4227968dfa | ||
|
09790bb994 | ||
|
bd1bfee941 | ||
|
0b3c49da90 | ||
|
c3d6e10795 | ||
|
1eae9bd18b | ||
|
99e24f5588 | ||
|
38e2bf955c | ||
|
0653dad5c5 | ||
|
51af830db8 | ||
|
2bde9d2b15 | ||
|
d9f9da99e6 | ||
|
64b0123acf | ||
|
35d853b851 | ||
|
1966baad84 | ||
|
a398566b33 | ||
|
9f2c473040 | ||
|
a73db4984a | ||
|
3498eb92bb | ||
|
455db18d71 | ||
|
2aeffe4095 | ||
|
c4fe9749d5 | ||
|
a0512585b1 | ||
|
c52bdb9a4a | ||
|
bbd9c89357 | ||
|
fb2190ace1 | ||
|
deb09bf5bf | ||
|
d951beb626 | ||
|
748dd5f2e6 | ||
|
75de4c8fcb | ||
|
432be09583 | ||
|
c822b9e2da | ||
|
c661fea07d | ||
|
782f85e05d | ||
|
96a9f7108c | ||
|
9475147435 | ||
|
909ec4191d | ||
|
59d00cca74 | ||
|
71a3e2c91b | ||
|
719202ba12 | ||
|
8ae4332110 | ||
|
964f53273c | ||
|
9e10022014 | ||
|
2eaabe13e3 | ||
|
3389b5dd16 | ||
|
3832afaeba | ||
|
e026ddf6be | ||
|
116fa673c6 | ||
|
517da9f972 | ||
|
74256dc411 | ||
|
1b23a62c13 | ||
|
97de520f9a | ||
|
a6333f3285 | ||
|
012f70336f | ||
|
7fbed26c26 | ||
|
a94af2678b | ||
|
eb57289b2a | ||
|
068b542c50 | ||
|
912fd36e29 | ||
|
b7a91770dc | ||
|
f0af503b4a | ||
|
3d023a5cf6 | ||
|
1e2d1aa118 | ||
|
ccfd3606dc | ||
|
fe95e09c8b | ||
|
bac90edfad | ||
|
b7043a428f | ||
|
5b47b83fe2 | ||
|
7289ed72f8 | ||
|
c4936ed535 | ||
|
8a5f05fb74 | ||
|
36f7bc4aae | ||
|
d5869e3f90 | ||
|
3b83d081db | ||
|
6861135925 | ||
|
b6f47c9927 | ||
|
796cc3c60c | ||
|
f1a57d76a2 | ||
|
87b61f7cff | ||
|
1999f607d6 | ||
|
bb2f553c46 | ||
|
6c2dc6756b | ||
|
f7c12264e8 | ||
|
9ef3dea884 | ||
|
cd90e252bf | ||
|
ac2475fb26 | ||
|
8f1a959da1 | ||
|
8c55e1b05b | ||
|
a0bbee8b79 | ||
|
caf85ad040 | ||
|
3999bee482 | ||
|
f0016b5368 | ||
|
1dec93de8a | ||
|
caf236d60a | ||
|
c8ed8e06f1 | ||
|
aa845b4727 | ||
|
1ebe5547d4 | ||
|
e2e7d5870a | ||
|
ea45d372f3 | ||
|
3113109f0a | ||
|
6ec51ff086 | ||
|
9eae4d9739 | ||
|
84aa746241 | ||
|
00ce078630 | ||
|
5426431adf | ||
|
726ec0fbfc | ||
|
094fc1f24b | ||
|
49cb8daf7d | ||
|
16660e995e | ||
|
8469f43285 | ||
|
6865f84eb1 | ||
|
036603c9e9 | ||
|
ab0f57aba3 | ||
|
13c755c197 | ||
|
5250d1fcaf | ||
|
492f857012 | ||
|
f1a0462ca3 | ||
|
6254ac6fbf | ||
|
5207b3a7f0 | ||
|
9e968de4e4 | ||
|
e3edb96568 | ||
|
dded42374c | ||
|
c561fb4fab | ||
|
67610b9f7f | ||
|
14aa70eea8 | ||
|
28da838bd1 | ||
|
fd18e0cc78 | ||
|
2a30a74886 | ||
|
6130ebb6d9 | ||
|
64500bfb37 | ||
|
cc077732c4 | ||
|
cd47f3b238 | ||
|
297519c401 | ||
|
09d410ec48 | ||
|
1369025092 | ||
|
5147233391 | ||
|
38585cb6af | ||
|
f53775d3f5 | ||
|
14ba7fc646 | ||
|
5b7bfbaa98 | ||
|
1d75b98393 | ||
|
c7d75b7789 | ||
|
4ccedb939c | ||
|
4bb463dd56 | ||
|
0ba600bb2b | ||
|
9ed6ee2161 | ||
|
c4ade296ae | ||
|
0ed81e3b1a | ||
|
d1933be86a | ||
|
15ff8de45c | ||
|
45a080016e | ||
|
78cf6e9086 | ||
|
7d65a6e264 | ||
|
91945ec77e | ||
|
7b472f13af | ||
|
a93777e3b7 | ||
|
2f310c72fa | ||
|
6a4ee68113 | ||
|
e73b5713fd | ||
|
1f81086a21 | ||
|
3e48f4b805 | ||
|
432be2ee57 | ||
|
f421139402 | ||
|
081223cc8f | ||
|
27ccc6b090 | ||
|
62faddac8d | ||
|
6804e4c679 | ||
|
10ee002091 | ||
|
1430eb26ea | ||
|
e17a18ad5d | ||
|
eeaa52bf5d | ||
|
07a6cb1252 | ||
|
7a2f6fb63f | ||
|
bb3ac095c4 | ||
|
e18d554489 | ||
|
a2625ecec6 | ||
|
0252b21901 | ||
|
f6343436b4 | ||
|
9db2f1cb91 | ||
|
2fd59f5aef | ||
|
18d96bc346 | ||
|
535da63e52 | ||
|
e14c9506b6 | ||
|
88aae5978f | ||
|
b9d72bfdf4 | ||
|
f4bb4fe51e | ||
|
cf77b9e7ee | ||
|
ff1399d1ba | ||
|
eb273a1873 | ||
|
0587931cae | ||
|
7408673e41 | ||
|
3c0664dfb6 | ||
|
d2477bba0c | ||
|
f37e444791 | ||
|
1ae08f49c5 | ||
|
10fda0b220 | ||
|
61d5dde497 | ||
|
edc33584da | ||
|
883da48762 | ||
|
49d0d9f557 | ||
|
852bc15a94 | ||
|
c43ddd9d62 | ||
|
c851961234 | ||
|
01aacb9280 | ||
|
dfa8c6c3d4 | ||
|
fda0e886e4 | ||
|
5a45e64999 | ||
|
70a2f71e33 | ||
|
4a70e68c22 | ||
|
542018cecb | ||
|
5e7c214c89 | ||
|
12eabd167d | ||
|
c73bfbd7b0 | ||
|
7063fce2af | ||
|
694cf6f762 | ||
|
83c77c1f18 | ||
|
d54ccece5c | ||
|
96506947cb | ||
|
afc3071576 | ||
|
10727d9a02 | ||
|
55d31a976f | ||
|
181b5aff97 | ||
|
31f1b34911 | ||
|
ef5499c8dc | ||
|
292ecf580e | ||
|
de1ca9af74 | ||
|
75832c1ad6 | ||
|
39a54d158d | ||
|
2bf9869e5f | ||
|
b13434c505 | ||
|
5197809d6b | ||
|
204a934553 | ||
|
de1bb90c23 | ||
|
478be72659 | ||
|
d972ed5a2b | ||
|
932d3dc10c | ||
|
f610d5930c | ||
|
05db2552b3 | ||
|
7bfd666321 | ||
|
a02f5ead7e | ||
|
d9fae7a02c | ||
|
29e2e8f607 | ||
|
7bc87b6e28 | ||
|
ed65ddf981 | ||
|
3c2017c7b8 | ||
|
6b2b849a26 | ||
|
b69c5bcd17 | ||
|
7b87f44518 | ||
|
5ab1e74c5f | ||
|
2bd239fe81 | ||
|
1a219e32fe | ||
|
203146f7e2 | ||
|
73bea1f454 | ||
|
408151c9cb | ||
|
f0adbc3c28 | ||
|
9acf057aae | ||
|
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 | ||
|
305afb3713 | ||
|
1acbef1890 | ||
|
f90f370fed | ||
|
d34a0ee20e | ||
|
01e3964232 | ||
|
153638c2cd | ||
|
4bb719359c | ||
|
847eb60806 | ||
|
e799bd3920 | ||
|
2b1aee9e71 | ||
|
d65f068310 | ||
|
b1c199e650 | ||
|
51014e7a8d | ||
|
530bf81940 | ||
|
2bba186c9e | ||
|
61241df0d4 | ||
|
b6b9b542d7 | ||
|
71f41d5233 | ||
|
a421af9ea9 | ||
|
75372ad0cc | ||
|
d1f292f462 | ||
|
890cf81627 | ||
|
d97f0a4c4d | ||
|
4370db6bdc | ||
|
770f3e5da3 | ||
|
0f0895f345 | ||
|
6d1933c8f3 | ||
|
776260c85a | ||
|
5a5463bd5d | ||
|
2f45f50d37 | ||
|
41ad7c5d26 | ||
|
df93d43c36 | ||
|
bc9c70556e | ||
|
f75daba6c0 | ||
|
47b6562605 | ||
|
1c9b89fdcc | ||
|
035dbe4901 |
@@ -44,6 +44,7 @@ module.exports = {
|
||||
toastr: 'readonly',
|
||||
Readability: 'readonly',
|
||||
isProbablyReaderable: 'readonly',
|
||||
ePub: 'readonly',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
2
.github/close-label.yml
vendored
Normal file
2
.github/close-label.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
🐛 Bug: ✅ Fixed
|
||||
🦄 Feature Request: ✅ Implemented
|
62
.github/issue-auto-comments.yml
vendored
Normal file
62
.github/issue-auto-comments.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
comment:
|
||||
footer: |
|
||||
---
|
||||
> I am a bot, and this is an automated message 🤖
|
||||
labels:
|
||||
- name: ✖️ Invalid
|
||||
labeled:
|
||||
issue:
|
||||
action: close
|
||||
body: >
|
||||
Hello @{{ issue.user.login }} your ticket has been marked as invalid.
|
||||
Please ensure you follow the issue template, provide all requested info,
|
||||
and be sure to check the docs + previous issues prior to raising tickets.
|
||||
pr:
|
||||
body: Thank you @{{ pull_request.user.login }} for suggesting this. Please follow the pull request templates.
|
||||
action: close
|
||||
|
||||
- name: 👩💻 Good First Issue
|
||||
labeled:
|
||||
issue:
|
||||
body: >
|
||||
This issue has been marked as a good first issue for first-time contributors to implement!
|
||||
This is a great way to support the project, while also improving your skills, you'll also be credited as a contributor once your PR is merged.
|
||||
If you're new to SillyTavern [here are a collection of resources](https://docs.sillytavern.app/)
|
||||
If you need any support at all, feel free to reach out via [Discord](https://discord.gg/sillytavern).
|
||||
|
||||
- name: ❌ wontfix
|
||||
labeled:
|
||||
issue:
|
||||
action: close
|
||||
body: >
|
||||
This ticked has been marked as 'wontfix', which usually means it is out-of-scope, or not feasible at this time.
|
||||
You can still fork the project and make the changes yourself.
|
||||
|
||||
- name: ✅ Fixed
|
||||
labeled:
|
||||
issue:
|
||||
body: >
|
||||
Hello @{{ issue.user.login }}! It looks like all or part of this issue has now been implemented.
|
||||
|
||||
|
||||
- name: ‼️ High Priority
|
||||
labeled:
|
||||
issue:
|
||||
body: >
|
||||
This ticket has been marked as high priority, and has been bumped to the top of the priority list.
|
||||
You should expect an implementation to be pushed out soon. Thank you for your patience.
|
||||
|
||||
- name: 💀 Spam
|
||||
labeled:
|
||||
issue:
|
||||
action: close
|
||||
locking: lock
|
||||
lock_reason: spam
|
||||
body: >
|
||||
This issue has been identified as spam, and is now locked.
|
||||
Users who repeatedly raise spam issues may be blocked or reported.
|
||||
|
||||
- name: ⛔ Don't Merge
|
||||
labeled:
|
||||
pr:
|
||||
body: This PR has been temporarily blocked from merging.
|
2
.github/readme.md
vendored
2
.github/readme.md
vendored
@@ -326,7 +326,7 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.**
|
||||
|
||||
* TAI Base by Humi: Unknown license
|
||||
* TAI Base by Humi: MIT
|
||||
* Cohee's modifications and derived code: AGPL v3
|
||||
* RossAscends' additions: AGPL v3
|
||||
* Portions of CncAnon's TavernAITurbo mod: Unknown license
|
||||
|
28
.github/workflows/add-comment-from-tag.yml
vendored
Normal file
28
.github/workflows/add-comment-from-tag.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Based on a label applied to an issue, the bot will add a comment with some additional info
|
||||
|
||||
name: 🎯 Auto-Reply to Labeled Tickets
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Label Commenter
|
||||
uses: peaceiris/actions-label-commenter@v1
|
||||
with:
|
||||
config_file: .github/issue-auto-comments.yml
|
||||
github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
17
.github/workflows/check-merge-conflicts.yml
vendored
Normal file
17
.github/workflows/check-merge-conflicts.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Detect and label pull requests that have merge conflicts
|
||||
name: 🏗️ Check Merge Conflicts
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- staging
|
||||
jobs:
|
||||
check-conflicts:
|
||||
if: github.repository == 'SillyTavern/SillyTavern'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mschilde/auto-label-merge-conflicts@master
|
||||
with:
|
||||
CONFLICT_LABEL_NAME: "🚫 Merge Conflicts"
|
||||
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
MAX_RETRIES: 5
|
||||
WAIT_MS: 5000
|
82
.github/workflows/close-stale-issues.yml
vendored
Normal file
82
.github/workflows/close-stale-issues.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
# Closes any issues that no longer have user interaction
|
||||
name: 🎯 Close Stale Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Runs every day at midnight UTC
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Comment on, then close issues that haven't been updated for ages
|
||||
- name: Close Stale Issues
|
||||
uses: actions/stale@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 360
|
||||
days-before-close: 5
|
||||
operations-per-run: 30
|
||||
remove-stale-when-updated: true
|
||||
enable-statistics: true
|
||||
stale-issue-message: >
|
||||
This issue has gone 3 months without an update. To keep the ticket open, please indicate that it is still relevant in a comment below.
|
||||
Otherwise it will be closed in 5 working days.
|
||||
stale-pr-message: >
|
||||
This PR is stale because it has been open 6 weeks with no activity. Either remove the stale label or comment below with a short update,
|
||||
otherwise this PR will be closed in 5 days.
|
||||
close-issue-message: >
|
||||
This issue was automatically closed because it has been stalled for over 1 year with no activity.
|
||||
close-pr-message: >
|
||||
This pull request was automatically closed because it has been stalled for over 1 year with no activity.
|
||||
stale-issue-label: '⚰️ Stale'
|
||||
close-issue-label: '🕸️ Inactive'
|
||||
stale-pr-label: '⚰️ Stale'
|
||||
close-pr-label: '🕸️ Inactive'
|
||||
exempt-issue-labels: '📌 Keep Open'
|
||||
exempt-pr-labels: '📌 Keep Open'
|
||||
labels-to-add-when-unstale: '📌 Keep Open'
|
||||
|
||||
# Comment on, then close issues that required a response from the user, but didn't get one
|
||||
- name: Close Issues without Response
|
||||
uses: actions/stale@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 5
|
||||
days-before-close: 3
|
||||
operations-per-run: 30
|
||||
remove-stale-when-updated: true
|
||||
stale-issue-message: >
|
||||
Hi! Looks like additional info is required for this issue to be addressed.
|
||||
Don't forget to provide this within the next few days to keep your ticket open.
|
||||
close-issue-message: 'Issue closed due to no response from user.'
|
||||
only-labels: '🚏 Awaiting User Response'
|
||||
labels-to-remove-when-unstale: '🚏 Awaiting User Response, 🛑 No Response'
|
||||
stale-issue-label: '🛑 No Response'
|
||||
close-issue-label: '🕸️ Inactive'
|
||||
exempt-issue-labels: '📌 Keep Open'
|
||||
exempt-pr-labels: '📌 Keep Open'
|
||||
|
||||
# Comment on issues that we should have replied to
|
||||
- name: Notify Repo Owner to Respond
|
||||
uses: actions/stale@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 7
|
||||
days-before-close: 365
|
||||
operations-per-run: 30
|
||||
remove-stale-when-updated: true
|
||||
stale-issue-message: Hey SillyTavern, - Don't forget to respond!
|
||||
stale-pr-message: Hey SillyTavern, - Don't forget to respond!
|
||||
only-labels: '👤 Awaiting Maintainer Response'
|
||||
labels-to-remove-when-unstale: '👤 Awaiting Maintainer Response'
|
||||
close-issue-message: 'Closed due to no response from repo author for over a year'
|
||||
close-pr-message: 'Closed due to no response from repo author for over a year'
|
||||
stale-issue-label: '👤 Awaiting Maintainer Response'
|
||||
stale-pr-label: '👤 Awaiting Maintainer Response'
|
||||
close-issue-label: '🕸️ Inactive'
|
||||
close-pr-label: '🕸️ Inactive'
|
||||
exempt-issue-labels: '📌 Keep Open'
|
||||
exempt-pr-labels: '📌 Keep Open'
|
13
.github/workflows/docker-publish.yml
vendored
13
.github/workflows/docker-publish.yml
vendored
@@ -21,6 +21,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'SillyTavern/SillyTavern'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -29,7 +30,7 @@ jobs:
|
||||
run: |
|
||||
echo "IMAGE_NAME=${REPO,,}" >> ${GITHUB_ENV}
|
||||
|
||||
# Using the following workaround because currently GitHub Actions
|
||||
# Using the following workaround because currently GitHub Actions
|
||||
# does not support logical AND/OR operations on triggers
|
||||
# It's currently not possible to have `branches` under the `schedule` trigger
|
||||
- name: Checkout the release branch (on release)
|
||||
@@ -64,7 +65,10 @@ jobs:
|
||||
id: metadata
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: ${{ env.BRANCH_NAME }}
|
||||
# Release version tag if the workflow is triggered by a release
|
||||
# Branch name tag if the workflow is triggered by a push
|
||||
tags: |
|
||||
${{ github.event_name == 'release' && github.ref_name || env.BRANCH_NAME }}
|
||||
|
||||
# Login into package repository as the person who created the release
|
||||
- name: Log in to the Container registry
|
||||
@@ -91,5 +95,6 @@ jobs:
|
||||
- name: Docker tag latest and push
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
run: |
|
||||
docker tag $IMAGE_NAME:${{ github.ref_name }} $IMAGE_NAME:latest
|
||||
docker push $IMAGE_NAME:latest
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
39
.github/workflows/get-pr-size.yml
vendored
Normal file
39
.github/workflows/get-pr-size.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# Adds a comment to new PRs, showing the compressed size and size difference of new code
|
||||
# And also labels the PR based on the number of lines changes
|
||||
|
||||
name: 🌈 Check PR Size
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
# Find and comment with compressed size
|
||||
- name: Get Compressed Size
|
||||
uses: preactjs/compressed-size-action@v2
|
||||
with:
|
||||
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
pattern: './dist/**/*.{js,css,html}'
|
||||
strip-hash: '\\b\\w{8}\\.'
|
||||
exclude: '**/node_modules/**'
|
||||
minimum-change-threshold: 100
|
||||
# Check number of lines of code added
|
||||
- name: Label based on Lines of Code
|
||||
uses: codelytv/pr-size-labeler@v1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
xs_max_size: '10'
|
||||
s_max_size: '100'
|
||||
m_max_size: '500'
|
||||
l_max_size: '1000'
|
||||
s_label: '🟩 PR - Small'
|
||||
m_label: '🟨 PR - Medium'
|
||||
l_label: '🟧 PR - Large'
|
||||
xl_label: '🟥 PR - XL'
|
||||
fail_if_xl: 'false'
|
||||
message_if_xl: >
|
||||
It looks like this PR is very large (over 1000 lines).
|
||||
Try to avoid addressing multiple issues in a single PR, and
|
||||
in the future consider breaking large tasks down into smaller steps.
|
||||
This it to make reviewing, testing, reverting and general quality management easier.
|
17
.github/workflows/manage-pending-labels-closed.yml
vendored
Normal file
17
.github/workflows/manage-pending-labels-closed.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# When a new comment is added to an issue, if it had the Stale or Awaiting User Response labels, then those labels will be removed
|
||||
|
||||
name: 🎯 Remove Pending Labels on Close
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
jobs:
|
||||
remove-labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remove Labels when Closed
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: remove-labels
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: '🚏 Awaiting User Response,⚰️ Stale,👤 Awaiting Maintainer Response'
|
42
.github/workflows/manage-pending-labels.yml
vendored
Normal file
42
.github/workflows/manage-pending-labels.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# When a new comment is added to an issue, if it had the Stale or Awaiting User Response labels, then those labels will be removed
|
||||
|
||||
name: 🎯 Add/ Remove Awaiting Response Labels
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
remove-stale:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.comment.author_association != 'COLLABORATOR' && github.event.comment.author_association != 'OWNER' }}
|
||||
steps:
|
||||
- name: Remove Stale labels when Updated
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: remove-labels
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: '🚏 Awaiting User Response,⚰️ Stale'
|
||||
|
||||
add-awaiting-author:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{!github.event.issue.pull_request && github.event.comment.author_association != 'COLLABORATOR' && github.event.comment.author_association != 'OWNER' && github.event.issue.state == 'open' }}
|
||||
steps:
|
||||
- name: Add Awaiting Author labels when Updated
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: add-labels
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: '👤 Awaiting Maintainer Response'
|
||||
|
||||
remove-awaiting-author:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.comment.author_association == 'OWNER' }}
|
||||
steps:
|
||||
- name: Remove Awaiting Author labels when Updated
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: remove-labels
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: '👤 Awaiting Maintainer Response'
|
43
.github/workflows/update-docs.yml
vendored
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
1
.gitignore
vendored
@@ -47,3 +47,4 @@ access.log
|
||||
public/css/user.css
|
||||
/plugins/
|
||||
/data
|
||||
/default/scaffold
|
||||
|
@@ -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.
|
||||
|
@@ -9,6 +9,8 @@ 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
|
||||
@@ -46,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:
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 338 KiB |
Binary file not shown.
Before Width: | Height: | Size: 598 KiB |
@@ -107,14 +107,6 @@
|
||||
"filename": "default_Seraphina.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "default_CodingSensei.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "default_FluxTheCat.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "Seraphina",
|
||||
"type": "sprites"
|
||||
@@ -539,6 +531,10 @@
|
||||
"filename": "presets/context/Llama 3 Instruct.json",
|
||||
"type": "context"
|
||||
},
|
||||
{
|
||||
"filename": "presets/context/Phi.json",
|
||||
"type": "context"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/Adventure.json",
|
||||
"type": "instruct"
|
||||
@@ -631,6 +627,10 @@
|
||||
"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"
|
||||
@@ -642,5 +642,21 @@
|
||||
{
|
||||
"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
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
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
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
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
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
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"
|
||||
}
|
@@ -231,6 +231,7 @@
|
||||
"api_url_scale": "",
|
||||
"show_external_models": false,
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"human_sysprompt_message": "Let's get started. Please generate your response based on the information and instructions provided above.",
|
||||
"use_ai21_tokenizer": false,
|
||||
"use_google_tokenizer": false,
|
||||
|
@@ -33,8 +33,8 @@
|
||||
"negative_prompt": "",
|
||||
"grammar_string": "",
|
||||
"banned_tokens": "",
|
||||
"ignore_eos_token_aphrodite": false,
|
||||
"spaces_between_special_tokens_aphrodite": true,
|
||||
"ignore_eos_token": false,
|
||||
"spaces_between_special_tokens": true,
|
||||
"type": "ooba",
|
||||
"legacy_api": false,
|
||||
"sampler_order": [
|
||||
|
@@ -33,8 +33,8 @@
|
||||
"negative_prompt": "",
|
||||
"grammar_string": "",
|
||||
"banned_tokens": "",
|
||||
"ignore_eos_token_aphrodite": false,
|
||||
"spaces_between_special_tokens_aphrodite": true,
|
||||
"ignore_eos_token": false,
|
||||
"spaces_between_special_tokens": true,
|
||||
"type": "ooba",
|
||||
"legacy_api": false,
|
||||
"sampler_order": [
|
||||
|
@@ -33,8 +33,8 @@
|
||||
"negative_prompt": "",
|
||||
"grammar_string": "",
|
||||
"banned_tokens": "",
|
||||
"ignore_eos_token_aphrodite": false,
|
||||
"spaces_between_special_tokens_aphrodite": true,
|
||||
"ignore_eos_token": false,
|
||||
"spaces_between_special_tokens": true,
|
||||
"type": "ooba",
|
||||
"legacy_api": false,
|
||||
"sampler_order": [
|
||||
|
@@ -387,14 +387,8 @@
|
||||
}
|
||||
],
|
||||
"tag_map": {
|
||||
"default_FluxTheCat.png": [
|
||||
"1345561466591"
|
||||
],
|
||||
"default_Seraphina.png": [
|
||||
"1345561466591"
|
||||
],
|
||||
"default_CodingSensei.png": [
|
||||
"1345561466591"
|
||||
]
|
||||
},
|
||||
"nai_settings": {
|
||||
@@ -630,6 +624,7 @@
|
||||
"show_external_models": false,
|
||||
"proxy_password": "",
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"use_ai21_tokenizer": false
|
||||
}
|
||||
}
|
||||
|
26
default/scaffold/README.md
Normal file
26
default/scaffold/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Content Scaffolding
|
||||
|
||||
Content files in this folder will be copied for all users (old and new) on the server startup.
|
||||
|
||||
1. You **must** create an `index.json` file in `/default/scaffold` for it to work. The syntax is the same as for default content.
|
||||
2. All file paths should be relative to `/default/scaffold`, the use of subdirectories is allowed.
|
||||
3. Scaffolded files are copied first, so they override any of the default files (presets/settings/etc.) that have the same file name.
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"filename": "themes/Midnight.json",
|
||||
"type": "theme"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/city.png",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "characters/Charlie.png",
|
||||
"type": "character"
|
||||
}
|
||||
]
|
||||
```
|
15
package-lock.json
generated
15
package-lock.json
generated
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "sillytavern",
|
||||
"version": "1.12.0-preview",
|
||||
"version": "1.12.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sillytavern",
|
||||
"version": "1.12.0-preview",
|
||||
"version": "1.12.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@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",
|
||||
@@ -46,6 +45,7 @@
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sillytavern-transformers": "^2.14.6",
|
||||
"simple-git": "^3.19.1",
|
||||
"tiktoken": "^1.0.15",
|
||||
"vectra": "^0.2.2",
|
||||
"wavefile": "^11.0.0",
|
||||
"write-file-atomic": "^5.0.1",
|
||||
@@ -82,10 +82,6 @@
|
||||
"version": "0.1.3",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@dqbd/tiktoken": {
|
||||
"version": "1.0.13",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"dev": true,
|
||||
@@ -4403,6 +4399,11 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiktoken": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.15.tgz",
|
||||
"integrity": "sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw=="
|
||||
},
|
||||
"node_modules/timm": {
|
||||
"version": "1.7.1",
|
||||
"license": "MIT"
|
||||
|
10
package.json
10
package.json
@@ -2,7 +2,6 @@
|
||||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@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",
|
||||
@@ -36,6 +35,7 @@
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sillytavern-transformers": "^2.14.6",
|
||||
"simple-git": "^3.19.1",
|
||||
"tiktoken": "^1.0.15",
|
||||
"vectra": "^0.2.2",
|
||||
"wavefile": "^11.0.0",
|
||||
"write-file-atomic": "^5.0.1",
|
||||
@@ -68,13 +68,15 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/SillyTavern/SillyTavern.git"
|
||||
},
|
||||
"version": "1.12.0-preview",
|
||||
"version": "1.12.0",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start-multi": "node server.js --disableCsrf",
|
||||
"start:no-csrf": "node server.js --disableCsrf",
|
||||
"postinstall": "node post-install.js",
|
||||
"lint": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js",
|
||||
"lint-fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix"
|
||||
"lint:fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix",
|
||||
"plugins:update": "node plugins update",
|
||||
"plugins:install": "node plugins install"
|
||||
},
|
||||
"bin": {
|
||||
"sillytavern": "./server.js"
|
||||
|
75
plugins.js
Normal file
75
plugins.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// Plugin manager script.
|
||||
// Usage: node plugins.js update
|
||||
// More operations coming soon.
|
||||
const { default: git } = require('simple-git');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { color } = require('./src/util');
|
||||
|
||||
const pluginsPath = './plugins';
|
||||
|
||||
const command = process.argv[2];
|
||||
|
||||
if (command === 'update') {
|
||||
console.log(color.magenta('Updating all plugins'));
|
||||
updatePlugins();
|
||||
}
|
||||
|
||||
if (command === 'install') {
|
||||
const pluginName = process.argv[3];
|
||||
console.log('Installing a new plugin', color.green(pluginName));
|
||||
installPlugin(pluginName);
|
||||
}
|
||||
|
||||
async function updatePlugins() {
|
||||
const directories = fs.readdirSync(pluginsPath)
|
||||
.filter(file => !file.startsWith('.'))
|
||||
.filter(file => fs.statSync(path.join(pluginsPath, file)).isDirectory());
|
||||
|
||||
console.log(`Found ${color.cyan(directories.length)} directories in ./plugins`);
|
||||
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
console.log(`Updating plugin ${color.green(directory)}...`);
|
||||
const pluginPath = path.join(pluginsPath, directory);
|
||||
const pluginRepo = git(pluginPath);
|
||||
await pluginRepo.fetch();
|
||||
const commitHash = await pluginRepo.revparse(['HEAD']);
|
||||
const trackingBranch = await pluginRepo.revparse(['--abbrev-ref', '@{u}']);
|
||||
const log = await pluginRepo.log({
|
||||
from: commitHash,
|
||||
to: trackingBranch,
|
||||
});
|
||||
|
||||
if (log.total === 0) {
|
||||
console.log(`Plugin ${color.blue(directory)} is already up to date`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await pluginRepo.pull();
|
||||
const latestCommit = await pluginRepo.revparse(['HEAD']);
|
||||
console.log(`Plugin ${color.green(directory)} updated to commit ${color.cyan(latestCommit)}`);
|
||||
} catch (error) {
|
||||
console.error(color.red(`Failed to update plugin ${directory}: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(color.magenta('All plugins updated!'));
|
||||
|
||||
}
|
||||
|
||||
async function installPlugin(pluginName) {
|
||||
try {
|
||||
const pluginPath = path.join(pluginsPath, path.basename(pluginName, '.git'));
|
||||
|
||||
if (fs.existsSync(pluginPath)) {
|
||||
return console.log(color.yellow(`Directory already exists at ${pluginPath}`));
|
||||
}
|
||||
|
||||
await git().clone(pluginName, pluginPath, { '--depth': 1 });
|
||||
console.log(`Plugin ${color.green(pluginName)} installed to ${color.cyan(pluginPath)}`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(color.red(`Failed to install plugin ${pluginName}`), error);
|
||||
}
|
||||
}
|
6
public/css/brands.min.css
vendored
Normal file
6
public/css/brands.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
8488
public/css/fontawesome.css
vendored
File diff suppressed because it is too large
Load Diff
9
public/css/fontawesome.min.css
vendored
Normal file
9
public/css/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
|
@@ -171,3 +171,78 @@
|
||||
.select2-results__option.select2-results__message::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-selection__choice__display {
|
||||
/* Fix weird alignment on the left side */
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
/* Styling for choice remove icon */
|
||||
span.select2.select2-container .select2-selection__choice__remove {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
}
|
||||
|
||||
span.select2.select2-container .select2-selection__choice__remove:hover {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--white30a);
|
||||
}
|
||||
|
||||
/* Custom class to support styling to show clickable choice options */
|
||||
.select2_choice_clickable+span.select2-container .select2-selection__choice__display {
|
||||
cursor: pointer;
|
||||
}
|
||||
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
}
|
||||
|
||||
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display:hover {
|
||||
background-color: var(--white30a);
|
||||
}
|
||||
|
||||
/* Custom class to support same line multi inputs of select2 controls */
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
|
||||
/* Allow search placeholder to take up all space if needed */
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
/* Fix weird styling choice or huge margin around selected options */
|
||||
margin-block-start: 2px;
|
||||
margin-block-end: 2px;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search__field {
|
||||
/* Min height to reserve spacing */
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
/* Min width to be clickable */
|
||||
min-width: 4em;
|
||||
align-content: center;
|
||||
/* Fix search textarea alignment issue with UL elements */
|
||||
margin-top: 0px;
|
||||
height: unset;
|
||||
/* Prevent height from jumping around when input is focused */
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
/* Min height to reserve spacing */
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
}
|
||||
|
||||
/* Make search bar invisible unless the select2 is active, to save space */
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container.select2-container--focus .select2-selection--multiple .select2-search--inline {
|
||||
height: unset;
|
||||
}
|
||||
|
@@ -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
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}
|
@@ -360,6 +360,14 @@
|
||||
flex: 2 !important;
|
||||
}
|
||||
|
||||
.flex3 {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.flex4 {
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.flexFlowColumn {
|
||||
flex-flow: column;
|
||||
}
|
||||
@@ -563,4 +571,4 @@ textarea:disabled {
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
@@ -103,7 +103,8 @@
|
||||
}
|
||||
|
||||
#bulkTagsList,
|
||||
#tagList .tag {
|
||||
#tagList .tag,
|
||||
#groupTagList .tag {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -193,7 +194,8 @@
|
||||
filter: brightness(75%) saturate(0.6);
|
||||
}
|
||||
|
||||
.tag_as_folder:hover {
|
||||
.tag_as_folder:hover,
|
||||
.tag_as_folder.flash {
|
||||
filter: brightness(150%) saturate(0.6) !important;
|
||||
}
|
||||
|
||||
|
@@ -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*/
|
||||
|
||||
@@ -433,14 +440,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;
|
||||
}
|
||||
|
@@ -76,6 +76,12 @@
|
||||
.world_entry_form_control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.world_entry_form_control .keyprimarytextpole,
|
||||
.world_entry_form_control .keysecondarytextpole {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.world_entry_thin_controls {
|
||||
@@ -101,7 +107,7 @@
|
||||
height: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
min-height: calc(var(--mainFontSize) + 14px);
|
||||
}
|
||||
|
||||
.delete_entry_button {
|
||||
@@ -157,7 +163,12 @@
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
#world_info_search,
|
||||
#world_info_search {
|
||||
width: 10em;
|
||||
min-width: 10em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#world_info_sort_order {
|
||||
width: 7em;
|
||||
}
|
||||
@@ -191,3 +202,58 @@
|
||||
.WIEntryHeaderTitleMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span.select2-container .select2-selection__choice__display:has(> .regex_item),
|
||||
span.select2-container .select2-results__option:has(> .result_block .regex_item) {
|
||||
background-color: #D27D2D30;
|
||||
}
|
||||
|
||||
.regex_item .regex_icon {
|
||||
background-color: var(--black30a);
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 7px;
|
||||
font-weight: bold;
|
||||
font-size: calc(var(--mainFontSize) * 0.75);
|
||||
padding: 0px 3px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.select2-results__option .regex_item .regex_icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.select2-results__option .item_count {
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
select.keyselect+span.select2-container .select2-selection--multiple {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.switch_input_type_icon {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
width: fit-content;
|
||||
margin-right: 5px;
|
||||
margin-top: calc(5px + var(--mainFontSize));
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 1px;
|
||||
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 1em;
|
||||
|
||||
opacity: 0.5;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.switch_input_type_icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
1360
public/global.d.ts
vendored
Normal file
1360
public/global.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
48
public/img/groq.svg
Normal file
48
public/img/groq.svg
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="107.644"
|
||||
height="156.436"
|
||||
viewBox="0 0 107.644 156.436"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg9"
|
||||
sodipodi:docname="groqcloud_dark_v2.svg"
|
||||
inkscape:version="1.3 (0e150ed, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview9"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.667"
|
||||
inkscape:cx="499.25037"
|
||||
inkscape:cy="56.971514"
|
||||
inkscape:window-width="1312"
|
||||
inkscape:window-height="449"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg9" />
|
||||
<defs
|
||||
id="defs9">
|
||||
<clipPath
|
||||
id="clip0_872_2594">
|
||||
<rect
|
||||
width="1000"
|
||||
height="200.345"
|
||||
id="rect9"
|
||||
x="0"
|
||||
y="0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
d="M 54.0487,0.00281139 C 24.4736,-0.29748861 0.303066,23.497811 0.00281057,53.072911 -0.297445,82.648011 23.4978,106.89401 53.0729,107.11901 c 0.3003,0 0.6756,0 0.9758,0 H 71.6888 V 87.077011 H 54.0487 c -18.4656,0.225 -33.6285,-14.563 -33.8537,-33.1033 -0.2252,-18.4657 14.5624,-33.6286 33.1031,-33.8538 0.2252,0 0.5255,0 0.7506,0 18.4657,0 33.5536,15.0128 33.5536,33.4784 v 49.316699 c 0,18.316 -14.9377,33.254 -33.2533,33.479 -8.7825,0 -17.1145,-3.603 -23.2698,-9.834 l -14.187,14.187 c 9.8333,9.909 23.1947,15.539 37.1565,15.689 h 0.7507 c 29.1998,-0.451 52.6946,-24.096 52.8446,-53.296 V 52.247211 C 106.894,23.197511 83.1735,0.00281139 54.1238,0.00281139 Z"
|
||||
id="path7" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
40
public/img/infermaticai.svg
Normal file
40
public/img/infermaticai.svg
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.0"
|
||||
width="70pt"
|
||||
height="70pt"
|
||||
viewBox="0 0 70 70"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="svg15"
|
||||
sodipodi:docname="infermatic.svg"
|
||||
inkscape:version="1.3 (0e150ed, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs15" />
|
||||
<sodipodi:namedview
|
||||
id="namedview15"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="pt"
|
||||
inkscape:zoom="0.75112613"
|
||||
inkscape:cx="306.2069"
|
||||
inkscape:cy="50.590705"
|
||||
inkscape:window-width="1312"
|
||||
inkscape:window-height="449"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg15" />
|
||||
<path
|
||||
id="path15"
|
||||
d="m 1030,375 v -75 h 75 74 l 6,33 c 4,18 5,51 3,72 l -3,40 -77,3 -78,3 z m 547,619 c -4,-4 -7,-41 -7,-81 v -74 l 78,3 77,3 v 75 75 l -70,3 c -39,1 -74,0 -78,-4 z m -547,-74 v -79 l 133,-3 132,-3 3,-267 2,-268 h 215 215 v 75 75 h -135 -135 l -2,273 -3,272 -212,3 -213,2 z"
|
||||
transform="matrix(0.1,0,0,-0.1,-103,99.999998)" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
1206
public/index.html
1206
public/index.html
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,46 @@ EventEmitter.prototype.on = function (event, listener) {
|
||||
this.events[event].push(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the listener the last to be called when the event is emitted
|
||||
* @param {string} event Event name
|
||||
* @param {function} listener Event listener
|
||||
*/
|
||||
EventEmitter.prototype.makeLast = function (event, listener) {
|
||||
if (typeof this.events[event] !== 'object') {
|
||||
this.events[event] = [];
|
||||
}
|
||||
|
||||
const events = this.events[event];
|
||||
const idx = events.indexOf(listener);
|
||||
|
||||
if (idx > -1) {
|
||||
events.splice(idx, 1);
|
||||
}
|
||||
|
||||
events.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the listener the first to be called when the event is emitted
|
||||
* @param {string} event Event name
|
||||
* @param {function} listener Event listener
|
||||
*/
|
||||
EventEmitter.prototype.makeFirst = function (event, listener) {
|
||||
if (typeof this.events[event] !== 'object') {
|
||||
this.events[event] = [];
|
||||
}
|
||||
|
||||
const events = this.events[event];
|
||||
const idx = events.indexOf(listener);
|
||||
|
||||
if (idx > -1) {
|
||||
events.splice(idx, 1);
|
||||
}
|
||||
|
||||
events.unshift(listener);
|
||||
}
|
||||
|
||||
EventEmitter.prototype.removeListener = function (event, listener) {
|
||||
var idx;
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "الإعدادات المسبقة لـ Kobold",
|
||||
"guikoboldaisettings": "إعدادات واجهة KoboldAI",
|
||||
"novelaipreserts": "الإعدادات المسبقة لـ NovelAI",
|
||||
"default": "افتراضي",
|
||||
"openaipresets": "الإعدادات المسبقة لـ OpenAI",
|
||||
"text gen webio(ooba) presets": "الإعدادات المسبقة لـ WebUI(ooba)",
|
||||
"response legth(tokens)": "طول الاستجابة (بعدد الاحرف او الرموز)",
|
||||
@@ -62,7 +61,7 @@
|
||||
"Temperature": "درجة الحرارة",
|
||||
"Frequency Penalty": "عقوبة التكرار",
|
||||
"Presence Penalty": "عقوبة الوجود",
|
||||
"Top-p": "أعلى p",
|
||||
"Top-p": "أعلى p",
|
||||
"Display bot response text chunks as they are generated": "عرض النصوص لجظة بلحظة",
|
||||
"Top A": "أعلى A",
|
||||
"Typical Sampling": "عينة نموذجية",
|
||||
@@ -101,7 +100,7 @@
|
||||
"Inserts jailbreak as a last system message.": "يدرج كسر الحظر كرسالة نظام أخيرة.",
|
||||
"This tells the AI to ignore its usual content restrictions.": "هذا يخبر الذكاء الاصطناعي بتجاهل القيود المعتادة على المحتوى.",
|
||||
"NSFW Encouraged": "NSFW مشجع",
|
||||
"Tell the AI that NSFW is allowed.": "قل للذكاء الاصطناعي أنه يُسمح بـ NSFW",
|
||||
"Tell the AI that NSFW is allowed.": "قل للذكاء الاصطناعي أنه يُسمح بـ NSFW",
|
||||
"NSFW Prioritized": "الأولوية للمحتوى غير مناسب للعمل",
|
||||
"NSFW prompt text goes first in the prompt to emphasize its effect.": "النص الغير مناسب للعمل يأتي أولاً في التعليمات لتأكيد تأثيره.",
|
||||
"Streaming": "البث المباشر ل",
|
||||
@@ -141,7 +140,7 @@
|
||||
"Influences bot behavior in its responses": "يؤثر على سلوك الروبوت في ردوده.",
|
||||
"Connect": "الاتصال",
|
||||
"Test Message": "رسالة اختبار",
|
||||
"API": "واجهة برمجة التطبيقات (API)",
|
||||
"API": "واجهة برمجة التطبيقات (API)",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "استخدام Horde",
|
||||
"API url": "رابط API",
|
||||
@@ -206,7 +205,7 @@
|
||||
"Scale API Key": "مفتاح API لـ Scale",
|
||||
"Alt Method": "طريقة بديلة",
|
||||
"AI21 API Key": "مفتاح API لـ AI21",
|
||||
"AI21 Model": "نموذج AI21",
|
||||
"AI21 Model": "نموذج AI21",
|
||||
"View API Usage Metrics": "عرض مقاييس استخدام واجهة برمجة التطبيقات",
|
||||
"Show External models (provided by API)": "عرض النماذج الخارجية (المقدمة من قبل واجهة برمجة التطبيقات)",
|
||||
"Bot": "روبوت:",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "سرد العالم أولاً",
|
||||
"Recursive Scan": "فحص متكرر",
|
||||
"Case Sensitive": "حساس لحالة الأحرف",
|
||||
"Match whole words": "تطابق الكلمات الكاملة",
|
||||
"Alert On Overflow": "تنبيه عند التجاوز",
|
||||
"World/Lore Editor": "محرر العالم/السرد",
|
||||
"--- None ---": "--- لا شيء ---",
|
||||
@@ -915,7 +913,7 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "استخدم المحلل النحوي المناسب لنماذج Google عبر واجهة برمجة التطبيقات الخاصة بهم. معالجة الإشارات الأولية بطيئة، ولكنها تقدم عداد رمز دقيق جدًا.",
|
||||
"Load koboldcpp order": "تحميل أمر koboldcpp",
|
||||
"Use Google Tokenizer": "استخدم محلل النحوي من Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold-Einstellungen von vorher",
|
||||
"guikoboldaisettings": "KoboldAI-Einstellungen für das Menü",
|
||||
"novelaipreserts": "NovelAI-Einstellungen von früher",
|
||||
"default": "Normal",
|
||||
"openaipresets": "OpenAI-Einstellungen von vorher",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)-Einstellungen für Texterstellung",
|
||||
"response legth(tokens)": "Länge der Antwort (Tokens)",
|
||||
@@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Globale Lore zuerst",
|
||||
"Recursive Scan": "Rekursive Suche",
|
||||
"Case Sensitive": "Groß-/Kleinschreibung beachten",
|
||||
"Match whole words": "Ganze Wörter abgleichen",
|
||||
"Alert On Overflow": "Warnung bei Überlauf",
|
||||
"World/Lore Editor": "Welt-/Lore-Editor",
|
||||
"--- None ---": "--- Keine ---",
|
||||
@@ -917,5 +915,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Preajustes de Kobold",
|
||||
"guikoboldaisettings": "Ajustes de interfaz de KoboldAI",
|
||||
"novelaipreserts": "Preajustes de NovelAI",
|
||||
"default": "Predeterminado",
|
||||
"openaipresets": "Preajustes de OpenAI",
|
||||
"text gen webio(ooba) presets": "Preajustes de Text Gen WebUI(ooba)",
|
||||
"response legth(tokens)": "Longitud de respuesta (tokens)",
|
||||
@@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Historia Global Primero",
|
||||
"Recursive Scan": "Escaneo Recursiva",
|
||||
"Case Sensitive": "Sensible a mayúsculas y minúsculas",
|
||||
"Match whole words": "Coincidir palabras completas",
|
||||
"Alert On Overflow": "Alerta en Desbordamiento",
|
||||
"World/Lore Editor": "Editor de Mundo/Historia",
|
||||
"--- None ---": "--- Ninguno ---",
|
||||
@@ -891,6 +889,7 @@
|
||||
"Chat API": " API de chat",
|
||||
"and pick a character": "y elige un personaje",
|
||||
"in the chat bar": "en la barra de chat",
|
||||
"You can browse a list of bundled characters in the Download Extensions & Assets menu within": "Puedes explorar una lista de personajes incluidos en el menú de Download Extensions & Assets dentro de ",
|
||||
"Confused or lost?": "¿Confundido o perdido?",
|
||||
"click these icons!": "¡Haz clic en estos iconos!",
|
||||
"SillyTavern Documentation Site": "Sitio de documentación de SillyTavern",
|
||||
@@ -914,7 +913,4 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Usa el tokenizador apropiado para los modelos de Google a través de su API. Procesamiento de indicaciones más lento, pero ofrece un recuento de tokens mucho más preciso.",
|
||||
"Load koboldcpp order": "Cargar orden de koboldcpp",
|
||||
"Use Google Tokenizer": "Usar Tokenizador de Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Préréglages de Kobold",
|
||||
"guikoboldaisettings": "Paramètres de l'interface utilisateur de KoboldAI",
|
||||
"novelaipreserts": "Préréglages de NovelAI",
|
||||
"default": "Par défaut",
|
||||
"openaipresets": "Préréglages d'OpenAI",
|
||||
"text gen webio(ooba) presets": "Préréglages de WebUI(ooba)",
|
||||
"response legth(tokens)": "Longueur de la réponse (en tokens)",
|
||||
@@ -205,7 +204,7 @@
|
||||
"Scale API Key": "Clé API Scale",
|
||||
"Alt Method": "Méthode alternative",
|
||||
"AI21 API Key": "Clé API AI21",
|
||||
"AI21 Model": "Modèle AI21",
|
||||
"AI21 Model": "Modèle AI21",
|
||||
"View API Usage Metrics": "Afficher les mesures d'utilisation de l'API",
|
||||
"Show External models (provided by API)": "Afficher les modèles externes (fournis par l'API)",
|
||||
"Bot": "Bot",
|
||||
@@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Lore global d'abord",
|
||||
"Recursive Scan": "Analyse récursive",
|
||||
"Case Sensitive": "Sensible à la casse",
|
||||
"Match whole words": "Correspondre aux mots entiers",
|
||||
"Alert On Overflow": "Alerte en cas de dépassement",
|
||||
"World/Lore Editor": "Éditeur de monde/lore",
|
||||
"--- None ---": "--- Aucun ---",
|
||||
@@ -914,5 +912,5 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Utilisez le tokenizer approprié pour les modèles Google via leur API. Traitement des invitations plus lent, mais offre un décompte de jetons beaucoup plus précis.",
|
||||
"Load koboldcpp order": "Charger l'ordre koboldcpp",
|
||||
"Use Google Tokenizer": "Utiliser le tokenizer Google"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Fyrir stillingar Kobold",
|
||||
"guikoboldaisettings": "Stillingar fyrir KoboldAI viðmót",
|
||||
"novelaipreserts": "Fyrir stillingar NovelAI",
|
||||
"default": "Sjálfgefið",
|
||||
"openaipresets": "Fyrir stillingar OpenAI",
|
||||
"text gen webio(ooba) presets": "Fyrir stillingar WebUI(ooba) textagerðar",
|
||||
"response legth(tokens)": "Lengd svars (í táknum eða stöfum)",
|
||||
@@ -62,7 +61,7 @@
|
||||
"Temperature": "Hitastig",
|
||||
"Frequency Penalty": "Tíðnarefning",
|
||||
"Presence Penalty": "Tilkoma refning",
|
||||
"Top-p": "Topp-p",
|
||||
"Top-p": "Topp-p",
|
||||
"Display bot response text chunks as they are generated": "Birta bætir svarborðstextabrot þegar þau eru búnar til",
|
||||
"Top A": "Topp A",
|
||||
"Typical Sampling": "Venjuleg úrtaka",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Fyrst heimsfræði",
|
||||
"Recursive Scan": "Endurkvæm skoðun",
|
||||
"Case Sensitive": "Skilgreiningarfræðilegt",
|
||||
"Match whole words": "Nákvæm samræmi",
|
||||
"Alert On Overflow": "Viðvörun um flæði",
|
||||
"World/Lore Editor": "Heims-/fræðiritari",
|
||||
"--- None ---": "--- Engin ---",
|
||||
@@ -915,5 +913,5 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Notaðu rétta tokenizer fyrir Google módel með þeirra API. Hægri umhvörf fyrir hvöttavinnslu, en býður upp á miklu nákvæmari talningu á táknunum.",
|
||||
"Load koboldcpp order": "Hlaðið inn færslu af koboldcpp",
|
||||
"Use Google Tokenizer": "Notaðu Google Tokenizer"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Preimpostazioni Kobold",
|
||||
"guikoboldaisettings": "Impostazioni dell'interfaccia KoboldAI",
|
||||
"novelaipreserts": "Preimpostazioni NovelAI",
|
||||
"default": "Predefinito",
|
||||
"openaipresets": "Preimpostazioni OpenAI",
|
||||
"text gen webio(ooba) presets": "Preimpostazioni WebUI(ooba) per la generazione di testo",
|
||||
"response legth(tokens)": "Lunghezza della risposta (token)",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Lore Globale Prima",
|
||||
"Recursive Scan": "Scansione Ricorsiva",
|
||||
"Case Sensitive": "Sensibile alle Maiuscole",
|
||||
"Match whole words": "Corrispondi a parole intere",
|
||||
"Alert On Overflow": "Avviso Su Overflow",
|
||||
"World/Lore Editor": "Editor di Mondo/Lore",
|
||||
"--- None ---": "--- Nessuno ---",
|
||||
@@ -917,5 +915,5 @@
|
||||
"Use Google Tokenizer": "Usa il Tokenizer di Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Koboldのプリセット",
|
||||
"guikoboldaisettings": "KoboldAIのGUI設定",
|
||||
"novelaipreserts": "NovelAIのプリセット",
|
||||
"default": "デフォルト",
|
||||
"openaipresets": "OpenAIのプリセット",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)のプリセット",
|
||||
"response legth(tokens)": "応答の長さ(トークン数)",
|
||||
@@ -140,7 +139,7 @@
|
||||
"Influences bot behavior in its responses": "返信でボットの動作に影響を与えます",
|
||||
"Connect": "接続",
|
||||
"Test Message": "テストメッセージ",
|
||||
"API": "API",
|
||||
"API": "API",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "ホードを使用",
|
||||
"API url": "API URL",
|
||||
@@ -494,7 +493,6 @@
|
||||
"Global Lore First": "グローバルロアを最初に表示",
|
||||
"Recursive Scan": "再帰的スキャン",
|
||||
"Case Sensitive": "大文字と小文字を区別する",
|
||||
"Match whole words": "完全な単語の一致",
|
||||
"Alert On Overflow": "オーバーフロー時に警告",
|
||||
"World/Lore Editor": "ワールド/ロアの編集",
|
||||
"--- None ---": "--- なし ---",
|
||||
@@ -914,5 +912,5 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Googleモデル用の適切なトークナイザーを使用します。 API経由で。 処理が遅くなりますが、トークンの数え上げがはるかに正確になります。",
|
||||
"Load koboldcpp order": "koboldcppオーダーを読み込む",
|
||||
"Use Google Tokenizer": "Googleトークナイザーを使用"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "코볼드 사전 설정",
|
||||
"guikoboldaisettings": "KoboldAI 인터페이스 설정",
|
||||
"novelaipreserts": "NovelAI 사전 설정",
|
||||
"default": "기본값",
|
||||
"openaipresets": "OpenAI 사전 설정",
|
||||
"text gen webio(ooba) presets": "텍스트 생성 WebUI(ooba) 사전 설정",
|
||||
"response legth(tokens)": "응답 길이 (토큰)",
|
||||
@@ -425,7 +424,7 @@
|
||||
"Start new chat": "새로운 채팅 시작",
|
||||
"View past chats": "과거 채팅 보기",
|
||||
"Delete messages": "메시지 삭제",
|
||||
"Impersonate": "사칭",
|
||||
"Impersonate": "대신 말하기",
|
||||
"Regenerate": "재생성",
|
||||
"PNG": "PNG",
|
||||
"JSON": "JSON",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "글로벌 로어 우선",
|
||||
"Recursive Scan": "재귀 스캔",
|
||||
"Case Sensitive": "대소문자 구분",
|
||||
"Match whole words": "전체 단어 일치",
|
||||
"Alert On Overflow": "오버플로우 알림",
|
||||
"World/Lore Editor": "월드/로어 편집기",
|
||||
"--- None ---": "--- 없음 ---",
|
||||
@@ -914,7 +912,30 @@
|
||||
"Learn how to contribute your idle GPU cycles to the Horde": "여유로운 GPU 주기를 호드에 기여하는 방법 배우기",
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Google 모델용 적절한 토크나이저를 사용하여 API를 통해 제공됩니다. 더 느린 프롬프트 처리지만 훨씬 정확한 토큰 계산을 제공합니다.",
|
||||
"Load koboldcpp order": "코볼드 CPP 순서로 로드",
|
||||
"Use Google Tokenizer": "Google 토크나이저 사용"
|
||||
"Use Google Tokenizer": "구글 토크나이저 사용",
|
||||
"Hide Chat Avatars": "채팅 아바타 숨기기",
|
||||
"Hide avatars in chat messages.": "채팅 메시지에서 아바타 숨김.",
|
||||
"Avatar Hover Magnification": "아바타 마우스오버 시 확대",
|
||||
"Enable magnification for zoomed avatar display.": "마우스 오버 시 아바타가 커지도록 설정하세요.",
|
||||
"AutoComplete Settings": "자동 완성 설정",
|
||||
"Autocomplete Matching": "자동 완성 매칭",
|
||||
"Starts with": "시작하는 단어로",
|
||||
"Autocomplete Style": "자동 완성 스타일",
|
||||
"Includes": "포함하는",
|
||||
"Fuzzy": "퍼지 매칭",
|
||||
"Follow Theme": "테마 적용",
|
||||
"Dark": "다크 모드",
|
||||
"Sets the font size of the autocomplete.": "자동 완성 글꼴 크기 설정",
|
||||
"Autocomplete Width": "자동 완성 너비 조절",
|
||||
"Parser Flags": "파서 플래그 설정",
|
||||
"Sets default flags for the STscript parser.": "STscript 파서 기본 플래그 설정",
|
||||
"Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "모든 구분자를 백슬래시로 이스케이핑하고, 백슬래시 자체도 이스케이프할 수 있도록 엄격한 방식으로 전환합니다.",
|
||||
"STscript Settings": "STscript 설정",
|
||||
"Smooth Streaming": "부드러운 스트리밍",
|
||||
"Experimental feature. May not work for all backends.": "실험적인 기능으로, 모든 백엔드에서 작동이 보장되지는 않을 수 있습니다.",
|
||||
"Char List Subheader": "문자 목록 하위 제목",
|
||||
"Account": "계정",
|
||||
"Theme Colors": "테마 색상",
|
||||
"# Messages to Load": "로딩할 메시지 수"
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{ "lang": "ar-sa", "display": "عربي (Arabic)" },
|
||||
{ "lang": "zh-cn", "display": "简体中文 (Chinese) (Simplified)" },
|
||||
{ "lang": "zh-tw", "display": "繁體中文 (Chinese) (Taiwan)" },
|
||||
{ "lang": "nl-nl", "display": "Nederlands (Dutch)" },
|
||||
{ "lang": "de-de", "display": "Deutsch (German)" },
|
||||
{ "lang": "fr-fr", "display": "Français (French)" },
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold voorinstellingen",
|
||||
"guikoboldaisettings": "KoboldAI-interface-instellingen",
|
||||
"novelaipreserts": "NovelAI-voorinstellingen",
|
||||
"default": "Standaard",
|
||||
"openaipresets": "OpenAI-voorinstellingen",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)-voorinstellingen voor tekstgeneratie",
|
||||
"response legth(tokens)": "Reactielengte (tokens)",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Globale Lore Eerst",
|
||||
"Recursive Scan": "Recursieve Scan",
|
||||
"Case Sensitive": "Hoofdlettergevoelig",
|
||||
"Match whole words": "Hele woorden matchen",
|
||||
"Alert On Overflow": "Waarschuwing bij overloop",
|
||||
"World/Lore Editor": "Wereld/Lore Editor",
|
||||
"--- None ---": "--- Geen ---",
|
||||
@@ -917,5 +915,5 @@
|
||||
"Use Google Tokenizer": "Google Tokenizer gebruiken"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Configurações predefinidas do Kobold",
|
||||
"guikoboldaisettings": "Configurações da interface do KoboldAI",
|
||||
"novelaipreserts": "Configurações predefinidas do NovelAI",
|
||||
"default": "Padrão",
|
||||
"openaipresets": "Configurações predefinidas do OpenAI",
|
||||
"text gen webio(ooba) presets": "Configurações predefinidas do WebUI(ooba) para geração de texto",
|
||||
"response legth(tokens)": "Comprimento da resposta (tokens)",
|
||||
@@ -493,7 +492,6 @@
|
||||
"Global Lore First": "Lore Global Primeiro",
|
||||
"Recursive Scan": "Verificação Recursiva",
|
||||
"Case Sensitive": "Sensível a Maiúsculas",
|
||||
"Match whole words": "Corresponder palavras inteiras",
|
||||
"Alert On Overflow": "Alerta em Overflow",
|
||||
"World/Lore Editor": "Editor de Mundo/Lore",
|
||||
"--- None ---": "--- Nenhum ---",
|
||||
@@ -915,5 +913,5 @@
|
||||
"Use Google Tokenizer": "Usar Tokenizer do Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Пресеты для Kobold",
|
||||
"guikoboldaisettings": "Настройки из интерфейса KoboldAI",
|
||||
"novelaipreserts": "Пресеты для NovelAI",
|
||||
"default": "По умолчанию",
|
||||
"openaipresets": "Пресеты для OpenAI",
|
||||
"text gen webio(ooba) presets": "Пресеты для WebUI(ooba)",
|
||||
"response legth(tokens)": "Ответ (в токенах)",
|
||||
@@ -38,7 +37,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 +96,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 +261,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": "Выключено для всех моделей",
|
||||
@@ -276,7 +275,7 @@
|
||||
"World Info": "Информация о мире",
|
||||
"Scan Depth": "Глубина сканирования",
|
||||
"Case-Sensitive": "С учетом регистра",
|
||||
"Match Whole Words": "Только целые слова",
|
||||
"Match Whole Words": "Только полное совпадение",
|
||||
"Use global setting": "Использовать глобальную настройку",
|
||||
"Yes": "Да",
|
||||
"No": "Нет",
|
||||
@@ -300,11 +299,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 +313,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 +380,7 @@
|
||||
"text": "текст",
|
||||
"Delete": "Удалить",
|
||||
"Cancel": "Отменить",
|
||||
"Advanced Defininitions": "Продвинутое описание",
|
||||
"Advanced Defininitions": "Расширенное описание",
|
||||
"Personality summary": "Сводка по личности",
|
||||
"A brief description of the personality": "Краткое описание личности",
|
||||
"Scenario": "Сценарий",
|
||||
@@ -431,7 +430,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 +474,7 @@
|
||||
"--- Pick to Edit ---": "--- Выберите для редактирования ---",
|
||||
"or": "или",
|
||||
"New": "Новый",
|
||||
"Priority": "Приритет",
|
||||
"Priority": "Приоритет",
|
||||
"Custom": "Пользовательский",
|
||||
"Title A-Z": "Название от A до Z",
|
||||
"Title Z-A": "Название от Z до A",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Сначала глобальный лор",
|
||||
"Recursive Scan": "Рекурсивное сканирование",
|
||||
"Case Sensitive": "Учитывать регистр",
|
||||
"Match whole words": "Только полное совпадение",
|
||||
"Alert On Overflow": "Оповещение о переполнении",
|
||||
"World/Lore Editor": "Редактировать мир или лор",
|
||||
"--- None ---": "--- Отсутствует ---",
|
||||
@@ -528,7 +526,7 @@
|
||||
"UI Border": "Границы UI",
|
||||
"Chat Style:": "Стиль чата",
|
||||
"Chat Width (PC)": "Ширина чата (для ПК)",
|
||||
"Chat Timestamps": "Временные метки в чате",
|
||||
"Chat Timestamps": "Метки времени в чате",
|
||||
"Tags as Folders": "Теги как папки",
|
||||
"Chat Truncation": "Усечение чата",
|
||||
"(0 = unlimited)": "(0 = неограниченное)",
|
||||
@@ -559,8 +557,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 +598,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 +627,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 +652,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 +668,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 +710,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 +844,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 +860,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": "Отправлять встроенные изображения",
|
||||
@@ -888,11 +885,11 @@
|
||||
"Want to update?": "Хотите обновиться?",
|
||||
"How to start chatting?": "Как начать общение?",
|
||||
"Click": "Нажмите",
|
||||
"and select a": " и выберите",
|
||||
"and select a": " и выберите ",
|
||||
"Chat API": "API чата",
|
||||
"and pick a character": "и выберите персонажа",
|
||||
"in the chat bar": " в поле чата",
|
||||
"Confused or lost?": "Запутались или потерялись?",
|
||||
"Confused or lost?": "Не можете в чём-то разобраться?",
|
||||
"click these icons!": "нажмите на эти значки!",
|
||||
"SillyTavern Documentation Site": "Сайт документации SillyTavern",
|
||||
"Extras Installation Guide": "Руководство по установке Extras",
|
||||
@@ -973,12 +970,209 @@
|
||||
"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.": "Используется в качестве разделителя между уже имеющимся сообщением и его новым отрывком, при генерации продолжения",
|
||||
"Regex Editor": "Редактор рег. выражений",
|
||||
"ext_regex_open_editor": "Открыть редактор",
|
||||
"ext_regex_import_script": "Импорт скрипта",
|
||||
"ext_regex_saved_scripts": "Сохранённые скрипты",
|
||||
"ext_regex_desc": "Regex - это инструмент, позволяющий находить и изменять строки, используя регулярные выражения. Для более подробной информации нажмите ? рядом с заголовком.",
|
||||
"Input": "Поле ввода",
|
||||
"ext_regex_test_input_placeholder": "Введите текст...",
|
||||
"Output": "Результат",
|
||||
"ext_regex_output_placeholder": "Пусто",
|
||||
"Script Name": "Название скрипта",
|
||||
"Find Regex": "Рег. выражение для поиска",
|
||||
"Replace With": "Замена",
|
||||
"ext_regex_replace_string_placeholder": "Чтобы вставить всё вхождение рег. выражения, используйте {{match}}. Чтобы вставить группу символов, используйте $1, $2 и т.д.",
|
||||
"Trim Out": "Усечение",
|
||||
"ext_regex_trim_placeholder": "Удалить перед обработкой ненужные части текста. Каждый элемент с новой строки.",
|
||||
"Slash Commands": "Слэш-команды",
|
||||
"Min Depth": "Мин. глубина",
|
||||
"ext_regex_min_depth_desc": "При форматировании затрагивать только те сообщения, которые находятся как минимум на глубине N. 0 = последнее сообщение, 1 = предпоследнее и т.д. Учитываются только видимые сообщения, т.е. не скрытые и не системные.",
|
||||
"ext_regex_max_depth_desc": "При форматировании затрагивать только те сообщения, которые находятся на глубине не более N. 0 = последнее сообщение, 1 = предпоследнее и т.д. Учитываются только видимые сообщения, т.е. не скрытые и не системные.",
|
||||
"ext_regex_min_depth_placeholder": "Неогранич.",
|
||||
"ext_regex_other_options": "Другие опции",
|
||||
"Only Format Display": "Только визуально",
|
||||
"ext_regex_only_format_prompt_desc": "История чата не изменится, замена будет осуществляться только в промпте (при генерации)",
|
||||
"Only Format Prompt (?)": "Только промпт",
|
||||
"Run On Edit": "Выполнять при редактировании",
|
||||
"Substitute Regex": "Заменить в рег. выражении",
|
||||
"ext_regex_substitute_regex_desc": "Перед выполнением заменять {{макросы}} в рег. выражении",
|
||||
"Test Mode": "Протестировать",
|
||||
"ext_regex_affects": "Затрагивает",
|
||||
"ext_regex_user_input": "Ваши сообщения",
|
||||
"ext_regex_ai_output": "Ответы ИИ",
|
||||
"ext_regex_disable_script": "Отключить скрипт",
|
||||
"ext_regex_enable_script": "Включить скрипт",
|
||||
"ext_regex_edit_script": "Редактировать",
|
||||
"ext_regex_export_script": "Экспортировать",
|
||||
"ext_regex_delete_script": "Удалить",
|
||||
"ext_sum_with": "Для пересказа использовать:",
|
||||
"ext_sum_main_api": "Основное API",
|
||||
"ext_sum_current_summary": "Текущий пересказ:",
|
||||
"ext_sum_restore_previous": "Восстановить предыдущий",
|
||||
"ext_sum_memory_placeholder": "Сгенерированный пересказ будет здесь...",
|
||||
"ext_sum_force_text": "Пересказать сейчас",
|
||||
"ext_sum_force_tip": "Сгенерировать пересказ прямо сейчас.",
|
||||
"Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "Отключить авто-обновление пересказа. Пересказ всё время будет фиксированным. Однако останется возможность принудительно обновить пересказ через кнопку \"Пересказать сейчас\" (доступно только через Основное API)",
|
||||
"ext_sum_pause": "Приостановить",
|
||||
"Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "Исключать из пересказа Информацию о мире и Заметки автора. Работает только для Основного API. Extras API всегда их исключает.",
|
||||
"ext_sum_no_wi_an": "Без мира и заметок",
|
||||
"ext_sum_settings_tip": "Изменить промпт пересказа, место для инжекта и т.д.",
|
||||
"ext_sum_settings": "Настройки пересказа",
|
||||
"ext_sum_prompt_builder": "Алгоритм формирования промпта",
|
||||
"ext_sum_prompt_builder_1_desc": "Расширение само составит промпт с учётом непересказанных сообщений. Во время генерации чат недоступен.",
|
||||
"ext_sum_prompt_builder_1": "Прямой, блокирующий",
|
||||
"ext_sum_prompt_builder_2_desc": "Расширение само составит промпт с учётом непересказанных сообщений. Во время генерации чат доступен. Может не поддерживаться некоторыми бэкендами.",
|
||||
"ext_sum_prompt_builder_2": "Прямой, неблокирующий",
|
||||
"ext_sum_prompt_builder_3_desc": "Расширение будет использовать стандартные основные настройки промпта, и добавит свой промпт в качестве последнего системного сообщения.",
|
||||
"ext_sum_prompt_builder_3": "Классический, блокирующий",
|
||||
"Summary Prompt": "Промпт для пересказа",
|
||||
"ext_sum_restore_default_prompt_tip": "Восстановить стандартный промпт",
|
||||
"ext_sum_prompt_placeholder": "Этот промпт будет отправлен ИИ при запросе на генерацию пересказа. Макрос {{words}} будет заменён на значение параметра \"Количество слов\".",
|
||||
"ext_sum_target_length_1": "Целевая длина пересказа (слов):",
|
||||
"ext_sum_target_length_2": "",
|
||||
"ext_sum_target_length_3": "",
|
||||
"ext_sum_api_response_length_1": "Длина ответа от API (токенов):",
|
||||
"ext_sum_api_response_length_2": "",
|
||||
"ext_sum_api_response_length_3": " ",
|
||||
"ext_sum_0_default": "по умолчанию = 0",
|
||||
"ext_sum_raw_max_msg": "[Прямое форматирование] Макс. сообщений в запросе",
|
||||
"ext_sum_0_unlimited": "неограничено = 0",
|
||||
"Update frequency": "Частота обновления",
|
||||
"ext_sum_update_every_messages_1": "Интервал обновления (кол-во сообщений):",
|
||||
"ext_sum_update_every_messages_2": "",
|
||||
"ext_sum_pause": "Приостановить",
|
||||
"ext_sum_update_every_words_1": "Интервал обновления (кол-во слов):",
|
||||
"ext_sum_update_every_words_2": "",
|
||||
"ext_sum_0_disable": "для отключения поставьте 0",
|
||||
"ext_sum_auto_adjust_desc": "Попытаться автоматически рассчитать значение интервала, исходя из статистики чата",
|
||||
"ext_sum_both_sliders": "Если оба ползунка отличны от нуля, то оба будут триггерить генерацию пересказа с соответствующей периодичностью.",
|
||||
"ext_sum_injection_template": "Шаблон для инжекта",
|
||||
"ext_sum_memory_template_placeholder": "Макрос {{summary}} будет заменён на содержимое пересказа",
|
||||
"ext_sum_injection_position": "Куда инжектить",
|
||||
"How many messages before the current end of the chat.": "Сколько сообщений от конца чата."
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Налаштування Kobold",
|
||||
"guikoboldaisettings": "З інтерфейсу KoboldAI",
|
||||
"novelaipreserts": "Налаштування NovelAI",
|
||||
"default": "За замовчуванням",
|
||||
"openaipresets": "Налаштування OpenAI",
|
||||
"text gen webio(ooba) presets": "Налаштування Text Completion",
|
||||
"response legth(tokens)": "Відповідь (токени)",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Глобальна інформація першою",
|
||||
"Recursive Scan": "Рекурсивне сканування",
|
||||
"Case Sensitive": "Чутливість до регістру",
|
||||
"Match whole words": "Відповідність цілим словам",
|
||||
"Alert On Overflow": "Сповіщення при переповненні",
|
||||
"World/Lore Editor": "Редактор світу/книги",
|
||||
"--- None ---": "--- Нічого ---",
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Cài đặt trước Kobold",
|
||||
"guikoboldaisettings": "Cài đặt giao diện KoboldAI",
|
||||
"novelaipreserts": "Cài đặt trước NovelAI",
|
||||
"default": "Mặc định",
|
||||
"openaipresets": "Cài đặt trước OpenAI",
|
||||
"text gen webio(ooba) presets": "Cài đặt trước WebUI(ooba) của máy tạo văn bản",
|
||||
"response legth(tokens)": "Độ dài phản hồi (trong các token)",
|
||||
@@ -62,7 +61,7 @@
|
||||
"Temperature": "Nhiệt độ",
|
||||
"Frequency Penalty": "Phạt Tần số",
|
||||
"Presence Penalty": "Phạt Sự hiện",
|
||||
"Top-p": "Top-p",
|
||||
"Top-p": "Top-p",
|
||||
"Display bot response text chunks as they are generated": "Hiển thị các phần văn bản phản hồi của bot khi chúng được tạo ra",
|
||||
"Top A": "Top A",
|
||||
"Typical Sampling": "Mẫu Đại diện",
|
||||
@@ -141,7 +140,7 @@
|
||||
"Influences bot behavior in its responses": "Ảnh hưởng đến hành vi của bot trong các phản hồi của nó",
|
||||
"Connect": "Kết nối",
|
||||
"Test Message": "Tin nhắn kiểm tra",
|
||||
"API": "Giao diện lập trình ứng dụng (API)",
|
||||
"API": "Giao diện lập trình ứng dụng (API)",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "Sử dụng Horde",
|
||||
"API url": "URL API",
|
||||
@@ -206,7 +205,7 @@
|
||||
"Scale API Key": "Khóa API của Scale",
|
||||
"Alt Method": "Phương pháp thay thế",
|
||||
"AI21 API Key": "Khóa API của AI21",
|
||||
"AI21 Model": "Mô hình AI21",
|
||||
"AI21 Model": "Mô hình AI21",
|
||||
"View API Usage Metrics": "Xem số liệu sử dụng API",
|
||||
"Show External models (provided by API)": "Hiển thị các mô hình bên ngoài (do API cung cấp)",
|
||||
"Bot": "Bot:",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Sử liệu toàn cầu đầu tiên",
|
||||
"Recursive Scan": "Quét đệ quy",
|
||||
"Case Sensitive": "Phân biệt chữ hoa chữ thường",
|
||||
"Match whole words": "Khớp toàn bộ từ",
|
||||
"Alert On Overflow": "Cảnh báo khi tràn",
|
||||
"World/Lore Editor": "Trình soạn thảo Thế giới/Sử liệu",
|
||||
"--- None ---": "--- Không ---",
|
||||
@@ -915,5 +913,5 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Sử dụng bộ mã hóa phù hợp cho các mô hình của Google thông qua API của họ. Xử lý lời mời chậm hơn, nhưng cung cấp đếm token chính xác hơn nhiều.",
|
||||
"Load koboldcpp order": "Tải đơn hàng koboldcpp",
|
||||
"Use Google Tokenizer": "Sử dụng bộ mã hóa của Google"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold 预设",
|
||||
"guikoboldaisettings": "KoboldAI 用户界面设置",
|
||||
"novelaipreserts": "NovelAI 预设",
|
||||
"default": "默认",
|
||||
"openaipresets": "对话补全预设",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba) 预设",
|
||||
"response legth(tokens)": "响应长度(以词符数计)",
|
||||
@@ -495,7 +494,6 @@
|
||||
"Global Lore First": "全局世界书优先",
|
||||
"Recursive Scan": "递归扫描",
|
||||
"Case Sensitive": "区分大小写",
|
||||
"Match whole words": "完整匹配单词",
|
||||
"Alert On Overflow": "溢出警报",
|
||||
"World/Lore Editor": "世界书编辑器",
|
||||
"--- None ---": "--- 无 ---",
|
||||
|
1192
public/locales/zh-tw.json
Normal file
1192
public/locales/zh-tw.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,13 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="darkreader-lock">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<style>
|
||||
/* Put critical CSS here. The rest should go in stylesheets. */
|
||||
body {
|
||||
background-color: rgb(36, 36, 37);
|
||||
}
|
||||
</style>
|
||||
<link rel="preload" as="style" href="style.css">
|
||||
<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" />
|
||||
@@ -20,8 +27,8 @@
|
||||
<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/fontawesome.min.css" rel="stylesheet">
|
||||
<link href="css/solid.min.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>
|
||||
|
1685
public/script.js
1685
public/script.js
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
}
|
||||
|
@@ -36,6 +36,7 @@ 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) {
|
||||
@@ -377,6 +378,7 @@ function RA_autoconnect(PrevApi) {
|
||||
|| (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI)
|
||||
|| (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
|
||||
|| (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
|
||||
|| (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
|
||||
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
|
||||
) {
|
||||
$('#api_button_openai').trigger('click');
|
||||
@@ -422,7 +424,7 @@ function restoreUserInput() {
|
||||
|
||||
const userInput = LoadLocal('userInput');
|
||||
if (userInput) {
|
||||
$('#send_textarea').val(userInput).trigger('input');
|
||||
$('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,12 +702,12 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
*/
|
||||
function autoFitSendTextArea() {
|
||||
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
|
||||
if (sendTextArea.scrollHeight == sendTextArea.offsetHeight) {
|
||||
if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {
|
||||
// Needs to be pulled dynamically because it is affected by font size changes
|
||||
const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height');
|
||||
sendTextArea.style.height = sendTextAreaMinHeight;
|
||||
}
|
||||
sendTextArea.style.height = sendTextArea.scrollHeight + 0.3 + 'px';
|
||||
sendTextArea.style.height = sendTextArea.scrollHeight + 3 + 'px';
|
||||
|
||||
if (!isFirefox) {
|
||||
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));
|
||||
@@ -1131,6 +1133,11 @@ export function initRossMods() {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#dialogue_del_mes_cancel').is(':visible')) {
|
||||
$('#dialogue_del_mes_cancel').trigger('click');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('.drawer-content')
|
||||
.not('#WorldInfo')
|
||||
.not('#left-nav-panel')
|
||||
|
@@ -9,9 +9,12 @@ import {
|
||||
} from '../script.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { getCharaFilename, debounce, delay } from './utils.js';
|
||||
import { getTokenCountAsync } from './tokenizers.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
export { MODULE_NAME as NOTE_MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
|
||||
@@ -84,9 +87,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();
|
||||
@@ -454,9 +457,59 @@ export function initAuthorsNote() {
|
||||
});
|
||||
$('#option_toggle_AN').on('click', onANMenuItemClick);
|
||||
|
||||
registerSlashCommand('note', setNoteTextCommand, [], '<span class=\'monospace\'>(text)</span> – sets an author\'s note for the currently selected chat', true, true);
|
||||
registerSlashCommand('depth', setNoteDepthCommand, [], '<span class=\'monospace\'>(number)</span> – sets an author\'s note depth for in-chat positioning', true, true);
|
||||
registerSlashCommand('freq', setNoteIntervalCommand, ['interval'], '<span class=\'monospace\'>(number)</span> – sets an author\'s note insertion frequency', true, true);
|
||||
registerSlashCommand('pos', setNotePositionCommand, ['position'], '(<span class=\'monospace\'>chat</span> or <span class=\'monospace\'>scenario</span>) – sets an author\'s note position', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'note',
|
||||
callback: setNoteTextCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'text', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets an author's note for the currently selected chat.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'depth',
|
||||
callback: setNoteDepthCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'number', [ARGUMENT_TYPE.NUMBER], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets an author's note depth for in-chat positioning.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'freq',
|
||||
callback: setNoteIntervalCommand,
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'number', [ARGUMENT_TYPE.NUMBER], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets an author's note insertion frequency.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pos',
|
||||
callback: setNotePositionCommand,
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'position', [ARGUMENT_TYPE.STRING], true, false, null, ['chat', 'scenario'],
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets an author's note position.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
}
|
||||
|
792
public/scripts/autocomplete/AutoComplete.js
Normal file
792
public/scripts/autocomplete/AutoComplete.js
Normal file
@@ -0,0 +1,792 @@
|
||||
import { power_user } from '../power-user.js';
|
||||
import { debounce, escapeRegex } from '../utils.js';
|
||||
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
|
||||
import { BlankAutoCompleteOption } from './BlankAutoCompleteOption.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
|
||||
import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Number}*/
|
||||
export const AUTOCOMPLETE_WIDTH = {
|
||||
'INPUT': 0,
|
||||
'CHAT': 1,
|
||||
'FULL': 2,
|
||||
};
|
||||
|
||||
export class AutoComplete {
|
||||
/**@type {HTMLTextAreaElement}*/ textarea;
|
||||
/**@type {boolean}*/ isFloating = false;
|
||||
/**@type {()=>boolean}*/ checkIfActivate;
|
||||
/**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
|
||||
|
||||
/**@type {boolean}*/ isActive = false;
|
||||
/**@type {boolean}*/ isReplaceable = false;
|
||||
/**@type {boolean}*/ isShowingDetails = false;
|
||||
/**@type {boolean}*/ wasForced = false;
|
||||
/**@type {boolean}*/ isForceHidden = false;
|
||||
/**@type {boolean}*/ canBeAutoHidden = false;
|
||||
|
||||
/**@type {string}*/ text;
|
||||
/**@type {AutoCompleteNameResult}*/ parserResult;
|
||||
/**@type {AutoCompleteSecondaryNameResult}*/ secondaryParserResult;
|
||||
get effectiveParserResult() { return this.secondaryParserResult ?? this.parserResult; }
|
||||
/**@type {string}*/ name;
|
||||
|
||||
/**@type {boolean}*/ startQuote;
|
||||
/**@type {boolean}*/ endQuote;
|
||||
/**@type {number}*/ selectionStart;
|
||||
|
||||
/**@type {RegExp}*/ fuzzyRegex;
|
||||
|
||||
/**@type {AutoCompleteOption[]}*/ result = [];
|
||||
/**@type {AutoCompleteOption}*/ selectedItem = null;
|
||||
|
||||
/**@type {HTMLElement}*/ clone;
|
||||
/**@type {HTMLElement}*/ domWrap;
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ detailsWrap;
|
||||
/**@type {HTMLElement}*/ detailsDom;
|
||||
|
||||
/**@type {function}*/ renderDebounced;
|
||||
/**@type {function}*/ renderDetailsDebounced;
|
||||
/**@type {function}*/ updatePositionDebounced;
|
||||
/**@type {function}*/ updateDetailsPositionDebounced;
|
||||
/**@type {function}*/ updateFloatingPositionDebounced;
|
||||
|
||||
get matchType() {
|
||||
return power_user.stscript.matching ?? 'fuzzy';
|
||||
}
|
||||
|
||||
get autoHide() {
|
||||
return power_user.stscript.autocomplete.autoHide ?? false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete.
|
||||
* @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
|
||||
* @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.
|
||||
* @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
|
||||
*/
|
||||
constructor(textarea, checkIfActivate, getNameAt, isFloating = false) {
|
||||
this.textarea = textarea;
|
||||
this.checkIfActivate = checkIfActivate;
|
||||
this.getNameAt = getNameAt;
|
||||
this.isFloating = isFloating;
|
||||
|
||||
this.domWrap = document.createElement('div'); {
|
||||
this.domWrap.classList.add('autoComplete-wrap');
|
||||
if (isFloating) this.domWrap.classList.add('isFloating');
|
||||
}
|
||||
this.dom = document.createElement('ul'); {
|
||||
this.dom.classList.add('autoComplete');
|
||||
this.domWrap.append(this.dom);
|
||||
}
|
||||
this.detailsWrap = document.createElement('div'); {
|
||||
this.detailsWrap.classList.add('autoComplete-detailsWrap');
|
||||
if (isFloating) this.detailsWrap.classList.add('isFloating');
|
||||
}
|
||||
this.detailsDom = document.createElement('div'); {
|
||||
this.detailsDom.classList.add('autoComplete-details');
|
||||
this.detailsWrap.append(this.detailsDom);
|
||||
}
|
||||
|
||||
this.renderDebounced = debounce(this.render.bind(this), 10);
|
||||
this.renderDetailsDebounced = debounce(this.renderDetails.bind(this), 10);
|
||||
this.updatePositionDebounced = debounce(this.updatePosition.bind(this), 10);
|
||||
this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
|
||||
this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
|
||||
|
||||
textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced));
|
||||
textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
|
||||
textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);
|
||||
textarea.addEventListener('selectionchange', ()=>this.show());
|
||||
textarea.addEventListener('blur', ()=>this.hide());
|
||||
if (isFloating) {
|
||||
textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
|
||||
}
|
||||
window.addEventListener('resize', ()=>this.updatePositionDebounced());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AutoCompleteOption} option
|
||||
*/
|
||||
makeItem(option) {
|
||||
const li = option.renderItem();
|
||||
// gotta listen to pointerdown (happens before textarea-blur)
|
||||
li.addEventListener('pointerdown', (evt)=>{
|
||||
evt.preventDefault();
|
||||
this.selectedItem = this.result.find(it=>it.name == li.getAttribute('data-name'));
|
||||
this.select();
|
||||
});
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AutoCompleteOption} item
|
||||
*/
|
||||
updateName(item) {
|
||||
const chars = Array.from(item.dom.querySelector('.name').children);
|
||||
switch (this.matchType) {
|
||||
case 'strict': {
|
||||
chars.forEach((it, idx)=>{
|
||||
if (idx + item.nameOffset < item.name.length) {
|
||||
it.classList.add('matched');
|
||||
} else {
|
||||
it.classList.remove('matched');
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'includes': {
|
||||
const start = item.name.toLowerCase().search(this.name);
|
||||
chars.forEach((it, idx)=>{
|
||||
if (idx + item.nameOffset < start) {
|
||||
it.classList.remove('matched');
|
||||
} else if (idx + item.nameOffset < start + item.name.length) {
|
||||
it.classList.add('matched');
|
||||
} else {
|
||||
it.classList.remove('matched');
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'fuzzy': {
|
||||
item.name.replace(this.fuzzyRegex, (_, ...parts)=>{
|
||||
parts.splice(-2, 2);
|
||||
if (parts.length == 2) {
|
||||
chars.forEach(c=>c.classList.remove('matched'));
|
||||
} else {
|
||||
let cIdx = item.nameOffset;
|
||||
parts.forEach((it, idx)=>{
|
||||
if (it === null || it.length == 0) return '';
|
||||
if (idx % 2 == 1) {
|
||||
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.add('matched'));
|
||||
} else {
|
||||
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.remove('matched'));
|
||||
}
|
||||
cIdx += it.length;
|
||||
});
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate score for the fuzzy match.
|
||||
* @param {AutoCompleteOption} option
|
||||
* @returns The option.
|
||||
*/
|
||||
fuzzyScore(option) {
|
||||
const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
|
||||
let start = null;
|
||||
let consecutive = [];
|
||||
let current = '';
|
||||
let offset = 0;
|
||||
parts.forEach((part, idx) => {
|
||||
if (idx % 2 == 0) {
|
||||
if (part.length > 0) {
|
||||
if (current.length > 0) {
|
||||
consecutive.push(current);
|
||||
}
|
||||
current = '';
|
||||
}
|
||||
} else {
|
||||
if (start === null) {
|
||||
start = offset;
|
||||
}
|
||||
current += part;
|
||||
}
|
||||
offset += part.length;
|
||||
});
|
||||
if (current.length > 0) {
|
||||
consecutive.push(current);
|
||||
}
|
||||
consecutive.sort((a,b)=>b.length - a.length);
|
||||
option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two auto complete options by their fuzzy score.
|
||||
* @param {AutoCompleteOption} a
|
||||
* @param {AutoCompleteOption} b
|
||||
*/
|
||||
fuzzyScoreCompare(a, b) {
|
||||
if (a.score.start < b.score.start) return -1;
|
||||
if (a.score.start > b.score.start) return 1;
|
||||
if (a.score.longestConsecutive > b.score.longestConsecutive) return -1;
|
||||
if (a.score.longestConsecutive < b.score.longestConsecutive) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
basicAutoHideCheck() {
|
||||
// auto hide only if at least one char has been typed after the name + space
|
||||
return this.textarea.selectionStart > this.parserResult.start
|
||||
+ this.parserResult.name.length
|
||||
+ (this.startQuote ? 1 : 0)
|
||||
+ (this.endQuote ? 1 : 0)
|
||||
+ 1
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the autocomplete.
|
||||
* @param {boolean} isInput Whether triggered by input.
|
||||
* @param {boolean} isForced Whether force-showing (ctrl+space).
|
||||
* @param {boolean} isSelect Whether an autocomplete option was just selected.
|
||||
*/
|
||||
async show(isInput = false, isForced = false, isSelect = false) {
|
||||
//TODO check if isInput and isForced are both required
|
||||
this.text = this.textarea.value;
|
||||
this.isReplaceable = false;
|
||||
|
||||
if (document.activeElement != this.textarea) {
|
||||
// only show with textarea in focus
|
||||
return this.hide();
|
||||
}
|
||||
if (!this.checkIfActivate()) {
|
||||
// only show if provider wants to
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
// disable force-hide if trigger was forced
|
||||
if (isForced) this.isForceHidden = false;
|
||||
|
||||
// request provider to get name result (potentially "incomplete", i.e. not an actual existing name) for
|
||||
// cursor position
|
||||
this.parserResult = await this.getNameAt(this.text, this.textarea.selectionStart);
|
||||
this.secondaryParserResult = null;
|
||||
|
||||
if (!this.parserResult) {
|
||||
// don't show if no name result found, e.g., cursor's area is not a command
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
// need to know if name can be inside quotes, and then check if quotes are already there
|
||||
if (this.parserResult.canBeQuoted) {
|
||||
this.startQuote = this.text[this.parserResult.start] == '"';
|
||||
this.endQuote = this.startQuote && this.text[this.parserResult.start + this.parserResult.name.length + 1] == '"';
|
||||
} else {
|
||||
this.startQuote = false;
|
||||
this.endQuote = false;
|
||||
}
|
||||
|
||||
// use lowercase name for matching
|
||||
this.name = this.parserResult.name.toLowerCase() ?? '';
|
||||
|
||||
const isCursorInNamePart = this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0);
|
||||
if (isForced || isInput) {
|
||||
// if forced (ctrl+space) or user input...
|
||||
if (isCursorInNamePart) {
|
||||
// ...and cursor is somewhere in the name part (including right behind the final char)
|
||||
// -> show autocomplete for the (partial if cursor in the middle) name
|
||||
this.name = this.name.slice(0, this.textarea.selectionStart - (this.parserResult.start) - (this.startQuote ? 1 : 0));
|
||||
this.parserResult.name = this.name;
|
||||
this.isReplaceable = true;
|
||||
this.isForceHidden = false;
|
||||
this.canBeAutoHidden = false;
|
||||
} else {
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
} else {
|
||||
// if not forced and no user input -> just show details
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
|
||||
if (isForced || isInput || isSelect) {
|
||||
// is forced or user input or just selected autocomplete option...
|
||||
if (!isCursorInNamePart) {
|
||||
// ...and cursor is not somwehere in the main name part -> check for secondary options (e.g., named arguments)
|
||||
const result = this.parserResult.getSecondaryNameAt(this.text, this.textarea.selectionStart, isSelect);
|
||||
if (result && (isForced || result.isRequired)) {
|
||||
this.secondaryParserResult = result;
|
||||
this.name = this.secondaryParserResult.name;
|
||||
this.isReplaceable = isForced || this.secondaryParserResult.isRequired;
|
||||
this.isForceHidden = false;
|
||||
this.canBeAutoHidden = false;
|
||||
} else {
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.matchType == 'fuzzy') {
|
||||
// only build the fuzzy regex if match type is set to fuzzy
|
||||
this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
|
||||
}
|
||||
|
||||
//TODO maybe move the matchers somewhere else; a single match function? matchType is available as property
|
||||
const matchers = {
|
||||
'strict': (name) => name.toLowerCase().startsWith(this.name),
|
||||
'includes': (name) => name.toLowerCase().includes(this.name),
|
||||
'fuzzy': (name) => this.fuzzyRegex.test(name),
|
||||
};
|
||||
|
||||
this.result = this.effectiveParserResult.optionList
|
||||
// filter the list of options by the partial name according to the matching type
|
||||
.filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)
|
||||
// remove aliases
|
||||
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
|
||||
|
||||
if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
|
||||
// no matching secondary results and forced trigger -> show current command details
|
||||
this.secondaryParserResult = null;
|
||||
this.result = [this.effectiveParserResult.optionList.find(it=>it.name == this.effectiveParserResult.name)];
|
||||
this.name = this.effectiveParserResult.name;
|
||||
this.fuzzyRegex = /(.*)(.*)(.*)/;
|
||||
}
|
||||
|
||||
this.result = this.result
|
||||
// update remaining options
|
||||
.map(option => {
|
||||
// build element
|
||||
option.dom = this.makeItem(option);
|
||||
// update replacer and add quotes if necessary
|
||||
if (this.effectiveParserResult.canBeQuoted) {
|
||||
option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;
|
||||
} else {
|
||||
option.replacer = option.name;
|
||||
}
|
||||
// calculate fuzzy score if matching is fuzzy
|
||||
if (this.matchType == 'fuzzy') this.fuzzyScore(option);
|
||||
// update the name to highlight the matched chars
|
||||
this.updateName(option);
|
||||
return option;
|
||||
})
|
||||
// sort by fuzzy score or alphabetical
|
||||
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
|
||||
;
|
||||
|
||||
|
||||
|
||||
if (this.isForceHidden) {
|
||||
// hidden with escape
|
||||
return this.hide();
|
||||
}
|
||||
if (this.autoHide && this.canBeAutoHidden && !isForced && this.effectiveParserResult == this.parserResult && this.result.length == 1) {
|
||||
// auto hide user setting enabled and somewhere after name part and would usually show command details
|
||||
return this.hide();
|
||||
}
|
||||
if (this.result.length == 0) {
|
||||
if (!isInput) {
|
||||
// no result and no input? hide autocomplete
|
||||
return this.hide();
|
||||
}
|
||||
// otherwise add "no match" notice
|
||||
const option = new BlankAutoCompleteOption(
|
||||
this.name.length ?
|
||||
this.effectiveParserResult.makeNoMatchText()
|
||||
: this.effectiveParserResult.makeNoOptionstext()
|
||||
,
|
||||
);
|
||||
this.result.push(option);
|
||||
} else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) {
|
||||
// only one result that is exactly the current value? just show hint, no autocomplete
|
||||
this.isReplaceable = false;
|
||||
this.isShowingDetails = false;
|
||||
} else if (!this.isReplaceable && this.result.length > 1) {
|
||||
return this.hide();
|
||||
}
|
||||
this.selectedItem = this.result[0];
|
||||
this.isActive = true;
|
||||
this.wasForced = isForced;
|
||||
this.renderDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide autocomplete.
|
||||
*/
|
||||
hide() {
|
||||
this.domWrap?.remove();
|
||||
this.detailsWrap?.remove();
|
||||
this.isActive = false;
|
||||
this.isShowingDetails = false;
|
||||
this.wasForced = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create updated DOM.
|
||||
*/
|
||||
render() {
|
||||
if (!this.isActive) return this.domWrap.remove();
|
||||
if (this.isReplaceable) {
|
||||
this.dom.innerHTML = '';
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const item of this.result) {
|
||||
if (item == this.selectedItem) {
|
||||
item.dom.classList.add('selected');
|
||||
} else {
|
||||
item.dom.classList.remove('selected');
|
||||
}
|
||||
frag.append(item.dom);
|
||||
}
|
||||
this.dom.append(frag);
|
||||
this.updatePosition();
|
||||
document.body.append(this.domWrap);
|
||||
} else {
|
||||
this.domWrap.remove();
|
||||
}
|
||||
this.renderDetailsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create updated DOM for details.
|
||||
*/
|
||||
renderDetails() {
|
||||
if (!this.isActive) return this.detailsWrap.remove();
|
||||
if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
|
||||
this.detailsDom.innerHTML = '';
|
||||
this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
|
||||
document.body.append(this.detailsWrap);
|
||||
this.updateDetailsPositionDebounced();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Update position of DOM.
|
||||
*/
|
||||
updatePosition() {
|
||||
if (this.isFloating) {
|
||||
this.updateFloatingPosition();
|
||||
} else {
|
||||
const rect = {};
|
||||
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
|
||||
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
|
||||
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect();
|
||||
this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
|
||||
this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
|
||||
this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
|
||||
if (this.isShowingDetails) {
|
||||
this.domWrap.style.setProperty('--leftOffset', '1vw');
|
||||
this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`);
|
||||
this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(${rect[power_user.stscript.autocomplete.width.right].right}px, ${this.isShowingDetails ? 74 : 0}vw)`);
|
||||
} else {
|
||||
this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`);
|
||||
this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(99vw, ${rect[power_user.stscript.autocomplete.width.right].right}px)`);
|
||||
}
|
||||
this.updateDetailsPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update position of details DOM.
|
||||
*/
|
||||
updateDetailsPosition() {
|
||||
if (this.isShowingDetails || !this.isReplaceable) {
|
||||
if (this.isFloating) {
|
||||
this.updateFloatingDetailsPosition();
|
||||
} else {
|
||||
const rect = {};
|
||||
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
|
||||
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
|
||||
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect();
|
||||
if (this.isReplaceable) {
|
||||
this.detailsWrap.classList.remove('full');
|
||||
const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
|
||||
this.detailsWrap.style.setProperty('--targetOffset', `${selRect.top}`);
|
||||
this.detailsWrap.style.setProperty('--rightOffset', '1vw');
|
||||
this.detailsWrap.style.setProperty('--bottomOffset', `calc(100vh - ${rect[AUTOCOMPLETE_WIDTH.INPUT].top}px)`);
|
||||
this.detailsWrap.style.setProperty('--leftOffset', `calc(100vw - ${this.domWrap.style.getPropertyValue('--rightOffset')}`);
|
||||
} else {
|
||||
this.detailsWrap.classList.add('full');
|
||||
this.detailsWrap.style.setProperty('--targetOffset', `${rect[AUTOCOMPLETE_WIDTH.INPUT].top}`);
|
||||
this.detailsWrap.style.setProperty('--bottomOffset', `calc(100vh - ${rect[AUTOCOMPLETE_WIDTH.INPUT].top}px)`);
|
||||
this.detailsWrap.style.setProperty('--leftOffset', `${rect[power_user.stscript.autocomplete.width.left].left}px`);
|
||||
this.detailsWrap.style.setProperty('--rightOffset', `calc(100vw - ${rect[power_user.stscript.autocomplete.width.right].right}px)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update position of floating autocomplete.
|
||||
*/
|
||||
updateFloatingPosition() {
|
||||
const location = this.getCursorPosition();
|
||||
const rect = this.textarea.getBoundingClientRect();
|
||||
// cursor is out of view -> hide
|
||||
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
|
||||
return this.hide();
|
||||
}
|
||||
const left = Math.max(rect.left, location.left);
|
||||
this.domWrap.style.setProperty('--targetOffset', `${left}`);
|
||||
if (location.top <= window.innerHeight / 2) {
|
||||
// if cursor is in lower half of window, show list above line
|
||||
this.domWrap.style.top = `${location.bottom}px`;
|
||||
this.domWrap.style.bottom = 'auto';
|
||||
this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
|
||||
} else {
|
||||
// if cursor is in upper half of window, show list below line
|
||||
this.domWrap.style.top = 'auto';
|
||||
this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`;
|
||||
this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;
|
||||
}
|
||||
}
|
||||
|
||||
updateFloatingDetailsPosition(location = null) {
|
||||
if (!location) location = this.getCursorPosition();
|
||||
const rect = this.textarea.getBoundingClientRect();
|
||||
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
|
||||
return this.hide();
|
||||
}
|
||||
const left = Math.max(rect.left, location.left);
|
||||
this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
|
||||
if (this.isReplaceable) {
|
||||
this.detailsWrap.classList.remove('full');
|
||||
if (left < window.innerWidth / 4) {
|
||||
// if cursor is in left part of screen, show details on right of list
|
||||
this.detailsWrap.classList.add('right');
|
||||
this.detailsWrap.classList.remove('left');
|
||||
} else {
|
||||
// if cursor is in right part of screen, show details on left of list
|
||||
this.detailsWrap.classList.remove('right');
|
||||
this.detailsWrap.classList.add('left');
|
||||
}
|
||||
} else {
|
||||
this.detailsWrap.classList.remove('left');
|
||||
this.detailsWrap.classList.remove('right');
|
||||
this.detailsWrap.classList.add('full');
|
||||
}
|
||||
if (location.top <= window.innerHeight / 2) {
|
||||
// if cursor is in lower half of window, show list above line
|
||||
this.detailsWrap.style.top = `${location.bottom}px`;
|
||||
this.detailsWrap.style.bottom = 'auto';
|
||||
this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
|
||||
} else {
|
||||
// if cursor is in upper half of window, show list below line
|
||||
this.detailsWrap.style.top = 'auto';
|
||||
this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`;
|
||||
this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate (keyboard) cursor coordinates within textarea.
|
||||
* @returns {{left:number, top:number, bottom:number}}
|
||||
*/
|
||||
getCursorPosition() {
|
||||
const inputRect = this.textarea.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(this.textarea);
|
||||
if (!this.clone) {
|
||||
this.clone = document.createElement('div');
|
||||
for (const key of style) {
|
||||
this.clone.style[key] = style[key];
|
||||
}
|
||||
this.clone.style.position = 'fixed';
|
||||
this.clone.style.visibility = 'hidden';
|
||||
document.body.append(this.clone);
|
||||
const mo = new MutationObserver(muts=>{
|
||||
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
|
||||
this.clone.remove();
|
||||
}
|
||||
});
|
||||
mo.observe(this.textarea.parentElement, { childList:true });
|
||||
}
|
||||
this.clone.style.height = `${inputRect.height}px`;
|
||||
this.clone.style.left = `${inputRect.left}px`;
|
||||
this.clone.style.top = `${inputRect.top}px`;
|
||||
this.clone.style.whiteSpace = style.whiteSpace;
|
||||
this.clone.style.tabSize = style.tabSize;
|
||||
const text = this.textarea.value;
|
||||
const before = text.slice(0, this.textarea.selectionStart);
|
||||
this.clone.textContent = before;
|
||||
const locator = document.createElement('span');
|
||||
locator.textContent = text[this.textarea.selectionStart];
|
||||
this.clone.append(locator);
|
||||
this.clone.append(text.slice(this.textarea.selectionStart + 1));
|
||||
this.clone.scrollTop = this.textarea.scrollTop;
|
||||
this.clone.scrollLeft = this.textarea.scrollLeft;
|
||||
const locatorRect = locator.getBoundingClientRect();
|
||||
const location = {
|
||||
left: locatorRect.left,
|
||||
top: locatorRect.top,
|
||||
bottom: locatorRect.bottom,
|
||||
};
|
||||
return location;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggle details view alongside autocomplete list.
|
||||
*/
|
||||
toggleDetails() {
|
||||
this.isShowingDetails = !this.isShowingDetails;
|
||||
this.renderDetailsDebounced();
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Select an item for autocomplete and put text into textarea.
|
||||
*/
|
||||
async select() {
|
||||
if (this.isReplaceable && this.selectedItem.value !== null) {
|
||||
this.textarea.value = `${this.text.slice(0, this.effectiveParserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;
|
||||
this.textarea.selectionStart = this.effectiveParserResult.start + this.selectedItem.replacer.length;
|
||||
this.textarea.selectionEnd = this.textarea.selectionStart;
|
||||
this.show(false, false, true);
|
||||
} else {
|
||||
const selectionStart = this.textarea.selectionStart;
|
||||
const selectionEnd = this.textarea.selectionDirection;
|
||||
this.textarea.selectionStart = selectionStart;
|
||||
this.textarea.selectionDirection = selectionEnd;
|
||||
}
|
||||
this.wasForced = false;
|
||||
this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Mark the item at newIdx in the autocomplete list as selected.
|
||||
* @param {number} newIdx
|
||||
*/
|
||||
selectItemAtIndex(newIdx) {
|
||||
this.selectedItem.dom.classList.remove('selected');
|
||||
this.selectedItem = this.result[newIdx];
|
||||
this.selectedItem.dom.classList.add('selected');
|
||||
const rect = this.selectedItem.dom.children[0].getBoundingClientRect();
|
||||
const rectParent = this.dom.getBoundingClientRect();
|
||||
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
||||
this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;
|
||||
}
|
||||
this.renderDetailsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard events.
|
||||
* @param {KeyboardEvent} evt The event.
|
||||
*/
|
||||
async handleKeyDown(evt) {
|
||||
// autocomplete is shown and cursor at end of current command name (or inside name and typed or forced)
|
||||
if (this.isActive && this.isReplaceable) {
|
||||
// actions in the list
|
||||
switch (evt.key) {
|
||||
case 'ArrowUp': {
|
||||
// select previous item
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const idx = this.result.indexOf(this.selectedItem);
|
||||
let newIdx;
|
||||
if (idx == 0) newIdx = this.result.length - 1;
|
||||
else newIdx = idx - 1;
|
||||
this.selectItemAtIndex(newIdx);
|
||||
return;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
// select next item
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const idx = this.result.indexOf(this.selectedItem);
|
||||
const newIdx = (idx + 1) % this.result.length;
|
||||
this.selectItemAtIndex(newIdx);
|
||||
return;
|
||||
}
|
||||
case 'Enter': {
|
||||
// pick the selected item to autocomplete
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
|
||||
if (this.selectedItem.name == this.name) break;
|
||||
evt.preventDefault();
|
||||
evt.stopImmediatePropagation();
|
||||
this.select();
|
||||
return;
|
||||
}
|
||||
case 'Tab': {
|
||||
// pick the selected item to autocomplete
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
|
||||
evt.preventDefault();
|
||||
evt.stopImmediatePropagation();
|
||||
this.select();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// details are shown, cursor can be anywhere
|
||||
if (this.isActive) {
|
||||
switch (evt.key) {
|
||||
case 'Escape': {
|
||||
// close autocomplete
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.isForceHidden = true;
|
||||
this.wasForced = false;
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
case 'Enter': {
|
||||
// hide autocomplete on enter (send, execute, ...)
|
||||
if (!evt.shiftKey) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// autocomplete shown or not, cursor anywhere
|
||||
switch (evt.key) {
|
||||
case ' ': {
|
||||
if (evt.ctrlKey) {
|
||||
if (this.isActive && this.isReplaceable) {
|
||||
// ctrl-space to toggle details for selected item
|
||||
this.toggleDetails();
|
||||
} else {
|
||||
// ctrl-space to force show autocomplete
|
||||
this.show(false, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (['Control', 'Shift', 'Alt'].includes(evt.key)) {
|
||||
// ignore keydown on modifier keys
|
||||
return;
|
||||
}
|
||||
switch (evt.key) {
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
case 'ArrowLeft': {
|
||||
if (this.isActive) {
|
||||
// keyboard navigation, wait for keyup to complete cursor move
|
||||
const oldText = this.textarea.value;
|
||||
await new Promise(resolve=>{
|
||||
window.addEventListener('keyup', resolve, { once:true });
|
||||
});
|
||||
if (this.selectionStart != this.textarea.selectionStart) {
|
||||
this.selectionStart = this.textarea.selectionStart;
|
||||
this.show(this.isReplaceable || oldText != this.textarea.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (this.isActive) {
|
||||
this.text != this.textarea.value && this.show(this.isReplaceable);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
public/scripts/autocomplete/AutoCompleteFuzzyScore.js
Normal file
16
public/scripts/autocomplete/AutoCompleteFuzzyScore.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
|
||||
export class AutoCompleteFuzzyScore {
|
||||
/**@type {number}*/ start;
|
||||
/**@type {number}*/ longestConsecutive;
|
||||
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} longestConsecutive
|
||||
*/
|
||||
constructor(start, longestConsecutive) {
|
||||
this.start = start;
|
||||
this.longestConsecutive = longestConsecutive;
|
||||
}
|
||||
}
|
44
public/scripts/autocomplete/AutoCompleteNameResult.js
Normal file
44
public/scripts/autocomplete/AutoCompleteNameResult.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
|
||||
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||
// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
|
||||
|
||||
|
||||
|
||||
export class AutoCompleteNameResult {
|
||||
/**@type {string} */ name;
|
||||
/**@type {number} */ start;
|
||||
/**@type {AutoCompleteOption[]} */ optionList = [];
|
||||
/**@type {boolean} */ canBeQuoted = false;
|
||||
/**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
|
||||
/**@type {()=>string} */ makeNoOptionstext = ()=>'No options';
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} name Name (potentially partial) of the name at the requested index.
|
||||
* @param {number} start Index where the name starts.
|
||||
* @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
|
||||
* @param {boolean} canBeQuoted Whether the name can be inside quotes.
|
||||
* @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
|
||||
* @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
|
||||
*/
|
||||
constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
|
||||
this.name = name;
|
||||
this.start = start;
|
||||
this.optionList = optionList;
|
||||
this.canBeQuoted = canBeQuoted;
|
||||
this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
|
||||
this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionstext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text The whole text
|
||||
* @param {number} index Cursor index within text
|
||||
* @param {boolean} isSelect Whether autocomplete was triggered by selecting an autocomplete option
|
||||
* @returns {AutoCompleteSecondaryNameResult}
|
||||
*/
|
||||
getSecondaryNameAt(text, index, isSelect) {
|
||||
return null;
|
||||
}
|
||||
}
|
206
public/scripts/autocomplete/AutoCompleteOption.js
Normal file
206
public/scripts/autocomplete/AutoCompleteOption.js
Normal file
@@ -0,0 +1,206 @@
|
||||
import { SlashCommand } from '../slash-commands/SlashCommand.js';
|
||||
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
|
||||
|
||||
|
||||
|
||||
export class AutoCompleteOption {
|
||||
/**@type {string}*/ name;
|
||||
/**@type {string}*/ typeIcon;
|
||||
/**@type {number}*/ nameOffset = 0;
|
||||
/**@type {AutoCompleteFuzzyScore}*/ score;
|
||||
/**@type {string}*/ replacer;
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
|
||||
|
||||
/**
|
||||
* Used as a comparison value when removing duplicates (e.g., when a SlashCommand has aliases).
|
||||
* @type {any}
|
||||
* */
|
||||
get value() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name, typeIcon = ' ') {
|
||||
this.name = name;
|
||||
this.typeIcon = typeIcon;
|
||||
}
|
||||
|
||||
|
||||
makeItem(key, typeIcon, noSlash, namedArguments = [], unnamedArguments = [], returnType = 'void', helpString = '', aliasList = []) {
|
||||
const li = document.createElement('li'); {
|
||||
li.classList.add('item');
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('type');
|
||||
type.classList.add('monospace');
|
||||
type.textContent = typeIcon;
|
||||
li.append(type);
|
||||
}
|
||||
const specs = document.createElement('span'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('span'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = noSlash ? '' : '/';
|
||||
key.split('').forEach(char=>{
|
||||
const span = document.createElement('span'); {
|
||||
span.textContent = char;
|
||||
name.append(span);
|
||||
}
|
||||
});
|
||||
specs.append(name);
|
||||
}
|
||||
const body = document.createElement('span'); {
|
||||
body.classList.add('body');
|
||||
const args = document.createElement('span'); {
|
||||
args.classList.add('arguments');
|
||||
for (const arg of namedArguments) {
|
||||
const argItem = document.createElement('span'); {
|
||||
argItem.classList.add('argument');
|
||||
argItem.classList.add('namedArgument');
|
||||
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||
const name = document.createElement('span'); {
|
||||
name.classList.add('argument-name');
|
||||
name.textContent = arg.name;
|
||||
argItem.append(name);
|
||||
}
|
||||
if (arg.enumList.length > 0) {
|
||||
const enums = document.createElement('span'); {
|
||||
enums.classList.add('argument-enums');
|
||||
for (const e of arg.enumList) {
|
||||
const enumItem = document.createElement('span'); {
|
||||
enumItem.classList.add('argument-enum');
|
||||
enumItem.textContent = e;
|
||||
enums.append(enumItem);
|
||||
}
|
||||
}
|
||||
argItem.append(enums);
|
||||
}
|
||||
} else {
|
||||
const types = document.createElement('span'); {
|
||||
types.classList.add('argument-types');
|
||||
for (const t of arg.typeList) {
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('argument-type');
|
||||
type.textContent = t;
|
||||
types.append(type);
|
||||
}
|
||||
}
|
||||
argItem.append(types);
|
||||
}
|
||||
}
|
||||
args.append(argItem);
|
||||
}
|
||||
}
|
||||
for (const arg of unnamedArguments) {
|
||||
const argItem = document.createElement('span'); {
|
||||
argItem.classList.add('argument');
|
||||
argItem.classList.add('unnamedArgument');
|
||||
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||
if (arg.enumList.length > 0) {
|
||||
const enums = document.createElement('span'); {
|
||||
enums.classList.add('argument-enums');
|
||||
for (const e of arg.enumList) {
|
||||
const enumItem = document.createElement('span'); {
|
||||
enumItem.classList.add('argument-enum');
|
||||
enumItem.textContent = e;
|
||||
enums.append(enumItem);
|
||||
}
|
||||
}
|
||||
argItem.append(enums);
|
||||
}
|
||||
} else {
|
||||
const types = document.createElement('span'); {
|
||||
types.classList.add('argument-types');
|
||||
for (const t of arg.typeList) {
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('argument-type');
|
||||
type.textContent = t;
|
||||
types.append(type);
|
||||
}
|
||||
}
|
||||
argItem.append(types);
|
||||
}
|
||||
}
|
||||
args.append(argItem);
|
||||
}
|
||||
}
|
||||
body.append(args);
|
||||
}
|
||||
const returns = document.createElement('span'); {
|
||||
returns.classList.add('returns');
|
||||
returns.textContent = returnType ?? 'void';
|
||||
// body.append(returns);
|
||||
}
|
||||
specs.append(body);
|
||||
}
|
||||
li.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
const content = document.createElement('span'); {
|
||||
content.classList.add('helpContent');
|
||||
content.innerHTML = helpString;
|
||||
const text = content.textContent;
|
||||
content.innerHTML = '';
|
||||
content.textContent = text;
|
||||
help.append(content);
|
||||
}
|
||||
li.append(help);
|
||||
}
|
||||
if (aliasList.length > 0) {
|
||||
const aliases = document.createElement('span'); {
|
||||
aliases.classList.add('aliases');
|
||||
aliases.append(' (alias: ');
|
||||
for (const aliasName of aliasList) {
|
||||
const alias = document.createElement('span'); {
|
||||
alias.classList.add('monospace');
|
||||
alias.textContent = `/${aliasName}`;
|
||||
aliases.append(alias);
|
||||
}
|
||||
}
|
||||
aliases.append(')');
|
||||
// li.append(aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
renderItem() {
|
||||
// throw new Error(`${this.constructor.name}.renderItem() is not implemented`);
|
||||
let li;
|
||||
li = this.makeItem(this.name, this.typeIcon, true);
|
||||
li.setAttribute('data-name', this.name);
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
renderDetails() {
|
||||
// throw new Error(`${this.constructor.name}.renderDetails() is not implemented`);
|
||||
const frag = document.createDocumentFragment();
|
||||
const specs = document.createElement('div'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('div'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = this.name;
|
||||
specs.append(name);
|
||||
}
|
||||
frag.append(specs);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
|
||||
|
||||
export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {
|
||||
/**@type {boolean}*/ isRequired = false;
|
||||
}
|
29
public/scripts/autocomplete/BlankAutoCompleteOption.js
Normal file
29
public/scripts/autocomplete/BlankAutoCompleteOption.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||
|
||||
export class BlankAutoCompleteOption extends AutoCompleteOption {
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
super(name);
|
||||
this.dom = this.renderItem();
|
||||
}
|
||||
|
||||
get value() { return null; }
|
||||
|
||||
|
||||
renderItem() {
|
||||
const li = document.createElement('li'); {
|
||||
li.classList.add('item');
|
||||
li.classList.add('blank');
|
||||
li.textContent = this.name;
|
||||
}
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
renderDetails() {
|
||||
const frag = document.createDocumentFragment();
|
||||
return frag;
|
||||
}
|
||||
}
|
44
public/scripts/autocomplete/MacroAutoCompleteOption.js
Normal file
44
public/scripts/autocomplete/MacroAutoCompleteOption.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||
|
||||
export class MacroAutoCompleteOption extends AutoCompleteOption {
|
||||
/**@type {string}*/ fullName;
|
||||
/**@type {string}*/ description;
|
||||
|
||||
|
||||
constructor(name, fullName, description) {
|
||||
super(name, '{}');
|
||||
this.fullName = fullName;
|
||||
this.description = description;
|
||||
this.nameOffset = 2;
|
||||
}
|
||||
|
||||
|
||||
renderItem() {
|
||||
let li;
|
||||
li = this.makeItem(`${this.fullName}`, '{}', true, [], [], null, this.description);
|
||||
li.setAttribute('data-name', this.name);
|
||||
li.setAttribute('data-option-type', 'macro');
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
renderDetails() {
|
||||
const frag = document.createDocumentFragment();
|
||||
const specs = document.createElement('div'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('div'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = this.fullName;
|
||||
specs.append(name);
|
||||
}
|
||||
frag.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
help.innerHTML = this.description;
|
||||
frag.append(help);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
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 { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { flashHighlight, stringFormat } from './utils.js';
|
||||
|
||||
const BG_METADATA_KEY = 'custom_background';
|
||||
const LIST_METADATA_KEY = 'chat_backgrounds';
|
||||
@@ -453,8 +454,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() {
|
||||
@@ -481,7 +481,20 @@ export function initBackgrounds() {
|
||||
$('#auto_background').on('click', autoBackgroundCommand);
|
||||
$('#add_bg_button').on('change', onBackgroundUploadSelected);
|
||||
$('#bg-filter').on('input', onBackgroundFilterInput);
|
||||
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], '– locks a background for the currently selected chat', true, true);
|
||||
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], '– unlocks a background for the currently selected chat', true, true);
|
||||
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], '– automatically changes the background based on the chat context using the AI request prompt', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
|
||||
callback: onLockBackgroundClick,
|
||||
aliases: ['bglock'],
|
||||
helpString: 'Locks a background for the currently selected chat',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
|
||||
callback: onUnlockBackgroundClick,
|
||||
aliases: ['bgunlock'],
|
||||
helpString: 'Unlocks a background for the currently selected chat',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'autobg',
|
||||
callback: autoBackgroundCommand,
|
||||
aliases: ['bgauto'],
|
||||
helpString: 'Automatically changes the background based on the chat context using the AI request prompt',
|
||||
}));
|
||||
|
||||
}
|
||||
|
98
public/scripts/char-data.js
Normal file
98
public/scripts/char-data.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @typedef {object} v2DataWorldInfoEntry
|
||||
* @property {string[]} keys - An array of primary keys associated with the entry.
|
||||
* @property {string[]} secondary_keys - An array of secondary keys associated with the entry (optional).
|
||||
* @property {string} comment - A human-readable description or explanation for the entry.
|
||||
* @property {string} content - The main content or data associated with the entry.
|
||||
* @property {boolean} constant - Indicates if the entry's content is fixed and unchangeable.
|
||||
* @property {boolean} selective - Indicates if the entry's inclusion is controlled by specific conditions.
|
||||
* @property {number} insertion_order - Defines the order in which the entry is inserted during processing.
|
||||
* @property {boolean} enabled - Controls whether the entry is currently active and used.
|
||||
* @property {string} position - Specifies the location or context where the entry applies.
|
||||
* @property {v2DataWorldInfoEntryExtensionInfos} extensions - An object containing additional details for extensions associated with the entry.
|
||||
* @property {number} id - A unique identifier assigned to the entry.
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} v2DataWorldInfoEntryExtensionInfos
|
||||
* @property {number} position - The order in which the extension is applied relative to other extensions.
|
||||
* @property {boolean} exclude_recursion - Prevents the extension from being applied recursively.
|
||||
* @property {number} probability - The chance (between 0 and 1) of the extension being applied.
|
||||
* @property {boolean} useProbability - Determines if the `probability` property is used.
|
||||
* @property {number} depth - The maximum level of nesting allowed for recursive application of the extension.
|
||||
* @property {number} selectiveLogic - Defines the logic used to determine if the extension is applied selectively.
|
||||
* @property {string} group - A category or grouping for the extension.
|
||||
* @property {boolean} group_override - Overrides any existing group assignment for the extension.
|
||||
* @property {number} group_weight - A value used for prioritizing extensions within the same group.
|
||||
* @property {boolean} prevent_recursion - Completely disallows recursive application of the extension.
|
||||
* @property {boolean} delay_until_recursion - Will only be checked during recursion.
|
||||
* @property {number} scan_depth - The maximum depth to search for matches when applying the extension.
|
||||
* @property {boolean} match_whole_words - Specifies if only entire words should be matched during extension application.
|
||||
* @property {boolean} use_group_scoring - Indicates if group weight is considered when selecting extensions.
|
||||
* @property {boolean} case_sensitive - Controls whether case sensitivity is applied during matching for the extension.
|
||||
* @property {string} automation_id - An identifier used for automation purposes related to the extension.
|
||||
* @property {number} role - The specific function or purpose of the extension.
|
||||
* @property {boolean} vectorized - Indicates if the extension is optimized for vectorized processing.
|
||||
* @property {number} display_index - The order in which the extension should be displayed for user interfaces.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} v2WorldInfoBook
|
||||
* @property {string} name - the name of the book
|
||||
* @property {v2DataWorldInfoEntry[]} entries - the entries of the book
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} v2CharData
|
||||
* @property {string} name - The character's name.
|
||||
* @property {string} description - A brief description of the character.
|
||||
* @property {string} character_version - The character's data version.
|
||||
* @property {string} personality - A short summary of the character's personality traits.
|
||||
* @property {string} scenario - A description of the character's background or setting.
|
||||
* @property {string} first_mes - The character's opening message in a conversation.
|
||||
* @property {string} mes_example - An example message demonstrating the character's conversation style.
|
||||
* @property {string} creator_notes - Internal notes or comments left by the character's creator.
|
||||
* @property {string[]} tags - A list of keywords or labels associated with the character.
|
||||
* @property {string} system_prompt - The system prompt used to interact with the character.
|
||||
* @property {string} post_history_instructions - Instructions for handling the character's conversation history.
|
||||
* @property {string} creator - The name of the person who created the character.
|
||||
* @property {string[]} alternate_greetings - Additional greeting messages the character can use.
|
||||
* @property {v2WorldInfoBook} character_book - Data about the character's world or story (if applicable).
|
||||
* @property {v2CharDataExtensionInfos} extensions - Additional details specific to the character.
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} v2CharDataExtensionInfos
|
||||
* @property {number} talkativeness - A numerical value indicating the character's propensity to talk.
|
||||
* @property {boolean} fav - A flag indicating whether the character is a favorite.
|
||||
* @property {string} world - The fictional world or setting where the character exists (if applicable).
|
||||
* @property {object} depth_prompt - Prompts used to explore the character's depth and complexity.
|
||||
* @property {number} depth_prompt.depth - The level of detail or nuance targeted by the prompt.
|
||||
* @property {string} depth_prompt.prompt - The actual prompt text used for deeper character interaction.
|
||||
* @property {"system" | "user" | "assistant"} depth_prompt.role - The role the character takes on during the prompted interaction (system, user, or assistant).
|
||||
* // Non-standard extensions added by external tools
|
||||
* @property {string} [pygmalion_id] - The unique identifier assigned to the character by the Pygmalion.chat.
|
||||
* @property {string} [github_repo] - The gitHub repository associated with the character.
|
||||
* @property {string} [source_url] - The source URL associated with the character.
|
||||
* @property {{full_path: string}} [chub] - The Chub-specific data associated with the character.
|
||||
* @property {{source: string[]}} [risuai] - The RisuAI-specific data associated with the character.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} v1CharData
|
||||
* @property {string} name - the name of the character
|
||||
* @property {string} description - the description of the character
|
||||
* @property {string} personality - a short personality description of the character
|
||||
* @property {string} scenario - a scenario description of the character
|
||||
* @property {string} first_mes - the first message in the conversation
|
||||
* @property {string} mes_example - the example message in the conversation
|
||||
* @property {string} creatorcomment - creator's notes of the character
|
||||
* @property {string[]} tags - the tags of the character
|
||||
* @property {number} talkativeness - talkativeness
|
||||
* @property {boolean|string} fav - fav
|
||||
* @property {string} create_date - create_date
|
||||
* @property {v2CharData} data - v2 data extension
|
||||
* // Non-standard extensions added by the ST server (not part of the original data)
|
||||
* @property {string} chat - name of the current chat file chat
|
||||
* @property {string} avatar - file name of the avatar image (acts as a unique identifier)
|
||||
* @property {string} json_data - the full raw JSON data of the character
|
||||
*/
|
||||
export default 0;// now this file is a module
|
@@ -53,11 +53,11 @@ import { ScraperManager } from './scrapers.js';
|
||||
* @returns {Promise<string>} Converted file text
|
||||
*/
|
||||
|
||||
const fileSizeLimit = 1024 * 1024 * 10; // 10 MB
|
||||
const fileSizeLimit = 1024 * 1024 * 100; // 100 MB
|
||||
const ATTACHMENT_SOURCE = {
|
||||
GLOBAL: 'global',
|
||||
CHAT: 'chat',
|
||||
CHARACTER: 'character',
|
||||
CHAT: 'chat',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -592,9 +592,10 @@ async function deleteMessageImage() {
|
||||
/**
|
||||
* 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) {
|
||||
async function deleteFileFromServer(url, silent = false) {
|
||||
try {
|
||||
const result = await fetch('/api/files/delete', {
|
||||
method: 'POST',
|
||||
@@ -602,7 +603,7 @@ async function deleteFileFromServer(url) {
|
||||
body: JSON.stringify({ path: url }),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
if (!result.ok && !silent) {
|
||||
const error = await result.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
@@ -669,6 +670,79 @@ async function editAttachment(attachment, source, callback) {
|
||||
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
|
||||
@@ -702,10 +776,25 @@ async function deleteAttachment(attachment, source, callback, confirm = true) {
|
||||
break;
|
||||
}
|
||||
|
||||
await deleteFileFromServer(attachment.url);
|
||||
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.
|
||||
*/
|
||||
@@ -755,13 +844,20 @@ async function openAttachmentManager() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -786,7 +882,13 @@ async function openAttachmentManager() {
|
||||
}
|
||||
|
||||
const buttonTemplate = template.find('.actionButtonTemplate .actionButton').clone();
|
||||
buttonTemplate.find('.actionButtonIcon').addClass(scraper.iconClass);
|
||||
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', () => {
|
||||
@@ -860,6 +962,50 @@ async function openAttachmentManager() {
|
||||
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 = '';
|
||||
@@ -883,10 +1029,34 @@ async function openAttachmentManager() {
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -989,6 +1159,10 @@ export async function uploadFileAttachmentToServer(file, target) {
|
||||
}
|
||||
|
||||
function ensureAttachmentsExist() {
|
||||
if (!Array.isArray(extension_settings.disabled_attachments)) {
|
||||
extension_settings.disabled_attachments = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(extension_settings.attachments)) {
|
||||
extension_settings.attachments = [];
|
||||
}
|
||||
@@ -1009,7 +1183,7 @@ function ensureAttachmentsExist() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all currently available attachments.
|
||||
* Gets all currently available attachments. Ignores disabled attachments.
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachments() {
|
||||
@@ -1018,11 +1192,11 @@ export function getDataBankAttachments() {
|
||||
const chatAttachments = chat_metadata.attachments ?? [];
|
||||
const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
|
||||
return [...globalAttachments, ...chatAttachments, ...characterAttachments];
|
||||
return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => !isAttachmentDisabled(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all attachments for a specific source.
|
||||
* Gets all attachments for a specific source. Includes disabled attachments.
|
||||
* @param {string} source Attachment source
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
@@ -1037,6 +1211,50 @@ export function getDataBankAttachmentsForSource(source) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1116,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');
|
||||
});
|
||||
|
14
public/scripts/constants.js
Normal file
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,8 +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/assets/character.html
Normal file
9
public/scripts/extensions/assets/character.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="characterAsset">
|
||||
<div class="characterAssetName">{{name}}</div>
|
||||
<img class="characterAssetImage" alt="{{name}}" src="{{url}}" />
|
||||
<div class="characterAssetDescription" title="{{description}}">{{description}}</div>
|
||||
<div class="characterAssetButtons flex-container">
|
||||
<div class="characterAssetDownloadButton right_menu_button fa-fw fa-solid fa-download" title="Download"></div>
|
||||
<div class="characterAssetCheckMark right_menu_button fa-fw fa-solid fa-check" title="Installed"></div>
|
||||
</div>
|
||||
</div>
|
@@ -3,8 +3,9 @@ TODO:
|
||||
*/
|
||||
//const DEBUG_TONY_SAMA_FORK_MODE = true
|
||||
|
||||
import { getRequestHeaders, callPopup, processDroppedFiles } from '../../../script.js';
|
||||
import { getRequestHeaders, callPopup, processDroppedFiles, eventSource, event_types } from '../../../script.js';
|
||||
import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { executeSlashCommands } from '../../slash-commands.js';
|
||||
import { getStringHash, isValidUrl } from '../../utils.js';
|
||||
export { MODULE_NAME };
|
||||
@@ -108,7 +109,7 @@ function downloadAssetsList(url) {
|
||||
</div>`);
|
||||
}
|
||||
|
||||
for (const i in availableAssets[assetType]) {
|
||||
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
|
||||
const asset = availableAssets[assetType][i];
|
||||
const elemId = `assets_install_${assetType}_${i}`;
|
||||
let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
|
||||
@@ -199,6 +200,9 @@ function downloadAssetsList(url) {
|
||||
</div>`);
|
||||
|
||||
if (assetType === 'character') {
|
||||
if (asset.highlight) {
|
||||
assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
|
||||
}
|
||||
assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset['url']}" alt="${displayName}"></div>`);
|
||||
}
|
||||
|
||||
@@ -328,6 +332,41 @@ async function deleteAsset(assetType, filename) {
|
||||
}
|
||||
}
|
||||
|
||||
async function openCharacterBrowser(forceDefault) {
|
||||
const url = forceDefault ? ASSETS_JSON_URL : String($('#assets-json-url-field').val());
|
||||
const fetchResult = await fetch(url, { cache: 'no-cache' });
|
||||
const json = await fetchResult.json();
|
||||
const characters = json.filter(x => x.type === 'character');
|
||||
|
||||
if (!characters.length) {
|
||||
toastr.error('No characters found in the assets list', 'Character browser');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'market', {}));
|
||||
|
||||
for (const character of characters.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const listElement = template.find(character.highlight ? '.contestWinnersList' : '.featuredCharactersList');
|
||||
const characterElement = $(await renderExtensionTemplateAsync(MODULE_NAME, 'character', character));
|
||||
const downloadButton = characterElement.find('.characterAssetDownloadButton');
|
||||
const checkMark = characterElement.find('.characterAssetCheckMark');
|
||||
const isInstalled = isAssetInstalled('character', character.id);
|
||||
|
||||
downloadButton.toggle(!isInstalled).on('click', async () => {
|
||||
downloadButton.toggleClass('fa-download fa-spinner fa-spin');
|
||||
await installAsset(character.url, 'character', character.id);
|
||||
downloadButton.hide();
|
||||
checkMark.show();
|
||||
});
|
||||
|
||||
checkMark.toggle(isInstalled);
|
||||
|
||||
listElement.append(characterElement);
|
||||
}
|
||||
|
||||
callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, allowVerticalScrolling: true, allowHorizontalScrolling: false });
|
||||
}
|
||||
|
||||
//#############################//
|
||||
// API Calls //
|
||||
//#############################//
|
||||
@@ -361,6 +400,11 @@ jQuery(async () => {
|
||||
const assetsJsonUrl = windowHtml.find('#assets-json-url-field');
|
||||
assetsJsonUrl.val(ASSETS_JSON_URL);
|
||||
|
||||
const charactersButton = windowHtml.find('#assets-characters-button');
|
||||
charactersButton.on('click', async function () {
|
||||
openCharacterBrowser(false);
|
||||
});
|
||||
|
||||
const connectButton = windowHtml.find('#assets-connect-button');
|
||||
connectButton.on('click', async function () {
|
||||
const url = String(assetsJsonUrl.val());
|
||||
@@ -397,4 +441,8 @@ jQuery(async () => {
|
||||
|
||||
windowHtml.find('#assets_filters').hide();
|
||||
$('#extensions_settings').append(windowHtml);
|
||||
|
||||
eventSource.on(event_types.OPEN_CHARACTER_LIBRARY, async (forceDefault) => {
|
||||
openCharacterBrowser(forceDefault);
|
||||
});
|
||||
});
|
||||
|
19
public/scripts/extensions/assets/market.html
Normal file
19
public/scripts/extensions/assets/market.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="flex-container flexFlowColumn padding5">
|
||||
<div class="contestWinners flex-container flexFlowColumn">
|
||||
<h3 class="flex-container alignItemsBaseline justifyCenter" title="These characters are the winners of character design contests and have outstandable quality.">
|
||||
<span data-i18n="Contest Winners">Contest Winners</span>
|
||||
<i class="fa-solid fa-star"></i>
|
||||
</h3>
|
||||
<div class="contestWinnersList characterAssetList">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="featuredCharacters flex-container flexFlowColumn">
|
||||
<h3 class="flex-container alignItemsBaseline justifyCenter" title="These characters are the finalists of character design contests and have remarkable quality.">
|
||||
<span data-i18n="Featured Characters">Featured Characters</span>
|
||||
<i class="fa-solid fa-thumbs-up"></i>
|
||||
</h3>
|
||||
<div class="featuredCharactersList characterAssetList">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -105,3 +105,54 @@
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
.characterAssetList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.characterAsset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
background-color: var(--black30a);
|
||||
border-radius: 10px;
|
||||
width: 17%;
|
||||
min-width: 150px;
|
||||
margin: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.characterAssetName {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.characterAssetImage {
|
||||
max-height: 140px;
|
||||
object-fit: scale-down;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.characterAssetDescription {
|
||||
font-size: 0.75em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.characterAssetButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -14,6 +14,10 @@
|
||||
<select id="assets_type_select" class="text_pole flex1">
|
||||
</select>
|
||||
<input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">
|
||||
<div id="assets-characters-button" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-image-portrait"></i>
|
||||
Characters
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-drawer-content" id="assets_menu">
|
||||
</div>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
Enter a URL or the ID of a Fandom wiki page to scrape:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://harrypotter.fandom.com/</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>harrypotter</code>
|
||||
|
8
public/scripts/extensions/attachments/files-dropped.html
Normal file
8
public/scripts/extensions/attachments/files-dropped.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="flex-container justifyCenter alignItemsBaseline">
|
||||
<span>Save <span class="droppedFilesCount">{{count}}</span> file(s) to...</span>
|
||||
<select class="droppedFilesTarget">
|
||||
{{#each targets}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
@@ -1,6 +1,15 @@
|
||||
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
|
||||
jQuery(async () => {
|
||||
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
||||
$('#extensionsMenu').prepend(buttons);
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db',
|
||||
callback: () => document.getElementById('manageAttachments')?.click(),
|
||||
aliases: ['databank', 'data-bank'],
|
||||
helpString: 'Open the data bank',
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -7,8 +7,13 @@
|
||||
<div data-i18n="These files will be available for extensions that support attachments (e.g. Vector Storage).">
|
||||
These files will be available for extensions that support attachments (e.g. Vector Storage).
|
||||
</div>
|
||||
<div data-i18n="Supported file types: Plain Text, PDF, Markdown, HTML, EPUB." class="marginTopBot5">
|
||||
Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.
|
||||
<div class="marginTopBot5">
|
||||
<span data-i18n="Supported file types: Plain Text, PDF, Markdown, HTML, EPUB." >
|
||||
Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.
|
||||
</span>
|
||||
<span data-i18n="Drag and drop files here to upload.">
|
||||
Drag and drop files here to upload.
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-container marginTopBot5">
|
||||
<input type="search" id="attachmentSearch" class="attachmentSearch text_pole margin0 flex1" placeholder="Search...">
|
||||
@@ -101,15 +106,20 @@
|
||||
<div class="attachmentListItemName flex1"></div>
|
||||
<small class="attachmentListItemCreated"></small>
|
||||
<small class="attachmentListItemSize"></small>
|
||||
<div class="viewAttachmentButton right_menu_button fa-solid fa-magnifying-glass" title="View attachment content"></div>
|
||||
<div class="editAttachmentButton right_menu_button fa-solid fa-pencil" title="Edit attachment"></div>
|
||||
<div class="deleteAttachmentButton right_menu_button fa-solid fa-trash" title="Delete attachment"></div>
|
||||
<div class="viewAttachmentButton right_menu_button fa-fw fa-solid fa-magnifying-glass" title="View attachment content"></div>
|
||||
<div class="disableAttachmentButton right_menu_button fa-fw fa-solid fa-comment" title="Disable attachment"></div>
|
||||
<div class="enableAttachmentButton right_menu_button fa-fw fa-solid fa-comment-slash" title="Enable attachment"></div>
|
||||
<div class="moveAttachmentButton right_menu_button fa-fw fa-solid fa-arrows-alt" title="Move attachment"></div>
|
||||
<div class="editAttachmentButton right_menu_button fa-fw fa-solid fa-pencil" title="Edit attachment"></div>
|
||||
<div class="downloadAttachmentButton right_menu_button fa-fw fa-solid fa-download" title="Download attachment"></div>
|
||||
<div class="deleteAttachmentButton right_menu_button fa-fw fa-solid fa-trash" title="Delete attachment"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actionButtonTemplate">
|
||||
<div class="actionButton list-group-item flex-container flexGap5" title="">
|
||||
<div class="actionButton list-group-item flex-container flexGap5" style="align-items: center;" title="">
|
||||
<i class="actionButtonIcon"></i>
|
||||
<img class="actionButtonImg"/>
|
||||
<span class="actionButtonText"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"display_name": "Chat Attachments",
|
||||
"display_name": "Data Bank (Chat Attachments)",
|
||||
"loading_order": 3,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
|
54
public/scripts/extensions/attachments/mediawiki-scrape.html
Normal file
54
public/scripts/extensions/attachments/mediawiki-scrape.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="scrapeInput" data-i18n="Enter a base URL of the MediaWiki to scrape.">
|
||||
Enter a <strong>base URL</strong> of the MediaWiki to scrape.
|
||||
</label>
|
||||
<i data-i18n="Don't include the page name!">
|
||||
Don't include the page name!
|
||||
</i>
|
||||
<small>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://streetcat.wiki/index.php</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>https://tcrf.net</code>
|
||||
</small>
|
||||
<input type="text" id="scrapeInput" name="scrapeInput" class="text_pole" placeholder="">
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="scrapeFilter">
|
||||
Optional regex to pick the content by its title:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n="Example:">Example:</span>
|
||||
<code>/Mr. (Fresh|Snack)/gi</code>
|
||||
</small>
|
||||
<input type="text" id="scrapeFilter" name="scrapeFilter" class="text_pole" placeholder="">
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label>
|
||||
Output format:
|
||||
</label>
|
||||
<label class="checkbox_label justifyLeft" for="scrapeOutputSingle">
|
||||
<input id="scrapeOutputSingle" type="radio" name="scrapeOutput" 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="scrapeOutputMulti">
|
||||
<input id="scrapeOutputMulti" type="radio" name="scrapeOutput" 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>
|
@@ -0,0 +1,8 @@
|
||||
<div class="flex-container justifyCenter alignItemsBaseline">
|
||||
<span>Move <strong class="moveAttachmentName">{{name}}</strong> to...</span>
|
||||
<select class="moveAttachmentTarget">
|
||||
{{#each targets}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
@@ -19,6 +19,16 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.attachmentListItem.disabled .attachmentListItemName {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.attachmentListItem.disabled .attachmentFileIcon {
|
||||
opacity: 0.75;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.attachmentListItemSize {
|
||||
min-width: 4em;
|
||||
text-align: right;
|
||||
|
@@ -5,7 +5,9 @@ import { getMessageTimeStamp } from '../../RossAscends-mods.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'caption';
|
||||
@@ -254,6 +256,19 @@ async function onSelectImage(e, prompt, quiet) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const caption = await getCaptionForFile(file, prompt, quiet);
|
||||
form && form.reset();
|
||||
return caption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a caption for an image file.
|
||||
* @param {File} file Input file
|
||||
* @param {string} prompt Caption prompt
|
||||
* @param {boolean} quiet Suppresses sending a message
|
||||
* @returns {Promise<string>} Generated caption
|
||||
*/
|
||||
async function getCaptionForFile(file, prompt, quiet) {
|
||||
try {
|
||||
setSpinnerIcon();
|
||||
const context = getContext();
|
||||
@@ -273,7 +288,6 @@ async function onSelectImage(e, prompt, quiet) {
|
||||
return '';
|
||||
}
|
||||
finally {
|
||||
form && form.reset();
|
||||
setImageIcon();
|
||||
}
|
||||
}
|
||||
@@ -288,9 +302,26 @@ function onRefineModeInput() {
|
||||
* @param {object} args Named parameters
|
||||
* @param {string} prompt Caption prompt
|
||||
*/
|
||||
function captionCommandCallback(args, prompt) {
|
||||
async function captionCommandCallback(args, prompt) {
|
||||
const quiet = isTrueBoolean(args?.quiet);
|
||||
const id = args?.id;
|
||||
|
||||
if (!isNaN(Number(id))) {
|
||||
const message = getContext().chat[id];
|
||||
if (message?.extra?.image) {
|
||||
try {
|
||||
const fetchResult = await fetch(message.extra.image);
|
||||
const blob = await fetchResult.blob();
|
||||
const file = new File([blob], 'image.jpg', { type: blob.type });
|
||||
return await getCaptionForFile(file, prompt, quiet);
|
||||
} catch (error) {
|
||||
toastr.error('Failed to get image from the message. Make sure the image is accessible.');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const quiet = isTrueBoolean(args?.quiet);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
@@ -404,12 +435,17 @@ jQuery(function () {
|
||||
<select id="caption_multimodal_model" class="flex1 text_pole">
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
|
||||
<option data-type="openai" value="gpt-4o">gpt-4o</option>
|
||||
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
|
||||
@@ -418,6 +454,8 @@ jQuery(function () {
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
|
||||
<option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
|
||||
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
|
||||
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
|
||||
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
|
||||
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
|
||||
<option data-type="ollama" value="llava:latest">llava:latest</option>
|
||||
@@ -492,5 +530,35 @@ jQuery(function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
registerSlashCommand('caption', captionCommandCallback, [], '<span class="monospace">quiet=true/false [prompt]</span> - caption an image with an optional prompt and passes the caption down the pipe. Only multimodal sources support custom prompts. Set the "quiet" argument to true to suppress sending a captioned message, default: false.', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
|
||||
callback: captionCommandCallback,
|
||||
returns: 'caption',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'],
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'id', 'get image from a message with this ID', [ARGUMENT_TYPE.NUMBER], false, false,
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'prompt', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Caption an image with an optional prompt and passes the caption down the pipe.
|
||||
</div>
|
||||
<div>
|
||||
Only multimodal sources support custom prompts.
|
||||
</div>
|
||||
<div>
|
||||
Provide a message ID to get an image from a message instead of uploading one.
|
||||
</div>
|
||||
<div>
|
||||
Set the "quiet" argument to true to suppress sending a captioned message, default: false.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
});
|
||||
|
@@ -2,10 +2,13 @@ import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHea
|
||||
import { dragElement, isMobile } from '../../RossAscends-mods.js';
|
||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { loadMovingUIState, power_user } from '../../power-user.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
|
||||
import { hideMutedSprites } from '../../group-chats.js';
|
||||
import { isJsonSchemaSupported } from '../../textgen-settings.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'expressions';
|
||||
@@ -94,7 +97,7 @@ async function forceUpdateVisualNovelMode() {
|
||||
}
|
||||
}
|
||||
|
||||
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, 100);
|
||||
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick);
|
||||
|
||||
async function updateVisualNovelMode(name, expression) {
|
||||
const container = $('#visual-novel-wrapper');
|
||||
@@ -905,8 +908,10 @@ async function setSpriteSetCommand(_, folder) {
|
||||
|
||||
$('#expression_override').val(folder.trim());
|
||||
onClickExpressionOverrideButton();
|
||||
removeExpression();
|
||||
moduleWorker();
|
||||
// removeExpression();
|
||||
// moduleWorker();
|
||||
const vnMode = isVisualNovelMode();
|
||||
await sendExpressionCall(folder, lastExpression, true, vnMode);
|
||||
}
|
||||
|
||||
async function classifyCommand(_, text) {
|
||||
@@ -971,8 +976,8 @@ function sampleClassifyText(text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Remove asterisks and quotes
|
||||
let result = text.replace(/[*"]/g, '');
|
||||
// Replace macros, remove asterisks and quotes
|
||||
let result = substituteParams(text).replace(/[*"]/g, '');
|
||||
|
||||
const SAMPLE_THRESHOLD = 500;
|
||||
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
|
||||
@@ -1015,12 +1020,12 @@ function parseLlmResponse(emotionResponse, labels) {
|
||||
const parsedEmotion = JSON.parse(emotionResponse);
|
||||
return parsedEmotion?.emotion ?? fallbackExpression;
|
||||
} catch {
|
||||
const fuse = new Fuse([emotionResponse]);
|
||||
for (const label of labels) {
|
||||
const result = fuse.search(label);
|
||||
if (result.length > 0) {
|
||||
return label;
|
||||
}
|
||||
const fuse = new Fuse(labels, { includeScore: true });
|
||||
console.debug('Using fuzzy search in labels:', labels);
|
||||
const result = fuse.search(emotionResponse);
|
||||
if (result.length > 0) {
|
||||
console.debug(`fuzzy search found: ${result[0].item} as closest for the LLM response:`, emotionResponse);
|
||||
return result[0].item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1270,13 +1275,10 @@ async function getExpressionsList() {
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async function resolveExpressionsList() {
|
||||
// get something for offline mode (default images)
|
||||
if (!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) {
|
||||
return DEFAULT_EXPRESSIONS;
|
||||
}
|
||||
|
||||
// See if we can retrieve a specific expression list from the API
|
||||
try {
|
||||
if (extension_settings.expressions.api == EXPRESSION_API.extras) {
|
||||
// Check Extras api first, if enabled and that module active
|
||||
if (extension_settings.expressions.api == EXPRESSION_API.extras && modules.includes('classify')) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/classify/labels';
|
||||
|
||||
@@ -1291,7 +1293,10 @@ async function getExpressionsList() {
|
||||
expressionsList = data.labels;
|
||||
return expressionsList;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// If running the local classify model (not using the LLM), we ask that one
|
||||
if (extension_settings.expressions.api == EXPRESSION_API.local) {
|
||||
const apiResult = await fetch('/api/extra/classify/labels', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
@@ -1303,11 +1308,12 @@ async function getExpressionsList() {
|
||||
return expressionsList;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
|
||||
// If there was no specific list, or an error, just return the default expressions
|
||||
return DEFAULT_EXPRESSIONS;
|
||||
}
|
||||
|
||||
const result = await resolveExpressionsList();
|
||||
@@ -1965,9 +1971,61 @@ function migrateSettings() {
|
||||
});
|
||||
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> – force sets the sprite for the current character', true, true);
|
||||
registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> – sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
|
||||
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> – Returns the last set sprite / expression for the named character.', true, true);
|
||||
registerSlashCommand('th', toggleTalkingHeadCommand, ['talkinghead'], '– Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', true, true);
|
||||
registerSlashCommand('classify', classifyCommand, [], '<span class="monospace">(text)</span> – performs an emotion classification of the given text and returns a label.', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sprite',
|
||||
aliases: ['emote'],
|
||||
callback: setSpriteSlashCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'spriteId', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Force sets the sprite for the current character.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'spriteoverride',
|
||||
aliases: ['costume'],
|
||||
callback: setSpriteSetCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'optional folder', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lastsprite',
|
||||
callback: (_, value) => lastExpression[value.trim()] ?? '',
|
||||
returns: 'sprite',
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'charName', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Returns the last set sprite / expression for the named character.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'th',
|
||||
callback: toggleTalkingHeadCommand,
|
||||
aliases: ['talkinghead'],
|
||||
helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'classify',
|
||||
callback: classifyCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'text', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
returns: 'emotion classification label for the given text',
|
||||
helpString: `
|
||||
<div>
|
||||
Performs an emotion classification of the given text and returns a label.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/classify I am so happy today!</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
})();
|
||||
|
@@ -64,10 +64,6 @@
|
||||
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
|
||||
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
|
||||
</div>
|
||||
<h3 id="image_list_header">
|
||||
<strong>Sprite set:</strong> <span id="image_list_header_name"></span>
|
||||
</h3>
|
||||
<div id="image_list"></div>
|
||||
<div class="expression_buttons flex-container spaceEvenly">
|
||||
<div id="expression_upload_pack_button" class="menu_button">
|
||||
<i class="fa-solid fa-file-zipper"></i>
|
||||
@@ -78,8 +74,13 @@
|
||||
<span>Remove all image overrides</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.
|
||||
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>/characters/</b> folder of your user data directory and name it as the name of the character.
|
||||
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
|
||||
<h3 id="image_list_header">
|
||||
<strong>Sprite set:</strong> <span id="image_list_header_name"></span>
|
||||
</h3>
|
||||
<div id="image_list"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -14,7 +14,6 @@
|
||||
display: flex;
|
||||
height: calc(100vh - var(--topBarBlockSize));
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user