Merge branch 'staging' into disk-cache

This commit is contained in:
Cohee
2025-03-19 22:40:11 +02:00
90 changed files with 2082 additions and 550 deletions
+4
View File
@@ -3,6 +3,9 @@ module.exports = {
extends: [
'eslint:recommended',
],
plugins: [
'jsdoc',
],
env: {
es6: true,
},
@@ -78,6 +81,7 @@ module.exports = {
'public/scripts/extensions/tts/lib/**',
],
rules: {
'jsdoc/no-undefined-types': ['warn', { disableReporting: true, markVariablesAsUsed: true }],
'no-unused-vars': ['error', { args: 'none' }],
'no-control-regex': 'off',
'no-constant-condition': ['error', { checkLoops: false }],
+2 -2
View File
@@ -14,7 +14,7 @@
- '(🐧 Linux)'
🦊 Firefox:
- '(firefox|mozilla)'
- '\b(firefox|mozilla)\b'
📱 Mobile:
- '(iphone|ios|android|📱 Termux)'
- '\b(iphone|ios|android|📱 Termux)\b'
+6
View File
@@ -34,6 +34,9 @@
🦊 Firefox:
- head-branch: ['\bfirefox\b']
🧑‍🤝‍🧑 Group Chat:
- head-branch: ['\bgroups?\b']
🖼️ Image Gen:
- head-branch: ['\bimage-gen\b']
@@ -58,6 +61,9 @@
📜 Prompt:
- head-branch: ['\bprompt\b']
🧠 Reasoning:
- head-branch: ['\breasoning\b', '\breason\b', '\bthinking\b']
🚚 Refactor:
- head-branch: ['\brefactor(s|ed)?\b']
+27 -12
View File
@@ -23,7 +23,7 @@ We have a [Documentation website](https://docs.sillytavern.app/) to answer most
SillyTavern (or ST for short) is a locally installed user interface that allows you to interact with text generation LLMs, image generation engines, and TTS voice models.
Beginning in February 2023 as a fork of TavernAI 1.2.8, SillyTavern now has over 200 contributors and 3 years of independent development under its belt, and continues to serve as a leading software for savvy AI hobbyists.
Beginning in February 2023 as a fork of TavernAI 1.2.8, SillyTavern now has over 200 contributors and 2 years of independent development under its belt, and continues to serve as a leading software for savvy AI hobbyists.
## Our Vision
@@ -192,28 +192,43 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
##### Volume Mappings
* [config] - The directory where SillyTavern configuration files will be stored on your host machine
* [data] - The directory where SillyTavern user data (including characters) will be stored on your host machine
* [plugins] - (optional) The directory where SillyTavern server plugins will be stored on your host machine
* [extensions] - (optional) The directory where global UI extensions will be stored on your host machine
* `CONFIG_PATH` - The directory where SillyTavern configuration files will be stored on your host machine
* `DATA_PATH` - The directory where SillyTavern user data (including characters) will be stored on your host machine
* `PLUGINS_PATH` - (optional) The directory where SillyTavern server plugins will be stored on your host machine
* `EXTENSIONS_PATH` - (optional) The directory where global UI extensions will be stored on your host machine
##### Port Mappings
* [PublicPort] - The port to expose the traffic on. This is mandatory, as you will be accessing the instance from outside of its virtual machine container. DO NOT expose this to the internet without implementing a separate service for security.
* `PUBLIC_PORT` - The port to expose the traffic on. This is mandatory, as you will be accessing the instance from outside of its virtual machine container. DO NOT expose this to the internet without implementing a separate service for security.
##### Additional Settings
* [DockerNet] - The docker network that the container should be created with a connection to. If you don't know what it is, see the [official Docker documentation](https://docs.docker.com/reference/cli/docker/network/).
* [version] - On the right-hand side of this GitHub page, you'll see "Packages". Select the "sillytavern" package and you'll see the image versions. The image tag "latest" will keep you up-to-date with the current release. You can also utilize "staging" and "release" tags that point to the nightly images of the respective branches, but this may not be appropriate, if you are utilizing extensions that could be broken, and may need time to update.
* `SILLYTAVERN_VERSION` - On the right-hand side of this GitHub page, you'll see "Packages". Select the "sillytavern" package and you'll see the image versions. The image tag "latest" will keep you up-to-date with the current release. You can also utilize "staging" that points to the nightly image the respective branch.
#### Install command
#### Running the container
1. Open your Command Line
2. Run the following command
2. Run the following command in a folder where you want to store the configuration and data files:
`docker run --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
```bash
SILLYTAVERN_VERSION="latest"
PUBLIC_PORT="8000"
CONFIG_PATH="./config"
DATA_PATH="./data"
PLUGINS_PATH="./plugins"
EXTENSIONS_PATH="./extensions"
> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.
docker run \
--name="sillytavern" \
-p "$PUBLIC_PORT:8000/tcp" \
-v "$CONFIG_PATH:/home/node/app/config:rw" \
-v "$DATA_PATH:/home/node/app/data:rw" \
-v "$EXTENSIONS_PATH:/home/node/app/public/scripts/extensions/third-party:rw" \
-v "$PLUGINS_PATH:/home/node/app/plugins:rw" \
ghcr.io/sillytavern/sillytavern:"$SILLYTAVERN_VERSION"
```
> By default the container will run in the foreground. If you want to run it in the background, add the `-d` flag to the `docker run` command.
### Building the image yourself
+20 -16
View File
@@ -7,6 +7,10 @@ on:
issue_comment:
types: [created]
permissions:
contents: read
issues: write
jobs:
label-on-content:
name: 🏷️ Label Issues by Content
@@ -16,7 +20,7 @@ jobs:
- name: Checkout Repository
# Checkout
# https://github.com/marketplace/actions/checkout
uses: actions/checkout@v4
uses: actions/checkout@v4.2.2
- name: Auto-Label Issues (Based on Issue Content)
# only auto label based on issue content once, on open (to prevent re-labeling removed labels)
@@ -24,11 +28,11 @@ jobs:
# Issue Labeler
# https://github.com/marketplace/actions/regex-issue-labeler
uses: github/issue-labeler@v3
uses: github/issue-labeler@v3.4
with:
configuration-path: .github/issues-auto-labels.yml
enable-versioned-regex: 0
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
label-on-labels:
name: 🏷️ Label Issues by Labels
@@ -39,40 +43,40 @@ jobs:
if: contains(fromJSON('["👩‍💻 Good First Issue", "🙏 Help Wanted", "🪲 Confirmed", "⚠️ High Priority", "❕ Medium Priority", "💤 Low Priority"]'), github.event.label.name)
# 🤖 Issues Helper
# https://github.com/marketplace/actions/issues-helper
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'add-labels'
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
labels: '👍 Approved'
- name: ❌ Remove progress labels when issue is marked done or stale
if: contains(fromJSON('["✅ Done", "✅ Done (staging)", "⚰️ Stale", "❌ wontfix"]'), github.event.label.name)
# 🤖 Issues Helper
# https://github.com/marketplace/actions/issues-helper
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'remove-labels'
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
labels: '🧑‍💻 In Progress,🤔 Unsure,🤔 Under Consideration'
- name: ❌ Remove temporary labels when confirmed labels are added
if: contains(fromJSON('["❌ wontfix","👍 Approved","👩‍💻 Good First Issue"]'), github.event.label.name)
# 🤖 Issues Helper
# https://github.com/marketplace/actions/issues-helper
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'remove-labels'
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
labels: '🤔 Unsure,🤔 Under Consideration'
- name: ❌ Remove no bug labels when "🪲 Confirmed" is added
if: github.event.label.name == '🪲 Confirmed'
# 🤖 Issues Helper
# https://github.com/marketplace/actions/issues-helper
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'remove-labels'
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
labels: '✖️ Not Reproducible,✖️ Not A Bug'
remove-stale-label:
@@ -85,10 +89,10 @@ jobs:
- name: Remove Stale Label
# 🤖 Issues Helper
# https://github.com/marketplace/actions/issues-helper
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'remove-labels'
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: '⚰️ Stale,🕸️ Inactive,🚏 Awaiting User Response,🛑 No Response'
@@ -101,12 +105,12 @@ jobs:
- name: Checkout Repository
# Checkout
# https://github.com/marketplace/actions/checkout
uses: actions/checkout@v4
uses: actions/checkout@v4.2.2
- name: Post Issue Comments Based on Labels
# Label Commenter
# https://github.com/marketplace/actions/label-commenter
uses: peaceiris/actions-label-commenter@v1
uses: peaceiris/actions-label-commenter@v1.10.0
with:
config_file: .github/issues-auto-comments.yml
github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -6,6 +6,10 @@ on:
- staging
- release
permissions:
contents: read
issues: write
jobs:
# This runs commits to staging/release, reading the commit messages. Check `pr-auto-manager.yml`:`update-linked-issues` for PR-linked updates.
update-linked-issues:
@@ -16,18 +20,18 @@ jobs:
- name: Checkout Repository
# Checkout
# https://github.com/marketplace/actions/checkout
uses: actions/checkout@v4
uses: actions/checkout@v4.2.2
- name: Extract Linked Issues from Commit Message
id: extract_issues
run: |
ISSUES=$(git log -1 --pretty=%B | grep -oiE '(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) #([0-9]+)' | awk '{print $2}' | tr -d '#' | jq -R -s -c 'split("\n")[:-1]')
ISSUES=$(git log ${{ github.event.before }}..${{ github.event.after }} --pretty=%B | grep -oiE '(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) #([0-9]+)' | awk '{print $2}' | tr -d '#' | jq -R -s -c 'split("\n")[:-1]')
echo "issues=$ISSUES" >> $GITHUB_ENV
- name: Label Linked Issues
id: label_linked_issues
env:
GH_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for ISSUE in $(echo $issues | jq -r '.[]'); do
if [ "${{ github.ref }}" == "refs/heads/staging" ]; then
+11 -6
View File
@@ -6,6 +6,11 @@ on:
schedule:
- cron: '0 0 * * *' # Runs every day at midnight UTC
permissions:
contents: read
issues: write
pull-requests: write
jobs:
mark-inactivity:
name: ⏳ Mark Issues/PRs without Activity
@@ -15,9 +20,9 @@ jobs:
- name: Mark Issues/PRs without Activity
# Close Stale Issues and PRs
# https://github.com/marketplace/actions/close-stale-issues
uses: actions/stale@v9
uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 183
days-before-close: 7
operations-per-run: 30
@@ -49,9 +54,9 @@ jobs:
- name: Mark Issues/PRs Awaiting User Response
# Close Stale Issues and PRs
# https://github.com/marketplace/actions/close-stale-issues
uses: actions/stale@v9
uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 7
days-before-close: 7
operations-per-run: 30
@@ -76,9 +81,9 @@ jobs:
- name: Mark Issues with Alternative Exists
# Close Stale Issues and PRs
# https://github.com/marketplace/actions/close-stale-issues
uses: actions/stale@v9
uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 7
days-before-close: 7
operations-per-run: 30
+7 -2
View File
@@ -6,6 +6,11 @@ on:
pull_request_target:
types: [closed]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
remove-labels:
name: 🗑️ Remove Pending Labels on Close
@@ -15,9 +20,9 @@ jobs:
- name: Remove Pending Labels on Close
# 🤖 Issues Helper
# https://github.com/marketplace/actions/issues-helper
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper@v3.6.0
with:
actions: remove-labels
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number || github.event.pull_request.number }}
labels: '🚏 Awaiting User Response,🧑‍💻 In Progress,📌 Keep Open,🚫 Merge Conflicts,🔬 Needs Testing,🔨 Needs Work,⚰️ Stale,⛔ Waiting For External/Upstream'
+7 -2
View File
@@ -6,6 +6,11 @@ on:
pull_request_target:
types: [opened]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
label-maintainer:
name: 🏷️ Label if Author is a Repo Maintainer
@@ -16,9 +21,9 @@ jobs:
- name: Label if Author is a Repo Maintainer
# 🤖 Issues Helper
# https://github.com/marketplace/actions/issues-helper
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'add-labels'
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number || github.event.pull_request.number }}
labels: '👷 Maintainer'
+91 -23
View File
@@ -6,18 +6,67 @@ on:
pull_request_review_comment:
types: [created]
permissions:
contents: read
pull-requests: write
jobs:
run-eslint:
name: ✅ Check ESLint on PR
runs-on: ubuntu-latest
# Only needs to run when code is changed
if: github.event.action == 'opened' || github.event.action == 'synchronize'
steps:
- name: Checkout Repository
# Checkout
# https://github.com/marketplace/actions/checkout
uses: actions/checkout@v4.2.2
- name: Setup Node.js
# Setup Node.js environment
# https://github.com/marketplace/actions/setup-node-js-environment
uses: actions/setup-node@v4.3.0
with:
node-version: 20
- name: Run npm install
run: npm ci
- name: Run ESLint
# Action ESLint
# https://github.com/marketplace/actions/action-eslint
uses: sibiraj-s/action-eslint@v3.0.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
eslint-args: '--ignore-path=.gitignore --quiet'
extensions: 'js,ts'
annotations: true
ignore-patterns: |
dist/
lib/
label-by-size:
name: 🏷️ Label PR by Size
# This job should run after all others, to prevent possible concurrency issues
needs: [label-by-branches, label-by-files, remove-stale-label, check-merge-blocking-labels, write-auto-comments]
runs-on: ubuntu-latest
# Only needs to run when code is changed
if: always() && (github.event.action == 'opened' || github.event.action == 'synchronize')
# Override permissions, the labeler needs issues write access
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Label PR Size
# Pull Request Size Labeler
# https://github.com/marketplace/actions/pull-request-size-labeler
uses: codelytv/pr-size-labeler@v1
uses: codelytv/pr-size-labeler@v1.10.2
with:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
xs_label: '🟩 ⬤○○○○'
xs_max_size: '20'
s_label: '🟩 ⬤⬤○○○'
@@ -28,7 +77,6 @@ jobs:
l_max_size: '1000'
xl_label: '🟥 ⬤⬤⬤⬤⬤'
fail_if_xl: 'false'
github_api_url: 'https://api.github.com'
files_to_ignore: |
"package-lock.json"
"public/lib/*"
@@ -36,55 +84,63 @@ jobs:
label-by-branches:
name: 🏷️ Label PR by Branches
runs-on: ubuntu-latest
# Only label once when PR is created or branches are changed, to allow manual label removal
if: github.event.action == 'opened' || (github.event.action == 'synchronize' && (github.event.changes.base || github.event.changes.head))
# Only label once when PR is created or when base branch is changed, to allow manual label removal
if: github.event.action == 'opened' || (github.event.action == 'synchronize' && github.event.changes.base)
steps:
- name: Checkout Repository
# Checkout
# https://github.com/marketplace/actions/checkout
uses: actions/checkout@v4
uses: actions/checkout@v4.2.2
- name: Apply Labels Based on Branch Name and Target Branch
# Pull Request Labeler
# https://github.com/marketplace/actions/labeler
uses: actions/labeler@v5
uses: actions/labeler@v5.0.0
with:
configuration-path: .github/pr-auto-labels-by-branch.yml
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
label-by-files:
name: 🏷️ Label PR by Files
runs-on: ubuntu-latest
# Only needs to run when code is changed
if: github.event.action == 'opened' || github.event.action == 'synchronize'
steps:
- name: Checkout Repository
# Checkout
# https://github.com/marketplace/actions/checkout
uses: actions/checkout@v4
uses: actions/checkout@v4.2.2
- name: Apply Labels Based on Changed Files
# Pull Request Labeler
# https://github.com/marketplace/actions/labeler
uses: actions/labeler@v5
uses: actions/labeler@v5.0.0
with:
configuration-path: .github/pr-auto-labels-by-files.yml
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
remove-stale-label:
name: 🗑️ Remove Stale Label on Comment
runs-on: ubuntu-latest
# Only runs when this is not done by the github actions bot
if: github.actor != 'github-actions[bot]'
# Only runs on comments not done by the github actions bot
if: github.event_name == 'pull_request_review_comment' && github.actor != 'github-actions[bot]'
# Override permissions, issue labeler needs issues write access
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Remove Stale Label
# 🤖 Issues Helper
# https://github.com/marketplace/actions/issues-helper
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper@v3.6.0
with:
actions: 'remove-labels'
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.pull_request.number }}
labels: '⚰️ Stale'
@@ -95,12 +151,18 @@ jobs:
# Run, even if the previous jobs were skipped/failed
if: always()
# Override permissions, as this needs to write a check
permissions:
checks: write
contents: read
pull-requests: read
steps:
- name: Check Merge Blocking
# GitHub Script
# https://github.com/marketplace/actions/github-scriptLabels
# https://github.com/marketplace/actions/github-script
id: label-check
uses: actions/github-script@v7
uses: actions/github-script@v7.0.1
with:
script: |
const prLabels = context.payload.pull_request.labels.map(label => label.name);
@@ -134,7 +196,7 @@ jobs:
write-auto-comments:
name: 💬 Post PR Comments Based on Labels
needs: [label-by-size, label-by-branches, label-by-files]
needs: [label-by-branches, label-by-files]
runs-on: ubuntu-latest
# Run, even if the previous jobs were skipped/failed
if: always()
@@ -143,15 +205,15 @@ jobs:
- name: Checkout Repository
# Checkout
# https://github.com/marketplace/actions/checkout
uses: actions/checkout@v4
uses: actions/checkout@v4.2.2
- name: Post PR Comments Based on Labels
# Label Commenter for PRs
# https://github.com/marketplace/actions/label-commenter
uses: peaceiris/actions-label-commenter@v1
uses: peaceiris/actions-label-commenter@v1.10.0
with:
config_file: .github/pr-auto-comments.yml
github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# This runs on merged PRs to staging, reading the PR body and directly linked issues. Check `issues-updates-on-merge.yml`:`update-linked-issues` for commit-based updates.
update-linked-issues:
@@ -159,6 +221,12 @@ jobs:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'staging'
# Override permissions, We need to be able to write to issues
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Extract Linked Issues From PR Description
id: extract_issues
@@ -172,7 +240,7 @@ jobs:
PR_NUMBER=${{ github.event.pull_request.number }}
REPO=${{ github.repository }}
API_URL="https://api.github.com/repos/$REPO/pulls/$PR_NUMBER/issues"
ISSUES=$(curl -s -H "Authorization: token ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" "$API_URL" | jq -r '.[].number' | jq -R -s -c 'split("\n")[:-1]')
ISSUES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$API_URL" | jq -r '.[].number' | jq -R -s -c 'split("\n")[:-1]')
echo "linked_issues=$ISSUES" >> $GITHUB_ENV
- name: Merge Issue Lists
@@ -184,7 +252,7 @@ jobs:
- name: Label Linked Issues
id: label_linked_issues
env:
GH_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for ISSUE in $(echo $final_issues | jq -r '.[]'); do
gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)"
@@ -7,6 +7,10 @@ on:
pull_request_target:
types: [synchronize]
permissions:
contents: read
pull-requests: write
jobs:
check-merge-conflicts:
name: ⚔️ Check Merge Conflicts
@@ -16,9 +20,9 @@ jobs:
- name: Check Merge Conflicts
# Label Conflicting Pull Requests
# https://github.com/marketplace/actions/label-conflicting-pull-requests
uses: eps1lon/actions-label-merge-conflict@v3
uses: eps1lon/actions-label-merge-conflict@v3.0.3
with:
dirtyLabel: '🚫 Merge Conflicts'
repoToken: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
repoToken: ${{ secrets.GITHUB_TOKEN }}
commentOnDirty: >
⚠️ This PR has conflicts that need to be resolved before it can be merged.
+2
View File
@@ -114,6 +114,8 @@ backups:
chat:
# Enable automatic chat backups
enabled: true
# Verify integrity of chat files before saving
checkIntegrity: true
# Maximum number of chat backups to keep per user (starting from the most recent). Set to -1 to keep all backups.
maxTotalBackups: -1
# Interval in milliseconds to throttle chat backups per user
+8
View File
@@ -786,5 +786,13 @@
{
"filename": "presets/context/DeepSeek-V2.5.json",
"type": "context"
},
{
"filename": "presets/reasoning/DeepSeek.json",
"type": "reasoning"
},
{
"filename": "presets/reasoning/Blank.json",
"type": "reasoning"
}
]
@@ -0,0 +1,6 @@
{
"name": "Blank",
"prefix": "",
"suffix": "",
"separator": ""
}
@@ -0,0 +1,6 @@
{
"name": "DeepSeek",
"prefix": "<think>\n",
"suffix": "\n</think>",
"separator": "\n\n"
}
+202 -4
View File
@@ -110,7 +110,8 @@
"@types/write-file-atomic": "^4.0.3",
"@types/yargs": "^17.0.33",
"@types/yauzl": "^2.10.3",
"eslint": "^8.57.1"
"eslint": "^8.57.1",
"eslint-plugin-jsdoc": "^48.10.0"
},
"engines": {
"node": ">= 18"
@@ -147,6 +148,21 @@
"integrity": "sha512-KlmTftToTtmb6aLVdne4NluS+POWputPF5J8v25UN/EQS+K9vahWEIe1NPRSFqBQclObkqHaj7JOnFrmnSm5MA==",
"license": "Apache-2.0"
},
"node_modules/@es-joy/jsdoccomment": {
"version": "0.46.0",
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz",
"integrity": "sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"comment-parser": "1.4.1",
"esquery": "^1.6.0",
"jsdoc-type-pratt-parser": "~4.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -970,6 +986,19 @@
"node": ">=14"
}
},
"node_modules/@pkgr/core": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -2099,6 +2128,16 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/are-docs-informative": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz",
"integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2614,6 +2653,16 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/comment-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
"integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/compress-commons": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
@@ -3560,6 +3609,69 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-jsdoc": {
"version": "48.10.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.10.0.tgz",
"integrity": "sha512-BEli0k8E0dzhJairAllwlkGnyYDZVKNn4WDmyKy+v6J5qGNuofjzxwNUi+55BOGmyO9mKBhqaidwGy+dxndn/Q==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@es-joy/jsdoccomment": "~0.46.0",
"are-docs-informative": "^0.0.2",
"comment-parser": "1.4.1",
"debug": "^4.3.5",
"escape-string-regexp": "^4.0.0",
"esquery": "^1.6.0",
"parse-imports": "^2.1.1",
"semver": "^7.6.3",
"spdx-expression-parse": "^4.0.0",
"synckit": "^0.9.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-jsdoc/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-jsdoc/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -3690,9 +3802,9 @@
}
},
"node_modules/esquery": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
"integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -5015,6 +5127,16 @@
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/jsdoc-type-pratt-parser": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz",
"integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -5891,6 +6013,20 @@
"integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==",
"license": "MIT"
},
"node_modules/parse-imports": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz",
"integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==",
"dev": true,
"license": "Apache-2.0 AND MIT",
"dependencies": {
"es-module-lexer": "^1.5.3",
"slashes": "^3.0.12"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -6600,6 +6736,19 @@
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
@@ -6811,6 +6960,13 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/slashes": {
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz",
"integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==",
"dev": true,
"license": "ISC"
},
"node_modules/sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
@@ -6903,6 +7059,31 @@
"source-map": "^0.6.0"
}
},
"node_modules/spdx-exceptions": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
"integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
"dev": true,
"license": "CC-BY-3.0"
},
"node_modules/spdx-expression-parse": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
"integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
}
},
"node_modules/spdx-license-ids": {
"version": "3.0.21",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
"integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
"dev": true,
"license": "CC0-1.0"
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
@@ -7042,6 +7223,23 @@
"node": ">=8"
}
},
"node_modules/synckit": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.1.0",
"tslib": "^2.6.2"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+2 -1
View File
@@ -140,6 +140,7 @@
"@types/write-file-atomic": "^4.0.3",
"@types/yargs": "^17.0.33",
"@types/yauzl": "^2.10.3",
"eslint": "^8.57.1"
"eslint": "^8.57.1",
"eslint-plugin-jsdoc": "^48.10.0"
}
}
-3
View File
@@ -101,15 +101,12 @@ const keyMigrationMap = [
newKey: 'performance.memoryCacheCapacity',
migrate: (value) => `${value}mb`,
},
// uncomment one release after 1.12.13
/*
{
oldKey: 'cookieSecret',
newKey: 'cookieSecret',
migrate: () => void 0,
remove: true,
},
*/
];
/**
+4
View File
@@ -1,7 +1,11 @@
import libs from './lib';
import getContext from './scripts/st-context';
import { power_user } from './scripts/power-user';
declare global {
// Custom types
declare type InstructSettings = typeof power_user.instruct;
// Global namespace modules
interface Window {
ai: any;
+34 -11
View File
@@ -1957,7 +1957,7 @@
<span data-i18n="Enable web search">Enable web search</span>
</label>
<div class="flexBasis100p toggle-description justifyLeft">
<span>
<span data-i18n="Use search capabilities provided by the backend.">
Use search capabilities provided by the backend.
</span>
</div>
@@ -2188,7 +2188,7 @@
<input id="horde_trusted_workers_only" type="checkbox" />
<span data-i18n="Trusted workers only">Trusted workers only</span>
</label>
<small id="adjustedHordeParams">Context: --, Response: --</small>
<small id="adjustedHordeParams"><span data-i18n="Context">Context</span>: --, <span data-i18n="Response">Response</span>: --</small>
<h4 data-i18n="API key">API key</h4>
<small>
<span data-i18n="Get it here:">Get it here: </span> <a target="_blank" href="https://aihorde.net/register" data-i18n="Register">Register</a> (<a id="horde_kudos" href="javascript:void(0);" data-i18n="View my Kudos">View my Kudos</a>)<br>
@@ -3193,6 +3193,7 @@
<option value="mistral-small-latest">mistral-small-latest</option>
<option value="mistral-medium-latest">mistral-medium-latest</option>
<option value="mistral-large-latest">mistral-large-latest</option>
<option value="mistral-saba-latest">mistral-saba-latest</option>
<option value="codestral-latest">codestral-latest</option>
<option value="codestral-mamba-latest">codestral-mamba-latest</option>
<option value="pixtral-12b-latest">pixtral-12b-latest</option>
@@ -3208,13 +3209,20 @@
<option value="mistral-small-2312">mistral-small-2312</option>
<option value="mistral-small-2402">mistral-small-2402</option>
<option value="mistral-small-2409">mistral-small-2409</option>
<option value="mistral-small-2501">mistral-small-2501</option>
<option value="mistral-small-2503">mistral-small-2503</option>
<option value="mistral-medium-2312">mistral-medium-2312</option>
<option value="mistral-large-2402">mistral-large-2402</option>
<option value="mistral-large-2407">mistral-large-2407</option>
<option value="mistral-large-2411">mistral-large-2411</option>
<option value="mistral-large-pixtral-2411">mistral-large-pixtral-2411</option>
<option value="mistral-saba-2502">mistral-saba-2502</option>
<option value="codestral-2405">codestral-2405</option>
<option value="codestral-2405-blue">codestral-2405-blue</option>
<option value="codestral-mamba-2407">codestral-mamba-2407</option>
<option value="codestral-2411-rc5">codestral-2411-rc5</option>
<option value="codestral-2412">codestral-2412</option>
<option value="codestral-2501">codestral-2501</option>
<option value="pixtral-12b-2409">pixtral-12b-2409</option>
<option value="pixtral-large-2411">pixtral-large-2411</option>
</optgroup>
@@ -3407,17 +3415,10 @@
<div class="flex-container">
<select id="model_custom_select" class="text_pole model_custom_select"></select>
</div>
<h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
<select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">
<option data-i18n="prompt_post_processing_none" value="">None</option>
<option data-i18n="prompt_post_processing_merge" value="merge">Merge consecutive roles</option>
<option data-i18n="prompt_post_processing_semi" value="semi">Semi-strict (alternating roles)</option>
<option data-i18n="prompt_post_processing_strict" value="strict">Strict (user first, alternating roles)</option>
</select>
</form>
<div id="01ai_form" data-source="01ai">
<h4>
<a data-i18n="01.AI API Key" href="https://platform.01.ai/" target="_blank" rel="noopener noreferrer">
<a data-i18n="01.AI API Key" href="https://platform.lingyiwanwu.com/" target="_blank" rel="noopener noreferrer">
01.AI API Key
</a>
</h4>
@@ -3432,6 +3433,15 @@
<select id="model_01ai_select">
</select>
</div>
<div id="prompt_post_porcessing_form" data-source="custom,openrouter">
<h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
<select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">
<option data-i18n="prompt_post_processing_none" value="">None</option>
<option data-i18n="prompt_post_processing_merge" value="merge">Merge consecutive roles</option>
<option data-i18n="prompt_post_processing_semi" value="semi">Semi-strict (alternating roles)</option>
<option data-i18n="prompt_post_processing_strict" value="strict">Strict (user first, alternating roles)</option>
</select>
</div>
<div class="flex-container flex">
<div id="api_button_openai" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect">Connect</div>
<div class="api_loading menu_button menu_button_icon" data-i18n="Cancel">Cancel</div>
@@ -3917,6 +3927,19 @@
<summary data-i18n="Reasoning Formatting">
Reasoning Formatting
</summary>
<div class="flex-container" title="Select your current Reasoning Template" data-i18n="[title]Select your current Reasoning Template">
<select id="reasoning_select" data-preset-manager-for="reasoning" class="flex1 text_pole"></select>
<div class="flex-container margin0 justifyCenter gap3px">
<input type="file" hidden data-preset-manager-file="reasoning" accept=".json, .settings">
<i data-preset-manager-update="reasoning" class="menu_button fa-solid fa-save" title="Update current template" data-i18n="[title]Update current template"></i>
<i data-preset-manager-rename="reasoning" class="menu_button fa-pencil fa-solid" title="Rename current template" data-i18n="[title]Rename current template"></i>
<i data-preset-manager-new="reasoning" class="menu_button fa-solid fa-file-circle-plus" title="Save template as" data-i18n="[title]Save template as"></i>
<i data-preset-manager-import="reasoning" class="displayNone menu_button fa-solid fa-file-import" title="Import template" data-i18n="[title]Import template"></i>
<i data-preset-manager-export="reasoning" class="displayNone menu_button fa-solid fa-file-export" title="Export template" data-i18n="[title]Export template"></i>
<i data-preset-manager-restore="reasoning" class="menu_button fa-solid fa-recycle" title="Restore current template" data-i18n="[title]Restore current template"></i>
<i data-preset-manager-delete="reasoning" class="menu_button fa-solid fa-trash-can" title="Delete template" data-i18n="[title]Delete template"></i>
</div>
</div>
<div class="flex-container">
<div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
<small data-i18n="Prefix">Prefix</small>
@@ -6563,7 +6586,7 @@
<div class="ch_name"></div>
<small class="ch_additional_info group_select_counter"></small>
</div>
<small class="character_name_block_sub_line">in this group</small>
<small class="character_name_block_sub_line" data-i18n="in this group">in this group</small>
<i class='group_fav_icon fa-solid fa-star'></i>
<input class="ch_fav" value="" hidden />
<div class="group_select_block_list ch_description"></div>
+157 -21
View File
@@ -23,9 +23,8 @@
"Mirostat Mode": "Режим",
"Mirostat Tau": "Tau",
"Mirostat Eta": "Eta",
"Variability parameter for Mirostat outputs": "Параметр изменчивости для выходных данных Mirostat.",
"Variability parameter for Mirostat outputs": "Вариативность для выходных данных Mirostat.",
"Learning rate of Mirostat": "Скорость обучения Mirostat.",
"Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "Сила условия регуляризации контрастивного поиска. Установите значение 0, чтобы отключить CS.",
"Temperature Last": "Температура последней",
"LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Перед этим обязательно выберите подходящий токенизатор.\nПоследовательности, которых не должно быть на выходе.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счетчик токенов, если не уверены.",
"Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]",
@@ -60,13 +59,11 @@
"Add BOS Token": "Добавлять BOS-токен",
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "Добавлять BOS-токен в начале промпта. Если выключить, ответы могут стать более креативными.",
"Ban EOS Token": "Запретить EOS-токен",
"Ban the eos_token. This forces the model to never end the generation prematurely": "Запрет EOS-токена не позволит модели завершить генерацию преждевременно",
"Ban the eos_token. This forces the model to never end the generation prematurely": "Запрет EOS-токена не позволит модели завершить генерацию самостоятельно (только при достижении лимита токенов)",
"Skip Special Tokens": "Пропускать спец. токены",
"Beam search": "Поиск Beam",
"Number of Beams": "Количество Beam",
"Beam search": "Beam Search",
"Length Penalty": "Штраф за длину",
"Early Stopping": "Преждевременная остановка",
"Contrastive search": "Контрастный поиск",
"Early Stopping": "Прекращать сразу",
"Penalty Alpha": "Penalty Alpha",
"Seed": "Зерно",
"Epsilon Cutoff": "Epsilon Cutoff",
@@ -89,7 +86,7 @@
"Text Completion presets": "Пресеты для Text Completion",
"Documentation on sampling parameters": "Документация по параметрам сэмплеров",
"Set all samplers to their neutral/disabled state.": "Установить все сэмплеры в нейтральное/отключенное состояние.",
"Only enable this if your model supports context sizes greater than 8192 tokens": "Включайте эту опцию, только если ваша модель поддерживает размер контекста более 8192 токенов.\nУвеличивайте только если вы знаете, что делаете.",
"Only enable this if your model supports context sizes greater than 8192 tokens": "Включайте эту опцию, только если ваша модель поддерживает размер контекста более 8192 токенов.\nУвеличивайте только если вы понимаете, что делаете.",
"Wrap in Quotes": "Заключать в кавычки",
"Wrap entire user message in quotes before sending.": "Перед отправкой заключать всё сообщение пользователя в кавычки.",
"Leave off if you use quotes manually for speech.": "Оставьте выключенным, если вручную выставляете кавычки для прямой речи.",
@@ -109,7 +106,7 @@
"Adjust response length to worker capabilities": "Подстраивать длину ответа под возможности рабочих машин",
"API key": "API-ключ",
"Tabby API key": "Tabby API-ключ",
"Get it here:": "Получить здесь:",
"Get it here:": "Получите здесь:",
"Register": "Зарегистрироваться",
"TogetherAI Model": "Модель TogetherAI",
"Example: 127.0.0.1:5001": "Пример: http://127.0.0.1:5001",
@@ -289,10 +286,10 @@
"Author's Note": "Заметки автора",
"Replace empty message": "Заменять пустые сообщения",
"Send this text instead of nothing when the text box is empty.": "Этот текст будет отправлен в случае отсутствия текста на отправку.",
"Unrestricted maximum value for the context slider": "Убрать потолок для ползунка контекста. Включайте только если точно знаете, что делаете",
"Unrestricted maximum value for the context slider": "Убрать потолок для ползунка контекста. Включайте только если точно понимаете, что делаете",
"Chat Completion Source": "Источник для Chat Completion",
"Avoid sending sensitive information to the Horde.": "Избегайте отправки личной информации Horde",
"Review the Privacy statement": "Ознакомиться с заявлением о конфиденциальности",
"Avoid sending sensitive information to the Horde.": "Избегайте отправки личной информации Horde.",
"Review the Privacy statement": "Ознакомьтесь с заявлением о конфиденциальности",
"Trusted workers only": "Только доверенные рабочие машины",
"For privacy reasons, your API key will be hidden after you reload the page.": "Из соображений безопасности ваш API-ключ будет скрыт после перезагрузки страницы.",
"-- Horde models not loaded --": "--Модель Horde не загружена--",
@@ -699,7 +696,7 @@
"Aggressive": "Агрессивный",
"Very aggressive": "Очень агрессивный",
"Eta_Cutoff_desc": "Eta cutoff - основной параметр специальной техники сэмплинга под названием Eta Sampling.&#13;В единицах 1e-4; разумное значение - 3.&#13;Установите в 0, чтобы отключить.&#13;См. статью Truncation Sampling as Language Model Desmoothing от Хьюитт и др. (2022) для получения подробной информации.",
"Learn how to contribute your idle GPU cycles to the Horde": "Узнайте, как внести свой вклад в свои свободные GPU-циклы в орду",
"Learn how to contribute your idle GPU cycles to the Horde": "Узнайте, как использовать время простоя вашего GPU для помощи Horde",
"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",
@@ -744,7 +741,7 @@
"Last Assistant Prefix": "Последний префикс ассистента",
"System Instruction Prefix": "Префикс системной инструкции",
"User Filler Message": "Принудительное сообщение пользователя",
"Permanent": "перманентных",
"Permanent": "постоянных",
"Alt. Greetings": "Др. варианты",
"Smooth Streaming": "Плавный стриминг",
"Save checkpoint": "Сохранить чекпоинт",
@@ -1227,7 +1224,6 @@
"JSON-serialized array of strings.": "Список строк в формате JSON.",
"Mirostat_desc": "Mirostat - своего рода термометр, измеряющий перплексию для выводимого текста.\nMirostat подгоняет перплексию генерируемого текста к перплексии входного текста, что позволяет избежать повторов.\n(когда по мере генерации текста авторегрессионным инференсом, перплексия всё больше приближается к нулю)\n а также ловушки перплексии (когда перплексия начинает уходить в сторону)\nБолее подробное описание в статье Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).\nРежим выбирает версию Mirostat. 0=отключить, 1=Mirostat 1.0 (только llama.cpp), 2=Mirostat 2.0.",
"Helpful tip coming soon.": "Подсказку скоро добавим.",
"Temperature_Last_desc": "Использовать Temperature сэмплер в последнюю очередь. Это почти всегда разумно.\nПри включении: сначала выборка набора правдоподобных токенов, затем применение Temperature для корректировки их относительных вероятностей (технически, логитов).\nПри отключении: сначала применение Temperature для корректировки относительных вероятностей ВСЕХ токенов, затем выборка правдоподобных токенов из этого.\nОтключение Temperature Last увеличивает вероятности в хвосте распределения, что увеличивает шансы получить несогласованный ответ.",
"Speculative Ngram": "Speculative Ngram",
"Use a different speculative decoding method without a draft model": "Use a different speculative decoding method without a draft model.\rUsing a draft model is preferred. Speculative ngram is not as effective.",
"Spaces Between Special Tokens": "Spaces Between Special Tokens",
@@ -1734,7 +1730,7 @@
"markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'.",
"Save and Update": "Сохранить и обновить",
"Profile name:": "Название профиля:",
"API returned an error": "API вернуло ошибку",
"API returned an error": "API ответило ошибкой",
"Failed to save preset": "Не удалось сохранить пресет",
"Preset name should be unique.": "Название пресета должно быть уникальным.",
"Invalid file": "Невалидный файл",
@@ -1756,8 +1752,7 @@
"dot quota_error": "имеется достаточно кредитов.",
"If you have sufficient credits, please try again later.": "Если кредитов достаточно, то повторите попытку позднее.",
"Proxy preset '${0}' not found": "Пресет '${0}' не найден",
"Window.ai returned an error": "Window.ai вернул ошибку",
"Get it here:": "Загрузите здесь:",
"Window.ai returned an error": "Window.ai ответил ошибкой",
"Extension is not installed": "Расширение не установлено",
"Update or remove your reverse proxy settings.": "Измените или удалите ваши настройки прокси.",
"An error occurred while importing prompts. More info available in console.": "В процессе импорта произошла ошибка. Подробную информацию см. в консоли.",
@@ -1866,7 +1861,7 @@
"Group Chat could not be saved": "Не удалось сохранить групповой чат",
"Deleted group member swiped. To get a reply, add them back to the group.": "Вы пытаетесь свайпнуть удалённого члена группы. Чтобы получить ответ, добавьте этого персонажа обратно в группу.",
"Currently no group selected.": "В данный момент не выбрано ни одной группы.",
"Not so fast! Wait for the characters to stop typing before deleting the group.": "Чуть помедленнее! Перед удалением группы дождитесь, пока персонаж закончит печатать.",
"Not so fast! Wait for the characters to stop typing before deleting the group.": "Чуть помедленнее! Перед удалением группы дождитесь, пока персонажи закончат печатать.",
"Delete the group?": "Удалить группу?",
"This will also delete all your chats with that group. If you want to delete a single conversation, select a \"View past chats\" option in the lower left menu.": "Вместе с ней будут удалены и все её чаты. Если требуется удалить только один чат, воспользуйтесь кнопкой \"Все чаты\" в меню в левом нижнем углу.",
"Can't peek a character while group reply is being generated": "Невозможно открыть карточку персонажа во время генерации ответа",
@@ -1997,7 +1992,7 @@
"Default persona deleted": "Удалена персона по умолчанию",
"The locked persona was deleted. You will need to set a new persona for this chat.": "Удалена привязанная к чату персона. Вам будет необходимо выбрать новую фиксированную персону для этого чата.",
"Persona deleted": "Персона удалена",
"You must bind a name to this persona before you can set it as the default.": "Прежде чем установить эту персону в качестве персоны по умолчанию, ей необходимо задать имя.",
"You must bind a name to this persona before you can set it as the default.": "Прежде чем установить эту персону в качестве персоны по умолчанию, ей необходимо присвоить имя.",
"Persona name not set": "У персоны отсутствует имя",
"Are you sure you want to remove the default persona?": "Вы точно хотите снять статус персоны по умолчанию?",
"This persona will no longer be used by default when you open a new chat.": "Эта персона больше не будет автоматически выбираться при старте нового чата",
@@ -2203,5 +2198,146 @@
"Input:": "Входные данные:",
"Tokenized text:": "Токенизированный текст:",
"Token IDs:": "Идентификаторы токенов:",
"Tokens:": "Токенов:"
"Tokens:": "Токенов:",
"Max prompt cost:": "Макс. стоимость промпта:",
"Reset custom sampler selection": "Сбросить подборку семплеров",
"Here you can toggle the display of individual samplers. (WIP)": "Здесь можно включить или выключить отображение каждого из сэмплеров отдельно. (WIP)",
"Request Model Reasoning": "Запрашивать цепочку рассуждений",
"Reasoning": "Рассуждения / Reasoning",
"Auto-Parse": "Авто-парсинг",
"reasoning_auto_parse": "Автоматически считывать блоки рассуждений, расположенные между префиксом и суффиксом рассуждений. Для работы должно быть указано и то, и другое.",
"Auto-Expand": "Разворачивать",
"reasoning_auto_expand": "Автоматически разворачивать блоки рассуждений.",
"Show Hidden": "Показывать время",
"reasoning_show_hidden": "Отображать затраченное на рассуждения время для моделей со скрытой цепочкой рассуждений",
"Add to Prompts": "Добавлять в промпт",
"reasoning_add_to_prompts": "Добавлять существующие блоки рассуждений в промпт. Для добавления новых используйте меню редактирования сообщений.",
"reasoning_max_additions": "Макс. кол-во блоков рассуждений в промпте, считается от последнего сообщения",
"Max": "Макс.",
"Reasoning Formatting": "Форматирование рассуждений",
"Prefix": "Префикс",
"Suffix": "Постфикс",
"Separator": "Разделитель",
"reasoning_separator": "Вставляется между рассуждениями и содержанием самого сообщения.",
"reasoning_prefix": "Вставляется перед рассуждениями.",
"reasoning_suffix": "Вставляется после рассуждений.",
"Seed_desc": "Фиксированное значение зерна позволяет получать предсказуемые, одинаковые результаты на одинаковых настройках. Поставьте -1 для рандомного зерна.",
"# of Beams": "Кол-во лучей",
"The number of sequences generated at each step with Beam Search.": "Кол-во вариантов, генерируемых Beam Search на каждом шаге работы.",
"Penalize sequences based on their length.": "Штрафует строки в зависимости от длины",
"Controls the stopping condition for beam search. If checked, the generation stops as soon as there are '# of Beams' sequences. If not checked, a heuristic is applied and the generation is stopped when it's very unlikely to find better candidates.": "Определяет, когда останавливать работу Beam Search. Поставив галочку, вы укажете поиску остановиться тогда, когда будет достигнуто кол-во лучей из соответствующего поля. Если галочку не отмечать, то генерация остановится тогда, когда сочтёт, что дальше найти лучших кандидатов слишком маловероятно.",
"A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "Жадный алгоритм LLM-сэмплинга, подбирающий наиболее вероятную последовательность слов или токенов путём исследования и расширения сразу нескольких вариантов. На каждом шаге он удерживает фиксированное кол-во самых подходящих вариантов (ширина луча).",
"Smooth_Sampling_desc": "Изменяет распределение с помощью квадратичных и кубических преобразований. Снижение Коэффициента сглаживания даёт более креативные ответы, обычно идеальное значение находится в диапазоне 0.2-0.3 (при кривой сглаживания=1.0). Повышение значения Кривой сглаживания сделает кривую круче, что приведёт к более агрессивной фильтрации маловероятных вариантов. Установив Кривую сглаживания = 1.0, вы фактически нейтрализуете этот параметр и будете работать только с Коэффициентом",
"Temperature_Last_desc": "Применять сэмплер Температуры в последнюю очередь. Почти всегда оправдано.\nПри включении: сначала все токены семплируются, и затем температура регулирует распределение у оставшихся (технически, у оставшихся логитов).\nПри выключении: сначала температура настраивает распределение ВСЕХ токенов, и потом они семплируются уже с этим обновлённым распределением.\nПри отключении этой опции токены в хвосте получают больше шансов попасть в итоговую последовательность, что может привести к менее связным и логичным ответам.",
"Swipe # for All Messages": "Номер свайпа на всех сообщениях",
"Display swipe numbers for all messages, not just the last.": "Отображать номер свайпа для всех сообщений, а не только для последнего.",
"Penalty Range": "Окно для штрафа",
"Never": "Никогда",
"Groups and Past Personas": "Для групп и прошлых персон",
"Always": "Всегда",
"Request model reasoning": "Запрашивать рассуждения",
"Allows the model to return its thinking process.": "Позволяет модели высылать в ответе свою цепочку рассуждений.",
"Rename Persona": "Переименовать персону",
"Change Persona Image": "Изменить изображение персоны",
"Duplicate Persona": "Клонировать персону",
"Delete Persona": "Удалить персону",
"Enter a new name for this persona:": "Введите новое имя персоны:",
"Connections": "Связи",
"Click to select this as default persona for the new chats. Click again to remove it.": "Нажмите, чтобы установить эту персону стандартной для всех новых чатов. Нажмите ещё раз, чтобы отключить.",
"Character": "Персонаж",
"Click to lock your selected persona to the current character. Click again to remove the lock.": "Нажмите, чтобы закрепить эту персону для текущего персонажа. Нажмите ещё раз, чтобы открепить.",
"Chat": "Чат",
"[No character connections. Click one of the buttons above to connect this persona.]": "[Связи отсутствуют. Нажмите на одну из кнопок выше, чтобы создать.]",
"Global Settings": "Общие настройки",
"Allow multiple persona connections per character": "Разрешить привязывать несколько персон к одному персонажу",
"When multiple personas are connected to a character, a popup will appear to select which one to use": "При связывании нескольких персон с персонажем, будет появляться окошко с предложением выбрать нужную.",
"Auto-lock a chosen persona to the chat": "Автоматически привязывать выбранную персону к чату",
"Whenever a persona is selected, it will be locked to the current chat and automatically selected when the chat is opened.": "При выборе новой персоны она автоматически будет привязана к текущему чату, и будет выбираться при его открытии.",
"Current Persona": "Текущая персона",
"The chat has been successfully converted!": "Чат успешно преобразован!",
"Manual": "Когда вы скажете",
"Auto Mode delay": "Задержка авто-режима",
"Use tag as folder": "Тег-папка",
"All connections to ${0} have been removed.": "Все связи с персонажем ${0} были удалены.",
"Personas Unlocked": "Персоны отвязаны",
"Remove All Connections": "Удалить все связи",
"Persona ${0} selected and auto-locked to current chat": "Персона ${0} выбрана и автоматически закреплена за этим чатом",
"This persona is only temporarily chosen. Click for more info.": "Данная персона выбрана лишь временно. Нажмите, чтобы узнать больше.",
"Temporary Persona": "Временная персона",
"A different persona is locked to this chat, or you have a different default persona set. The currently selected persona will only be temporary, and resets on reload. Consider locking this persona to the chat if you want to permanently use it.": "К этому чату уже привязана иная персона, либо у вас выбрана иная персона по-умолчанию. Выбранная в данный момент персона будет временной, и сбросится после перезагрузки. Если хотите всегда использовать её в этом чате, советуем её прикрепить.",
"Current Persona: ${0}": "Выбранная персона: ${0}",
"Chat persona: ${0}": "Персона для этого чата: ${0}",
"Default persona: ${0}": "Персона по умолчанию (стандартная): ${0}",
"Persona ${0} is now unlocked from this chat.": "Персона ${0} отвязана от этого чата.",
"Persona Unlocked": "Персона отвязана",
"Persona ${0} is now unlocked from character ${1}.": "Персона ${0} отвязана от персонажа ${1}.",
"Persona Not Found": "Персона не найдена",
"Persona Locked": "Персона закреплена",
"User persona ${0} is locked to character ${1}${2}": "Персона ${0} прикреплена к персонажу ${1}${2}",
"Persona Name Not Set": "У персоны отсутствует имя",
"You must bind a name to this persona before you can set a lorebook.": "Перед привязкой лорбука персоне необходимо присвоить имя.",
"Default Persona Removed": "Персона по умолчанию снята",
"Persona is locked to the current character": "Персона закреплена за этим персонажем",
"Persona is locked to the current chat": "Персона закреплена за этим чатом",
"characters": "перс.",
"character": "персонаж",
"in this group": "в группе",
"Chatting Since": "Первая беседа",
"Context": "Контекст",
"Response": "Ответ",
"Connected": "Подключено",
"Enter new background name:": "Введите новое название для фона:",
"AI Horde Website": "Сайт AI Horde",
"Enable web search": "Включить поиск в Интернете",
"Use search capabilities provided by the backend.": "Разрешить использование предоставляемых бэкендом функций поиска.",
"Request inline images": "Запрашивать inline-изображения",
"Allows the model to return image attachments.": "Разрешить модели отправлять вложения в виде картинок.",
"Request inline images_desc_2": "Не совместимо со следующим функционалом: вызов функций, поиск в Интернете, системный промпт.",
"Connected Personas": "Связанные персоны",
"[Currently no personas connected]": "[Связанных персон нет]",
"The following personas are connected to the current character.\n\nClick on a persona to select it for the current character.\nShift + Click to unlink the persona from the character.": "С этим персонажем связаны следующие персоны.\n\nНажмите на персону, чтобы выбрать её для данного персонажа.\nShift + ЛКМ, чтобы её отвязать.",
"Persona Connections": "Связи с персонами",
"Pooled order": "Если уже давно не отвечали",
"Attach a File": "Приложить файл",
"Attach a file or image to a current chat.": "Приложить файл или изображение к текущему чату",
"Remove the file": "Удалить файл",
"Delete the Chat File?": "Удалить чат?",
"Forbidden": "Доступ запрещён",
"To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.": "Чтобы видеть здесь ваши API-ключи, установите параметр allowKeysExposure в config.yaml в положение true, после чего перезапустите сервер SillyTavern.",
"Invalid endpoint URL. Requests may fail.": "Некорректный адрес эндпоинта. Запросы могут не проходить.",
"How to install extensions?": "Как устанавливать расширения?",
"Click the flashing button to install extensions.": "Чтобы их установить, нажмите на мигающую кнопку.",
"ext_regex_reasoning_desc": "Содержимое блоков рассуждений. При отмеченной галочке \"Только промпт\" будут также обработаны добавленные в промпт рассуждения.",
"Macro in Find Regex": "Макросы в рег. выражении",
"Don't substitute": "Не заменять",
"Substitute (raw)": "Заменять в \"чистом\" виде",
"Substitute (escaped)": "Заменять после экранирования",
"ext_regex_other_options_desc": "По умолчанию, расширение вносит изменения в сам файл чата.\nПри включении одной из опций (или обеих), файл чата останется нетронутым, при этом сами изменения по-прежнему будут действовать.",
"ext_regex_flags_help": "Нажмите, чтобы узнать больше о флагах в рег. выражениях.",
"Applies to all matches": "Заменяет все вхождения",
"Applies to the first match": "Заменяет первое вхождение",
"Case insensitive": "Не чувствительно к регистру",
"Case sensitive": "Чувствительно к регистру",
"Find Regex is empty": "Рег. выражение не указано",
"Click the button to save it as a file.": "Нажмите на кнопку справа, чтобы сохранить его в файл.",
"Export as JSONL": "Экспорт в формате JSONL",
"Thought for some time": "Какое-то время заняли размышления",
"Thinking...": "В раздумьях...",
"Thought for ${0}": "Размышления заняли ${0}",
"Hidden reasoning - Add reasoning block": "Рассуждения скрыты - Добавить блок рассуждений",
"Add reasoning block": "Добавить блок рассуждений",
"Edit reasoning": "Редактировать рассуждения",
"Copy reasoning": "Скопировать рассуждения",
"Confirm Edit": "Подтвердить",
"Remove reasoning": "Удалить рассуждения",
"Cancel edit": "Отменить редактирование",
"Remove Reasoning": "Удалить рассуждения",
"Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.": "Вы точно хотите удалить блок рассуждений?<br />Основное сообщение останется на месте.",
"Reasoning Parse": "Парсинг рассуждений",
"Both prefix and suffix must be set in the Reasoning Formatting settings.": "В настройках форматирования рассуждений должны быть заданы префикс и суффикс.",
"Invalid return type '${0}', defaulting to 'reasoning'.": "Некорректный возвращаемый тип, используем стандартный 'reasoning'.",
"Reasoning already exists.": "Рассуждения уже присутствуют.",
"Edit Message": "Редактирование",
"Status check bypassed": "Проверка статуса отключена",
"Valid": "Работает"
}
+232 -80
View File
@@ -172,6 +172,7 @@ import {
copyText,
escapeHtml,
saveBase64AsFile,
uuidv4,
} from './scripts/utils.js';
import { debounce_timeout } from './scripts/constants.js';
@@ -494,6 +495,8 @@ export const event_types = {
GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
GENERATE_AFTER_DATA: 'generate_after_data',
GROUP_MEMBER_DRAFTED: 'group_member_drafted',
GROUP_WRAPPER_STARTED: 'group_wrapper_started',
GROUP_WRAPPER_FINISHED: 'group_wrapper_finished',
WORLD_INFO_ACTIVATED: 'world_info_activated',
TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready',
@@ -514,6 +517,9 @@ export const event_types = {
ONLINE_STATUS_CHANGED: 'online_status_changed',
IMAGE_SWIPED: 'image_swiped',
CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
CONNECTION_PROFILE_CREATED: 'connection_profile_created',
CONNECTION_PROFILE_DELETED: 'connection_profile_deleted',
CONNECTION_PROFILE_UPDATED: 'connection_profile_updated',
TOOL_CALLS_PERFORMED: 'tool_calls_performed',
TOOL_CALLS_RENDERED: 'tool_calls_rendered',
};
@@ -589,7 +595,7 @@ let is_delete_mode = false;
let fav_ch_checked = false;
let scrollLock = false;
export let abortStatusCheck = new AbortController();
let charDragDropHandler = null;
export let charDragDropHandler = null;
/** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed;
@@ -1140,7 +1146,7 @@ export async function clearItemizedPrompts() {
async function getStatusHorde() {
try {
const hordeStatus = await checkHordeStatus();
setOnlineStatus(hordeStatus ? 'Connected' : 'no_connection');
setOnlineStatus(hordeStatus ? t`Connected` : 'no_connection');
}
catch {
setOnlineStatus('no_connection');
@@ -1207,7 +1213,7 @@ async function getStatusTextgen() {
}
if ([textgen_types.GENERIC, textgen_types.OOBA].includes(textgen_settings.type) && textgen_settings.bypass_status_check) {
setOnlineStatus('Status check bypassed');
setOnlineStatus(t`Status check bypassed`);
return resultCheckStatus();
}
@@ -1232,7 +1238,7 @@ async function getStatusTextgen() {
setOnlineStatus(textgen_settings.togetherai_model);
} else if (textgen_settings.type === textgen_types.OLLAMA) {
loadOllamaModels(data?.data);
setOnlineStatus(textgen_settings.ollama_model || 'Connected');
setOnlineStatus(textgen_settings.ollama_model || t`Connected`);
} else if (textgen_settings.type === textgen_types.INFERMATICAI) {
loadInfermaticAIModels(data?.data);
setOnlineStatus(textgen_settings.infermaticai_model);
@@ -1256,7 +1262,7 @@ async function getStatusTextgen() {
setOnlineStatus(textgen_settings.tabby_model || data?.result);
} else if (textgen_settings.type === textgen_types.GENERIC) {
loadGenericModels(data?.data);
setOnlineStatus(textgen_settings.generic_model || data?.result || 'Connected');
setOnlineStatus(textgen_settings.generic_model || data?.result || t`Connected`);
} else {
setOnlineStatus(data?.result);
}
@@ -1370,8 +1376,11 @@ export function resultCheckStatus() {
* If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing.
* If the character is different from the currently selected one, it will clear the chat and reset any selected character or group.
* @param {number} id The ID of the character to switch to.
* @param {object} [options] Options for the switch.
* @param {boolean} [options.switchMenu=true] Whether to switch the right menu to the character edit menu if the character is already selected.
* @returns {Promise<void>} A promise that resolves when the character is switched.
*/
export async function selectCharacterById(id) {
export async function selectCharacterById(id, { switchMenu = true } = {}) {
if (characters[id] === undefined) {
return;
}
@@ -1400,9 +1409,9 @@ export async function selectCharacterById(id) {
}
} else {
//if clicked on character that was already selected
selected_button = 'character_edit';
switchMenu && (selected_button = 'character_edit');
await unshallowCharacter(this_chid);
select_selected_character(this_chid);
select_selected_character(this_chid, { switchMenu });
}
}
@@ -1787,6 +1796,7 @@ export async function getCharacters() {
body: JSON.stringify({}),
});
if (response.ok === true) {
const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null;
characters.splice(0, characters.length);
const getData = await response.json();
for (let i = 0; i < getData.length; i++) {
@@ -1800,8 +1810,16 @@ export async function getCharacters() {
characters[i]['chat'] = String(characters[i]['chat']);
}
if (this_chid !== undefined) {
$('#avatar_url_pole').val(characters[this_chid].avatar);
if (previousAvatar) {
const newCharacterId = characters.findIndex(x => x.avatar === previousAvatar);
if (newCharacterId >= 0) {
setCharacterId(newCharacterId);
await selectCharacterById(newCharacterId, { switchMenu: false });
} else {
await Popup.show.text(t`ERROR: The active character is no longer available.`, t`The page will be refreshed to prevent data loss. Press "OK" to continue.`);
return location.reload();
}
}
await getGroups();
@@ -2730,6 +2748,7 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
environment.mesExamplesRaw = fields.mesExamples || '';
environment.charVersion = fields.version || '';
environment.char_version = fields.version || '';
environment.charDepthPrompt = fields.charDepthPrompt || '';
}
// Must be substituted last so that they're replaced inside {{description}}
@@ -3081,13 +3100,38 @@ export function baseChatReplace(value, name1, name2) {
/**
* Returns the character card fields for the current character.
* @returns {{system: string, mesExamples: string, description: string, personality: string, persona: string, scenario: string, jailbreak: string, version: string}}
* @param {object} [options]
* @param {number} [options.chid] Optional character index
*
* @typedef {object} CharacterCardFields
* @property {string} system System prompt
* @property {string} mesExamples Message examples
* @property {string} description Description
* @property {string} personality Personality
* @property {string} persona Persona
* @property {string} scenario Scenario
* @property {string} jailbreak Jailbreak instructions
* @property {string} version Character version
* @property {string} charDepthPrompt Character depth note
* @returns {CharacterCardFields} Character card fields
*/
export function getCharacterCardFields() {
const result = { system: '', mesExamples: '', description: '', personality: '', persona: '', scenario: '', jailbreak: '', version: '' };
export function getCharacterCardFields({ chid = null } = {}) {
const currentChid = chid ?? this_chid;
const result = {
system: '',
mesExamples: '',
description: '',
personality: '',
persona: '',
scenario: '',
jailbreak: '',
version: '',
charDepthPrompt: '',
};
result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2);
const character = characters[this_chid];
const character = characters[currentChid];
if (!character) {
return result;
@@ -3101,9 +3145,10 @@ export function getCharacterCardFields() {
result.system = power_user.prefer_character_prompt ? baseChatReplace(character.data?.system_prompt?.trim(), name1, name2) : '';
result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(character.data?.post_history_instructions?.trim(), name1, name2) : '';
result.version = character.data?.character_version ?? '';
result.charDepthPrompt = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2);
if (selected_group) {
const groupCards = getGroupCharacterCards(selected_group, Number(this_chid));
const groupCards = getGroupCharacterCards(selected_group, Number(currentChid));
if (groupCards) {
result.description = groupCards.description;
@@ -3596,7 +3641,8 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
throw new Error(data.response);
}
const message = cleanUpMessage(extractMessageFromData(data), false, false, true);
// format result, exclude user prompt bias
const message = cleanUpMessage(extractMessageFromData(data), false, false, true, null, false);
if (!message) {
throw new Error('No message generated');
@@ -3905,6 +3951,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
mesExamples,
system,
jailbreak,
charDepthPrompt,
} = getCharacterCardFields();
if (main_api !== 'openai') {
@@ -3927,7 +3974,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan, role);
});
} else {
const depthPromptText = baseChatReplace(characters[this_chid]?.data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2) || '';
const depthPromptText = charDepthPrompt || '';
const depthPromptDepth = characters[this_chid]?.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
const depthPromptRole = getExtensionPromptRoleByName(characters[this_chid]?.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default);
setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan, depthPromptRole);
@@ -5868,13 +5915,14 @@ function extractMultiSwipes(data, type) {
return swipes;
}
export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncompleteSentences = false, stoppingStrings = null) {
export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncompleteSentences = false, stoppingStrings = null, includeUserPromptBias = true) {
if (!getMessage) {
return '';
}
// Add the prompt bias before anything else
if (
includeUserPromptBias &&
power_user.user_prompt_bias &&
!isImpersonate &&
!isContinue &&
@@ -6261,7 +6309,6 @@ export function syncMesToSwipe(messageId = null) {
}
const targetMessage = chat[targetMessageId];
if (!targetMessage) {
return false;
}
@@ -6294,6 +6341,68 @@ export function syncMesToSwipe(messageId = null) {
return true;
}
/**
* Syncs swipe data back to the message data at the given message ID (or the last message if no ID is given).
* If the swipe ID is not provided, the current swipe ID in the message object is used.
*
* If the swipe data is invalid in some way, this function will exit out without doing anything.
* @param {number?} [messageId=null] - The ID of the message to sync with the swipe data. If no ID is given, the last message is used.
* @param {number?} [swipeId=null] - The ID of the swipe to sync. If no ID is given, the current swipe ID in the message object is used.
* @returns {boolean} Whether the swipe data was successfully synced to the message
*/
export function syncSwipeToMes(messageId = null, swipeId = null) {
if (!chat.length) {
return false;
}
const targetMessageId = messageId ?? chat.length - 1;
if (targetMessageId >= chat.length || targetMessageId < 0) {
console.warn(`[syncSwipeToMes] Invalid message ID: ${messageId}`);
return false;
}
const targetMessage = chat[targetMessageId];
if (!targetMessage) {
return false;
}
if (swipeId !== null) {
if (isNaN(swipeId) || swipeId < 0) {
console.warn(`[syncSwipeToMes] Invalid swipe ID: ${swipeId}`);
return false;
}
targetMessage.swipe_id = swipeId;
}
// No swipe data there yet, exit out
if (typeof targetMessage.swipe_id !== 'number') {
return false;
}
// If swipes structure is invalid, exit out
if (!Array.isArray(targetMessage.swipe_info) || !Array.isArray(targetMessage.swipes)) {
return false;
}
const targetSwipeId = targetMessage.swipe_id;
if (!targetMessage.swipes[targetSwipeId] || !targetMessage.swipe_info[targetSwipeId]) {
console.warn(`[syncSwipeToMes] Invalid swipe ID: ${targetSwipeId}`);
return false;
}
const targetSwipeInfo = targetMessage.swipe_info[targetSwipeId];
if (typeof targetSwipeInfo !== 'object') {
return false;
}
targetMessage.mes = targetMessage.swipes[targetSwipeId];
targetMessage.send_date = targetSwipeInfo.send_date;
targetMessage.gen_started = targetSwipeInfo.gen_started;
targetMessage.gen_finished = targetSwipeInfo.gen_finished;
targetMessage.extra = structuredClone(targetSwipeInfo.extra);
return true;
}
/**
* Saves the image to the message object.
* @param {ParsedImage} img Image object
@@ -6527,6 +6636,8 @@ export async function renameCharacter(name = null, { silent = false, renameChats
await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
// Unload current character
setCharacterId(undefined);
// Reload characters list
await getCharacters();
@@ -6535,7 +6646,6 @@ export async function renameCharacter(name = null, { silent = false, renameChats
if (newChId !== -1) {
// Select the character after the renaming
setCharacterId(undefined);
await selectCharacterById(newChId);
// Async delay to update UI
@@ -6668,7 +6778,22 @@ export function saveChatDebounced() {
}, DEFAULT_SAVE_EDIT_TIMEOUT);
}
export async function saveChat(chatName, withMetadata, mesId) {
/**
* Saves the chat to the server.
* @param {object} [options] - Additional options.
* @param {string} [options.chatName] The name of the chat file to save to
* @param {object} [options.withMetadata] Additional metadata to save with the chat
* @param {number} [options.mesId] The message ID to save the chat up to
* @param {boolean} [options.force] Force the saving despire the integrity check result
*
* @returns {Promise<void>}
*/
export async function saveChat({ chatName, withMetadata, mesId, force = false } = {}) {
if (arguments.length > 0 && typeof arguments[0] !== 'object') {
console.trace('saveChat called with positional arguments. Please use an object instead.');
[chatName, withMetadata, mesId, force] = arguments;
}
const metadata = { ...chat_metadata, ...(withMetadata || {}) };
const fileName = chatName ?? characters[this_chid]?.chat;
@@ -6688,53 +6813,59 @@ export async function saveChat(chatName, withMetadata, mesId) {
toastr.error(t`Trying to save group chat with regular saveChat function. Aborting to prevent corruption.`);
throw new Error('Group chat saved from saveChat');
}
/*
if (item.is_user) {
//var str = item.mes.replace(`${name1}:`, `${name1}:`);
//chat[i].mes = str;
//chat[i].name = name1;
} else if (i !== chat.length - 1 && chat[i].swipe_id !== undefined) {
// delete chat[i].swipes;
// delete chat[i].swipe_id;
}
*/
});
const trimmed_chat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)
? chat.slice(0, parseInt(mesId) + 1)
: chat;
const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)
? chat.slice(0, Number(mesId) + 1)
: chat.slice();
var save_chat = [
const chatToSave = [
{
user_name: name1,
character_name: name2,
create_date: chat_create_date,
chat_metadata: metadata,
},
...trimmed_chat,
...trimmedChat,
];
return jQuery.ajax({
type: 'POST',
url: '/api/chats/save',
data: JSON.stringify({
ch_name: characters[this_chid].name,
file_name: fileName,
chat: save_chat,
avatar_url: characters[this_chid].avatar,
}),
beforeSend: function () {
},
cache: false,
dataType: 'json',
contentType: 'application/json',
success: function (data) { },
error: function (jqXHR, exception) {
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`);
console.log(exception);
console.log(jqXHR);
},
});
try {
const result = await fetch('/api/chats/save', {
method: 'POST',
cache: 'no-cache',
headers: getRequestHeaders(),
body: JSON.stringify({
ch_name: characters[this_chid].name,
file_name: fileName,
chat: chatToSave,
avatar_url: characters[this_chid].avatar,
force: force,
}),
});
if (result.ok) {
return;
}
const errorData = await result.json();
const isIntegrityError = errorData?.error === 'integrity' && !force;
if (!isIntegrityError) {
throw new Error(result.statusText);
}
const forceSaveConfirmed = await Popup.show.confirm(
t`ERROR: Chat integrity check failed.`,
t`Continuing the operation may result in data loss. Would you like to overwrite the chat file anyway? Pressing "NO" will cancel the save operation.`,
{ okButton: t`Yes, overwrite`, cancelButton: t`No, cancel` },
) === POPUP_RESULT.AFFIRMATIVE;
if (forceSaveConfirmed) {
await saveChat({ chatName, withMetadata, mesId, force: true });
}
} catch (error) {
console.error(error);
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`);
}
}
async function read_avatar_load(input) {
@@ -6861,14 +6992,14 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
*/
export async function unshallowCharacter(characterId) {
if (characterId === undefined) {
console.warn('Undefined character cannot be unshallowed');
console.debug('Undefined character cannot be unshallowed');
return;
}
/** @type {import('./scripts/char-data.js').v1CharData} */
const character = characters[characterId];
if (!character) {
console.warn('Character not found:', characterId);
console.debug('Character not found:', characterId);
return;
}
@@ -6879,7 +7010,7 @@ export async function unshallowCharacter(characterId) {
const avatar = character.avatar;
if (!avatar) {
console.warn('Character has no avatar field:', characterId);
console.debug('Character has no avatar field:', characterId);
return;
}
@@ -6911,6 +7042,9 @@ export async function getChat() {
} else {
chat_create_date = humanizedDateTime();
}
if (!chat_metadata['integrity']) {
chat_metadata['integrity'] = uuidv4();
}
await getChatResult();
eventSource.emit('chatLoaded', { detail: { id: this_chid, character: characters[this_chid] } });
@@ -7882,14 +8016,19 @@ export function select_rm_info(type, charId, previousCharId = null) {
}
}
export function select_selected_character(chid) {
/**
* Selects the right menu for displaying the character editor.
* @param {number|string} chid Character array index
* @param {object} [param1] Options for the switch
* @param {boolean} [param1.switchMenu=true] Whether to switch the menu
*/
export function select_selected_character(chid, { switchMenu = true } = {}) {
//character select
//console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')');
select_rm_create();
setMenuType('character_edit');
select_rm_create({ switchMenu });
switchMenu && setMenuType('character_edit');
$('#delete_button').css('display', 'flex');
$('#export_button').css('display', 'flex');
var display_name = characters[chid].name;
//create text poles
$('#rm_button_back').css('display', 'none');
@@ -7904,7 +8043,7 @@ export function select_selected_character(chid) {
// Don't update the navbar name if we're peeking the group member defs
if (!selected_group) {
$('#rm_button_selected_ch').children('h2').text(display_name);
$('#rm_button_selected_ch').children('h2').text(characters[chid].name);
}
$('#add_avatar_button').val('');
@@ -7935,22 +8074,20 @@ export function select_selected_character(chid) {
$('#chat_import_avatar_url').val(characters[chid].avatar);
$('#chat_import_character_name').val(characters[chid].name);
$('#character_json_data').val(characters[chid].json_data);
let this_avatar = default_avatar;
if (characters[chid].avatar != 'none') {
this_avatar = getThumbnailUrl('avatar', characters[chid].avatar);
}
updateFavButtonState(characters[chid].fav || characters[chid].fav == 'true');
$('#avatar_load_preview').attr('src', this_avatar);
$('#name_div').removeClass('displayBlock');
$('#name_div').addClass('displayNone');
$('#renameCharButton').css('display', '');
const avatarUrl = characters[chid].avatar != 'none' ? getThumbnailUrl('avatar', characters[chid].avatar) : default_avatar;
$('#avatar_load_preview').attr('src', avatarUrl);
$('.open_alternate_greetings').data('chid', chid);
$('#set_character_world').data('chid', chid);
setWorldInfoButtonClass(chid);
checkEmbeddedWorld(chid);
$('#name_div').removeClass('displayBlock');
$('#name_div').addClass('displayNone');
$('#renameCharButton').css('display', '');
$('#form_create').attr('actiontype', 'editcharacter');
$('.form_create_bottom_buttons_block .chat_lorebook_button').show();
@@ -7962,8 +8099,13 @@ export function select_selected_character(chid) {
saveSettingsDebounced();
}
function select_rm_create() {
setMenuType('create');
/**
* Selects the right menu for creating a new character.
* @param {object} [options] Options for the switch
* @param {boolean} [options.switchMenu=true] Whether to switch the menu
*/
function select_rm_create({ switchMenu = true } = {}) {
switchMenu && setMenuType('create');
//console.log('select_rm_Create() -- selected button: '+selected_button);
if (selected_button == 'create') {
@@ -7973,7 +8115,7 @@ function select_rm_create() {
}
}
selectRightMenuWithAnimation('rm_ch_create_block');
switchMenu && selectRightMenuWithAnimation('rm_ch_create_block');
$('#set_chat_scenario').hide();
$('#delete_button_div').css('display', 'none');
@@ -8293,10 +8435,9 @@ export async function deleteSwipe(swipeId = null) {
lastMessage.swipe_info.splice(swipeId, 1);
}
// Select the next swip, or the one before if it was the last one
// Select the next swipe, or the one before if it was the last one
const newSwipeId = Math.min(swipeId, lastMessage.swipes.length - 1);
lastMessage.swipe_id = newSwipeId;
lastMessage.mes = lastMessage.swipes[newSwipeId];
syncSwipeToMes(null, newSwipeId);
await saveChatConditional();
await reloadCurrentChat();
@@ -9196,6 +9337,17 @@ function swipe_right(_event, { source, repeated } = {}) {
}
}
/**
* @typedef {object} ConnectAPIMap
* @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
* @property {string?} [button] - CSS selector for the API button
* @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
* @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
*/
/**
* @type {Record<string, ConnectAPIMap>}
*/
export const CONNECT_API_MAP = {
// Default APIs not contined inside text gen / chat gen
'kobold': {
@@ -10417,7 +10569,7 @@ jQuery(async function () {
e.stopPropagation();
chat_file_for_del = $(this).attr('file_name');
console.debug('detected cross click for' + chat_file_for_del);
callPopup('<h3>Delete the Chat File?</h3>', 'del_chat');
callPopup('<h3>' + t`Delete the Chat File?` + '</h3>', 'del_chat');
});
$('#advanced_div').click(function () {
+1 -1
View File
@@ -4,7 +4,7 @@ import { DOMPurify, Popper } from '../lib.js';
import { event_types, eventSource, is_send_press, main_api, substituteParams } from '../script.js';
import { is_group_generating } from './group-chats.js';
import { Message, TokenHandler } from './openai.js';
import { Message, MessageCollection, TokenHandler } from './openai.js';
import { power_user } from './power-user.js';
import { debounce, waitUntilCondition, escapeHtml } from './utils.js';
import { debounce_timeout } from './constants.js';
+5
View File
@@ -17,6 +17,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
export { MODULE_NAME as NOTE_MODULE_NAME };
import { t } from './i18n.js';
import { MacrosParser } from './macros.js';
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
@@ -576,4 +577,8 @@ export function initAuthorsNote() {
`,
}));
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
MacrosParser.registerMacro('authorsNote', () => chat_metadata[metadata_keys.prompt] ?? '', t`The contents of the Author's Note`);
MacrosParser.registerMacro('charAuthorsNote', () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '', t`The contents of the Character Author's Note`);
MacrosParser.registerMacro('defaultAuthorsNote', () => extension_settings.note.default ?? '', t`The contents of the Default Author's Note`);
}
@@ -3,10 +3,8 @@ 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';
import { Popup, getTopmostModalLayer } from '../popup.js';
/**@readonly*/
/**@enum {Number}*/
@@ -1,4 +1,3 @@
import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
import { AutoCompleteOption } from './AutoCompleteOption.js';
@@ -1,19 +1,18 @@
import { SlashCommand } from '../slash-commands/SlashCommand.js';
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
export class AutoCompleteOption {
/**@type {string}*/ name;
/**@type {string}*/ typeIcon;
/**@type {string}*/ type;
/**@type {number}*/ nameOffset = 0;
/**@type {AutoCompleteFuzzyScore}*/ score;
/**@type {string}*/ replacer;
/**@type {HTMLElement}*/ dom;
/**@type {(input:string)=>boolean}*/ matchProvider;
/**@type {(input:string)=>string}*/ valueProvider;
/**@type {boolean}*/ makeSelectable = false;
/** @type {string} */ name;
/** @type {string} */ typeIcon;
/** @type {string} */ type;
/** @type {number} */ nameOffset = 0;
/** @type {AutoCompleteFuzzyScore} */ score;
/** @type {string} */ replacer;
/** @type {HTMLElement} */ dom;
/** @type {(input:string)=>boolean} */ matchProvider;
/** @type {(input:string)=>string} */ valueProvider;
/** @type {boolean} */ makeSelectable = false;
/**
+2 -1
View File
@@ -5,6 +5,7 @@ import { saveMetadataDebounced } from './extensions.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { flashHighlight, stringFormat } from './utils.js';
import { t } from './i18n.js';
const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
@@ -243,7 +244,7 @@ async function getNewBackgroundName(referenceElement) {
const fileExtension = oldBg.split('.').pop();
const fileNameBase = isCustom ? oldBg.split('/').pop() : oldBg;
const oldBgExtensionless = fileNameBase.replace(`.${fileExtension}`, '');
const newBgExtensionless = await callPopup('<h3>Enter new background name:</h3>', 'input', oldBgExtensionless);
const newBgExtensionless = await callPopup('<h3>' + t`Enter new background name:` + '</h3>', 'input', oldBgExtensionless);
if (!newBgExtensionless) {
console.debug('no new_bg_extensionless');
+4 -5
View File
@@ -1,7 +1,6 @@
import {
characters,
saveChat,
system_messages,
system_message_types,
this_chid,
openCharacterChat,
@@ -13,7 +12,7 @@ import {
saveChatConditional,
saveItemizedPrompts,
} from '../script.js';
import { humanizedDateTime, getMessageTimeStamp } from './RossAscends-mods.js';
import { humanizedDateTime } from './RossAscends-mods.js';
import {
getGroupPastChats,
group_activation_strategy,
@@ -156,7 +155,7 @@ export async function createBranch(mesId) {
if (selected_group) {
await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);
} else {
await saveChat(name, newMetadata, mesId);
await saveChat({ chatName: name, withMetadata: newMetadata, mesId });
}
// append to branches list if it exists
// otherwise create it
@@ -212,7 +211,7 @@ export async function createNewBookmark(mesId, { forceName = null } = {}) {
if (selected_group) {
await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);
} else {
await saveChat(name, newMetadata, mesId);
await saveChat({ chatName: name, withMetadata: newMetadata, mesId });
}
lastMes.extra['bookmark_link'] = name;
@@ -358,7 +357,7 @@ export async function convertSoloToGroupChat() {
// Click on the freshly selected group to open it
await openGroupById(group.id);
toastr.success('The chat has been successfully converted!');
toastr.success(t`The chat has been successfully converted!`);
}
/**
+237 -37
View File
@@ -1,20 +1,20 @@
import { getPresetManager } from './preset-manager.js';
import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
import { getTextGenServer } from './textgen-settings.js';
import { extractReasoningFromData } from './reasoning.js';
import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';
// #region Type Definitions
/**
* @typedef {Object} TextCompletionRequestBase
* @property {string} prompt - The text prompt for completion
* @property {number} max_tokens - Maximum number of tokens to generate
* @property {string} [model] - Optional model name
* @property {string} api_type - Type of API to use
* @property {string} [api_server] - Optional API server URL
* @property {number} [temperature] - Optional temperature parameter
* @property {number} [min_p] - Optional min_p parameter
*/
/** @typedef {Record<string, any> & TextCompletionRequestBase} TextCompletionRequest */
/**
* @typedef {Object} TextCompletionPayloadBase
* @property {string} prompt - The text prompt for completion
@@ -41,9 +41,17 @@ import { getTextGenServer } from './textgen-settings.js';
* @property {string} chat_completion_source - Source provider for chat completion
* @property {number} max_tokens - Maximum number of tokens to generate
* @property {number} [temperature] - Optional temperature parameter for response randomness
* @property {string} [custom_url] - Optional custom URL for chat completion
*/
/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
/**
* @typedef {Object} ExtractedData
* @property {string} content - Extracted content.
* @property {string} reasoning - Extracted reasoning.
*/
// #endregion
/**
@@ -53,11 +61,11 @@ export class TextCompletionService {
static TYPE = 'textgenerationwebui';
/**
* @param {TextCompletionRequest} custom
* @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom
* @returns {TextCompletionPayload}
*/
static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, ...props }) {
return {
static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
const payload = {
...props,
prompt,
max_tokens,
@@ -66,15 +74,25 @@ export class TextCompletionService {
api_type,
api_server: api_server ?? getTextGenServer(api_type),
temperature,
min_p,
stream: false,
};
// Remove undefined values to avoid API errors
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
return payload;
}
/**
* Sends a text completion request to the specified server
* @param {TextCompletionPayload} data Request data
* @param {boolean?} extractData Extract message from the response. Default true
* @returns {Promise<string | any>} Extracted data or the raw response
* @returns {Promise<ExtractedData | any>} Extracted data or the raw response
* @throws {Error}
*/
static async sendRequest(data, extractData = true) {
@@ -91,31 +109,150 @@ export class TextCompletionService {
throw json;
}
return extractData ? extractMessageFromData(json, this.TYPE) : json;
if (!extractData) {
return json;
}
return {
content: extractMessageFromData(json, this.TYPE),
reasoning: extractReasoningFromData(json, {
mainApi: this.TYPE,
textGenType: data.api_type,
ignoreShowThoughts: true,
}),
};
}
/**
* @param {string} presetName
* @param {TextCompletionRequest} custom
* @param {boolean?} extractData Extract message from the response. Default true
* @returns {Promise<string | any>} Extracted data or the raw response
* Process and send a text completion request with optional preset & instruct
* @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom
* @param {Object} options - Configuration options
* @param {string?} [options.presetName] - Name of the preset to use for generation settings
* @param {string?} [options.instructName] - Name of instruct preset for message formatting
* @param {boolean} extractData - Whether to extract structured data from response
* @returns {Promise<ExtractedData | any>} Extracted data or the raw response
* @throws {Error}
*/
static async sendRequestWithPreset(presetName, custom, extractData = true) {
const presetManager = getPresetManager(this.TYPE);
if (!presetManager) {
throw new Error('Preset manager not found');
static async processRequest(
custom,
options = {},
extractData = true,
) {
const { presetName, instructName } = options;
let requestData = { ...custom };
const prompt = custom.prompt;
// Apply generation preset if specified
if (presetName) {
const presetManager = getPresetManager(this.TYPE);
if (presetManager) {
const preset = presetManager.getCompletionPresetByName(presetName);
if (preset) {
// Convert preset to payload and merge with custom parameters
const presetPayload = this.presetToGeneratePayload(preset, {});
requestData = { ...presetPayload, ...requestData };
} else {
console.warn(`Preset "${presetName}" not found, continuing with default settings`);
}
} else {
console.warn('Preset manager not found, continuing with default settings');
}
}
const preset = presetManager.getCompletionPresetByName(presetName);
if (!preset) {
throw new Error('Preset not found');
// Handle instruct formatting if requested
if (Array.isArray(prompt) && instructName) {
const instructPresetManager = getPresetManager('instruct');
let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
if (instructPreset) {
// Clone the preset to avoid modifying the original
instructPreset = structuredClone(instructPreset);
instructPreset.macro = false;
instructPreset.names_behavior = names_behavior_types.NONE;
// Format messages using instruct formatting
const formattedMessages = [];
for (const message of prompt) {
let messageContent = message.content;
if (!message.ignoreInstruct) {
messageContent = formatInstructModeChat(
message.role,
message.content,
message.role === 'user',
false,
undefined,
undefined,
undefined,
undefined,
instructPreset,
);
// Add prompt formatting for the last message
if (message === prompt[prompt.length - 1]) {
messageContent += formatInstructModePrompt(
undefined,
false,
undefined,
undefined,
undefined,
false,
false,
instructPreset,
);
}
}
formattedMessages.push(messageContent);
}
requestData.prompt = formattedMessages.join('');
if (instructPreset.output_suffix) {
requestData.stop = [instructPreset.output_suffix];
requestData.stopping_strings = [instructPreset.output_suffix];
}
} else {
console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
requestData.prompt = prompt.map(x => x.content).join('\n\n');
}
} else if (typeof prompt === 'string') {
requestData.prompt = prompt;
} else {
requestData.prompt = prompt.map(x => x.content).join('\n\n');
}
const data = this.createRequestData({ ...preset, ...custom });
// @ts-ignore
const data = this.createRequestData(requestData);
return await this.sendRequest(data, extractData);
}
/**
* Converts a preset to a valid text completion payload.
* Only supports temperature.
* @param {Object} preset - The preset configuration
* @param {Object} customPreset - Additional parameters to override preset values
* @returns {Object} - Formatted payload for text completion API
*/
static presetToGeneratePayload(preset, customPreset = {}) {
if (!preset || typeof preset !== 'object') {
throw new Error('Invalid preset: must be an object');
}
// Merge preset with custom parameters
const settings = { ...preset, ...customPreset };
// Initialize base payload with common parameters
let payload = {
'temperature': settings.temp ? Number(settings.temp) : undefined,
'min_p': settings.min_p ? Number(settings.min_p) : undefined,
};
// Remove undefined values to avoid API errors
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
return payload;
}
}
/**
@@ -128,23 +265,33 @@ export class ChatCompletionService {
* @param {ChatCompletionPayload} custom
* @returns {ChatCompletionPayload}
*/
static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) {
return {
static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) {
const payload = {
...props,
messages,
model,
chat_completion_source,
max_tokens,
temperature,
custom_url,
stream: false,
};
// Remove undefined values to avoid API errors
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
return payload;
}
/**
* Sends a chat completion request
* @param {ChatCompletionPayload} data Request data
* @param {boolean?} extractData Extract message from the response. Default true
* @returns {Promise<string | any>} Extracted data or the raw response
* @returns {Promise<ExtractedData | any>} Extracted data or the raw response
* @throws {Error}
*/
static async sendRequest(data, extractData = true) {
@@ -161,29 +308,82 @@ export class ChatCompletionService {
throw json;
}
return extractData ? extractMessageFromData(json, this.TYPE) : json;
if (!extractData) {
return json;
}
return {
content: extractMessageFromData(json, this.TYPE),
reasoning: extractReasoningFromData(json, {
mainApi: this.TYPE,
textGenType: data.chat_completion_source,
ignoreShowThoughts: true,
}),
};
}
/**
* @param {string} presetName
* Process and send a chat completion request with optional preset
* @param {ChatCompletionPayload} custom
* @param {boolean} extractData Extract message from the response. Default true
* @returns {Promise<string | any>} Extracted data or the raw response
* @param {Object} options - Configuration options
* @param {string?} [options.presetName] - Name of the preset to use for generation settings
* @param {boolean} extractData - Whether to extract structured data from response
* @returns {Promise<ExtractedData | any>} Extracted data or the raw response
* @throws {Error}
*/
static async sendRequestWithPreset(presetName, custom, extractData = true) {
const presetManager = getPresetManager(this.TYPE);
if (!presetManager) {
throw new Error('Preset manager not found');
static async processRequest(custom, options, extractData = true) {
const { presetName } = options;
let requestData = { ...custom };
// Apply generation preset if specified
if (presetName) {
const presetManager = getPresetManager(this.TYPE);
if (presetManager) {
const preset = presetManager.getCompletionPresetByName(presetName);
if (preset) {
// Convert preset to payload and merge with custom parameters
const presetPayload = this.presetToGeneratePayload(preset, {});
requestData = { ...presetPayload, ...requestData };
} else {
console.warn(`Preset "${presetName}" not found, continuing with default settings`);
}
} else {
console.warn('Preset manager not found, continuing with default settings');
}
}
const preset = presetManager.getCompletionPresetByName(presetName);
if (!preset) {
throw new Error('Preset not found');
}
const data = this.createRequestData({ ...preset, ...custom });
const data = this.createRequestData(requestData);
return await this.sendRequest(data, extractData);
}
/**
* Converts a preset to a valid chat completion payload
* Only supports temperature.
* @param {Object} preset - The preset configuration
* @param {Object} customParams - Additional parameters to override preset values
* @returns {Object} - Formatted payload for chat completion API
*/
static presetToGeneratePayload(preset, customParams = {}) {
if (!preset || typeof preset !== 'object') {
throw new Error('Invalid preset: must be an object');
}
// Merge preset with custom parameters
const settings = { ...preset, ...customParams };
// Initialize base payload with common parameters
const payload = {
temperature: settings.temperature ? Number(settings.temperature) : undefined,
};
// Remove undefined values to avoid API errors
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
return payload;
}
}
+1 -1
View File
@@ -424,7 +424,7 @@ jQuery(async () => {
installHintButton.on('click', async function () {
const installButton = $('#third_party_extension_button');
flashHighlight(installButton, 5000);
toastr.info('Click the flashing button to install extensions.', 'How to install extensions?');
toastr.info(t`Click the flashing button to install extensions.`, t`How to install extensions?`);
});
const connectButton = windowHtml.find('#assets-connect-button');
@@ -1,4 +1,4 @@
<div id="attachFile" class="list-group-item flex-container flexGap5" title="Attach a file or image to a current chat.">
<div id="attachFile" class="list-group-item flex-container flexGap5" data-i18n="[title]Attach a file or image to a current chat." title="Attach a file or image to a current chat.">
<div class="fa-fw fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
<span data-i18n="Attach a File">Attach a File</span>
</div>
@@ -42,6 +42,9 @@
<option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option>
<option data-type="mistral" value="pixtral-large-latest">pixtral-large-latest</option>
<option data-type="mistral" value="pixtral-large-2411">pixtral-large-2411</option>
<option data-type="mistral" value="mistral-large-pixtral-2411">mistral-large-pixtral-2411</option>
<option data-type="mistral" value="mistral-small-2503">mistral-small-2503</option>
<option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>
<option data-type="zerooneai" value="yi-vision">yi-vision</option>
<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>
@@ -1,4 +1,4 @@
import { Fuse } from '../../../lib.js';
import { DOMPurify, Fuse } from '../../../lib.js';
import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
@@ -39,6 +39,7 @@ const CC_COMMANDS = [
'proxy',
'stop-strings',
'start-reply-with',
'reasoning-template',
];
const TC_COMMANDS = [
@@ -54,6 +55,7 @@ const TC_COMMANDS = [
'tokenizer',
'stop-strings',
'start-reply-with',
'reasoning-template',
];
const FANCY_NAMES = {
@@ -70,6 +72,7 @@ const FANCY_NAMES = {
'tokenizer': 'Tokenizer',
'stop-strings': 'Custom Stopping Strings',
'start-reply-with': 'Start Reply With',
'reasoning-template': 'Reasoning Template',
};
/**
@@ -154,6 +157,7 @@ const profilesProvider = () => [
* @property {string} [tokenizer] Tokenizer
* @property {string} [stop-strings] Custom Stopping Strings
* @property {string} [start-reply-with] Start Reply With
* @property {string} [reasoning-template] Reasoning Template
* @property {string[]} [exclude] Commands to exclude
*/
@@ -267,11 +271,16 @@ async function createConnectionProfile(forceName = null) {
});
const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
let name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
// If it's cancelled, it will be false
if (!name) {
return null;
}
name = DOMPurify.sanitize(String(name));
if (!name) {
toastr.error('Name cannot be empty.');
return null;
}
if (isNameTaken(name) || name === NONE) {
toastr.error('A profile with the same name already exists.');
@@ -303,7 +312,8 @@ async function deleteConnectionProfile() {
return;
}
const name = extension_settings.connectionManager.profiles[index].name;
const profile = extension_settings.connectionManager.profiles[index];
const name = profile.name;
const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);
if (!confirm) {
@@ -313,6 +323,8 @@ async function deleteConnectionProfile() {
extension_settings.connectionManager.profiles.splice(index, 1);
extension_settings.connectionManager.selectedProfile = null;
saveSettingsDebounced();
await eventSource.emit(event_types.CONNECTION_PROFILE_DELETED, profile);
}
/**
@@ -512,6 +524,7 @@ async function renderDetailsContent(detailsContent) {
saveSettingsDebounced();
renderConnectionProfiles(profiles);
await renderDetailsContent(detailsContent);
await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
});
@@ -523,9 +536,11 @@ async function renderDetailsContent(detailsContent) {
console.log('No profile selected');
return;
}
const oldProfile = structuredClone(profile);
await updateConnectionProfile(profile);
await renderDetailsContent(detailsContent);
saveSettingsDebounced();
await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
toastr.success('Connection profile updated', '', { timeOut: 1500 });
});
@@ -559,7 +574,7 @@ async function renderDetailsContent(detailsContent) {
return acc;
}, {});
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
let newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
rows: 2,
customButtons: [{
text: t`Save and Update`,
@@ -571,9 +586,15 @@ async function renderDetailsContent(detailsContent) {
}],
});
// If it's cancelled, it will be false
if (!newName) {
return;
}
newName = DOMPurify.sanitize(String(newName));
if (!newName) {
toastr.error('Name cannot be empty.');
return;
}
if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
toastr.error('A profile with the same name already exists.');
@@ -584,6 +605,7 @@ async function renderDetailsContent(detailsContent) {
return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
}).get();
const oldProfile = structuredClone(profile);
if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
profile.exclude = newExcludeList;
for (const command of newExcludeList) {
@@ -598,10 +620,11 @@ async function renderDetailsContent(detailsContent) {
if (profile.name !== newName) {
toastr.success('Connection profile renamed.');
profile.name = String(newName);
profile.name = newName;
}
saveSettingsDebounced();
await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
renderConnectionProfiles(profiles);
await renderDetailsContent(detailsContent);
});
@@ -704,6 +727,7 @@ async function renderDetailsContent(detailsContent) {
saveSettingsDebounced();
renderConnectionProfiles(profiles);
await renderDetailsContent(detailsContent);
await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
return profile.name;
},
}));
@@ -718,9 +742,11 @@ async function renderDetailsContent(detailsContent) {
toastr.warning('No profile selected.');
return '';
}
const oldProfile = structuredClone(profile);
await updateConnectionProfile(profile);
await renderDetailsContent(detailsContent);
saveSettingsDebounced();
await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
return profile.name;
},
}));
+59 -17
View File
@@ -4,7 +4,7 @@ import { characters, eventSource, event_types, generateRaw, getRequestHeaders, m
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar, isFalseBoolean } from '../../utils.js';
import { hideMutedSprites, selected_group } from '../../group-chats.js';
import { isJsonSchemaSupported } from '../../textgen-settings.js';
import { debounce_timeout } from '../../constants.js';
@@ -17,6 +17,7 @@ import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandRetur
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
import { Popup, POPUP_RESULT } from '../../popup.js';
import { t } from '../../i18n.js';
import { removeReasoningFromString } from '../../reasoning.js';
export { MODULE_NAME };
/**
@@ -678,7 +679,7 @@ async function setSpriteFolderCommand(_, folder) {
return '';
}
async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {
async function classifyCallback(/** @type {{api: string?, filter: string?, prompt: string?}} */ { api = null, filter = null, prompt = null }, text) {
if (!text) {
toastr.error('No text provided');
return '';
@@ -689,13 +690,14 @@ async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ {
}
const expressionApi = EXPRESSION_API[api] || extension_settings.expressions.api;
const filterAvailable = !isFalseBoolean(filter);
if (!modules.includes('classify') && expressionApi == EXPRESSION_API.extras) {
toastr.warning('Text classification is disabled or not available');
return '';
}
const label = await getExpressionLabel(text, expressionApi, { customPrompt: prompt });
const label = await getExpressionLabel(text, expressionApi, { filterAvailable: filterAvailable, customPrompt: prompt });
console.debug(`Classification result for "${text}": ${label}`);
return label;
}
@@ -928,6 +930,9 @@ function parseLlmResponse(emotionResponse, labels) {
return response;
} catch {
// Clean possible reasoning from response
emotionResponse = removeReasoningFromString(emotionResponse);
const fuse = new Fuse(labels, { includeScore: true });
console.debug('Using fuzzy search in labels:', labels);
const result = fuse.search(emotionResponse);
@@ -988,10 +993,11 @@ function onTextGenSettingsReady(args) {
* @param {string} text - The text to classify and retrieve the expression label for.
* @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification.
* @param {object} [options={}] - Optional arguments.
* @param {boolean?} [options.filterAvailable=null] - Whether to filter available expressions. If not specified, uses the extension setting.
* @param {string?} [options.customPrompt=null] - The custom prompt to use for classification.
* @returns {Promise<string?>} - The label of the expression.
*/
export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) {
export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { filterAvailable = null, customPrompt = null } = {}) {
// Return if text is undefined, saving a costly fetch request
if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) {
return extension_settings.expressions.fallback_expression;
@@ -1003,6 +1009,11 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
text = sampleClassifyText(text);
filterAvailable ??= extension_settings.expressions.filterAvailable;
if (filterAvailable && ![EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(expressionsApi)) {
console.debug('Filter available is only supported for LLM and WebLLM expressions');
}
try {
switch (expressionsApi) {
// Local BERT pipeline
@@ -1027,7 +1038,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
return extension_settings.expressions.fallback_expression;
}
const expressionsList = await getExpressionsList();
const expressionsList = await getExpressionsList({ filterAvailable: filterAvailable });
const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
@@ -1040,7 +1051,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
return extension_settings.expressions.fallback_expression;
}
const expressionsList = await getExpressionsList();
const expressionsList = await getExpressionsList({ filterAvailable: filterAvailable });
const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
const messages = [
{ role: 'user', content: text + '\n\n' + prompt },
@@ -1320,12 +1331,28 @@ function getCachedExpressions() {
return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);
}
export async function getExpressionsList() {
// Return cached list if available
if (Array.isArray(expressionsList)) {
return getCachedExpressions();
export async function getExpressionsList({ filterAvailable = false } = {}) {
// If there is no cached list, load and cache it
if (!Array.isArray(expressionsList)) {
expressionsList = await resolveExpressionsList();
}
const expressions = getCachedExpressions();
// Filtering is only available for llm and webllm APIs
if (!filterAvailable || ![EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)) {
return expressions;
}
// Get expressions with available sprites
const currentLastMessage = selected_group ? getLastCharacterMessage() : null;
const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage?.name);
return expressions.filter(label => {
const expression = spriteCache[spriteFolderName]?.find(x => x.label === label);
return (expression?.files.length ?? 0) > 0;
});
/**
* Returns the list of expressions from the API or fallback in offline mode.
* @returns {Promise<string[]>}
@@ -1372,9 +1399,6 @@ export async function getExpressionsList() {
expressionsList = DEFAULT_EXPRESSIONS.slice();
return expressionsList;
}
const result = await resolveExpressionsList();
return [...result, ...extension_settings.expressions.custom].filter(onlyUnique);
}
/**
@@ -1810,7 +1834,7 @@ async function onClickExpressionUpload(event) {
}
}
} else {
spriteName = withoutExtension(clickedFileName);
spriteName = withoutExtension(expression);
}
if (!spriteName) {
@@ -2102,6 +2126,10 @@ function migrateSettings() {
extension_settings.expressions.rerollIfSame = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#expressions_filter_available').prop('checked', extension_settings.expressions.filterAvailable).on('input', function () {
extension_settings.expressions.filterAvailable = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);
$(document).on('dragstart', '.expression', (e) => {
e.preventDefault();
@@ -2154,7 +2182,7 @@ function migrateSettings() {
imgElement.src = '';
}
setExpressionOverrideHtml();
setExpressionOverrideHtml(true); // force-clear, as the character might not have an override defined
if (isVisualNovelMode()) {
$('#visual-novel-wrapper').empty();
@@ -2279,13 +2307,13 @@ function migrateSettings() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'expression-list',
aliases: ['expressions'],
/** @type {(args: {return: string}) => Promise<string>} */
/** @type {(args: {return: string, filter: string}) => Promise<string>} */
callback: async (args) => {
let returnType =
/** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
(args.return);
const list = await getExpressionsList();
const list = await getExpressionsList({ filterAvailable: !isFalseBoolean(args.filter) });
return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
},
@@ -2298,6 +2326,13 @@ function migrateSettings() {
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'filter',
description: 'Filter the list to only include expressions that have available sprites for the current character.',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumList: commonEnumProviders.boolean('trueFalse')(),
defaultValue: 'true',
}),
],
returns: 'The comma-separated list of available expressions, including custom expressions.',
helpString: 'Returns a list of available expressions, including custom expressions.',
@@ -2313,6 +2348,13 @@ function migrateSettings() {
typeList: [ARGUMENT_TYPE.STRING],
enumList: Object.keys(EXPRESSION_API).map(api => new SlashCommandEnumValue(api, null, enumTypes.enum)),
}),
SlashCommandNamedArgument.fromProps({
name: 'filter',
description: 'Filter the list to only include expressions that have available sprites for the current character.',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumList: commonEnumProviders.boolean('trueFalse')(),
defaultValue: 'true',
}),
SlashCommandNamedArgument.fromProps({
name: 'prompt',
description: 'Custom prompt for classification. Only relevant if Classifier API is set to LLM.',
@@ -29,7 +29,11 @@
</select>
</div>
<div class="expression_llm_prompt_block m-b-1 m-t-1">
<label for="expression_llm_prompt" class="title_restorable">
<label class="checkbox_label" for="expressions_filter_available" title="When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them." data-i18n="[title]When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.">
<input id="expressions_filter_available" type="checkbox">
<span data-i18n="Filter expressions for available sprites">Filter expressions for available sprites</span>
</label>
<label for="expression_llm_prompt" class="title_restorable m-t-1">
<span data-i18n="LLM Prompt">LLM Prompt</span>
<div id="expression_llm_prompt_restore" title="Restore default value" class="right_menu_button">
<i class="fa-solid fa-clock-rotate-left fa-sm"></i>
@@ -1,21 +1,18 @@
// eslint-disable-next-line no-unused-vars
import { QuickReply } from '../src/QuickReply.js';
import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js';
import { QuickReplySet } from '../src/QuickReplySet.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from '../src/QuickReplySettings.js';
// eslint-disable-next-line no-unused-vars
import { SettingsUi } from '../src/ui/SettingsUi.js';
import { onlyUnique } from '../../../utils.js';
export class QuickReplyApi {
/**@type {QuickReplySettings}*/ settings;
/**@type {SettingsUi}*/ settingsUi;
/** @type {QuickReplySettings} */ settings;
/** @type {SettingsUi} */ settingsUi;
constructor(/**@type {QuickReplySettings}*/settings, /**@type {SettingsUi}*/settingsUi) {
constructor(/** @type {QuickReplySettings} */settings, /** @type {SettingsUi} */settingsUi) {
this.settings = settings;
this.settingsUi = settingsUi;
}
@@ -1,18 +1,16 @@
import { warn } from '../index.js';
// eslint-disable-next-line no-unused-vars
import { QuickReply } from './QuickReply.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from './QuickReplySettings.js';
export class AutoExecuteHandler {
/**@type {QuickReplySettings}*/ settings;
/** @type {QuickReplySettings} */ settings;
/**@type {Boolean[]}*/ preventAutoExecuteStack = [];
/** @type {Boolean[]}*/ preventAutoExecuteStack = [];
constructor(/**@type {QuickReplySettings}*/settings) {
constructor(/** @type {QuickReplySettings} */settings) {
this.settings = settings;
}
@@ -24,7 +22,7 @@ export class AutoExecuteHandler {
async performAutoExecute(/**@type {QuickReply[]}*/qrList) {
async performAutoExecute(/** @type {QuickReply[]} */qrList) {
for (const qr of qrList) {
this.preventAutoExecuteStack.push(qr.preventAutoExecute);
try {
@@ -49,7 +49,7 @@ export class QuickReply {
/**@type {string}*/ automationId = '';
/**@type {function}*/ onExecute;
/**@type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>}*/ onDebug;
/** @type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>} */ onDebug;
/**@type {function}*/ onDelete;
/**@type {function}*/ onUpdate;
/**@type {function}*/ onInsertBefore;
@@ -635,7 +635,6 @@ export class QuickReply {
}, { passive:true });
const getLineStart = ()=>{
const start = message.selectionStart;
const end = message.selectionEnd;
let lineStart;
if (start == 0 || message.value[start - 1] == '\n') {
// cursor is already at beginning of line
@@ -701,7 +700,6 @@ export class QuickReply {
} else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) {
// new line, keep indent
const start = message.selectionStart;
const end = message.selectionEnd;
let lineStart = getLineStart();
const indent = /^([^\S\n]*)/.exec(message.value.slice(lineStart))[1] ?? '';
if (indent.length) {
@@ -8,18 +8,17 @@ import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashC
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { isTrueBoolean } from '../../../utils.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplyApi } from '../api/QuickReplyApi.js';
import { QuickReply } from './QuickReply.js';
import { QuickReplySet } from './QuickReplySet.js';
export class SlashCommandHandler {
/**@type {QuickReplyApi}*/ api;
/** @type {QuickReplyApi} */ api;
constructor(/**@type {QuickReplyApi}*/api) {
constructor(/** @type {QuickReplyApi} */api) {
this.api = api;
}
@@ -27,7 +26,7 @@ export class SlashCommandHandler {
init() {
function getExecutionIcons(/**@type {QuickReply} */ qr) {
function getExecutionIcons(/** @type {QuickReply} */ qr) {
let icons = '';
if (qr.preventAutoExecute) icons += '🚫';
if (qr.isHidden) icons += '👁️';
@@ -1,11 +1,10 @@
import { animation_duration } from '../../../../../script.js';
import { dragElement } from '../../../../RossAscends-mods.js';
import { loadMovingUIState } from '../../../../power-user.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from '../QuickReplySettings.js';
export class ButtonUi {
/**@type {QuickReplySettings}*/ settings;
/** @type {QuickReplySettings} */ settings;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLElement}*/ popoutDom;
@@ -3,14 +3,13 @@ import { getSortableDelay } from '../../../../utils.js';
import { log, warn } from '../../index.js';
import { QuickReply } from '../QuickReply.js';
import { QuickReplySet } from '../QuickReplySet.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from '../QuickReplySettings.js';
export class SettingsUi {
/**@type {QuickReplySettings}*/ settings;
/** @type {QuickReplySettings} */ settings;
/**@type {HTMLElement}*/ template;
/**@type {HTMLElement}*/ dom;
/** @type {HTMLElement} */ template;
/** @type {HTMLElement} */ dom;
/**@type {HTMLInputElement}*/ isEnabled;
/**@type {HTMLInputElement}*/ isCombined;
@@ -1,5 +1,4 @@
import { QuickReply } from '../../QuickReply.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplySet } from '../../QuickReplySet.js';
import { MenuHeader } from './MenuHeader.js';
import { MenuItem } from './MenuItem.js';
+3 -4
View File
@@ -18,9 +18,8 @@
<div id="regex_info_block_wrapper">
<div id="regex_info_block" class="info-block"></div>
<!-- TODO replace 3rd-party link with our own docs when it's done -->
<a id="regex_info_block_flags_hint" href="http://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#advanced_searching_with_flags" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-circle-info" title="Click here to learn more about regex flags."></i>
<a id="regex_info_block_flags_hint" href="https://docs.sillytavern.app/extensions/regex/#flags" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-circle-info" data-i18n="[title]ext_regex_flags_help" title="Click here to learn more about regex flags."></i>
</a>
</div>
@@ -148,7 +147,7 @@
</label>
<span>
<small data-i18n="ext_regex_other_options" data-i18n="Ephemerality">Ephemerality</small>
<span class="fa-solid fa-circle-question note-link-span" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
<span class="fa-solid fa-circle-question note-link-span" data-i18n="[title]ext_regex_other_options_desc" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
</span>
<label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_visual_desc" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
<input type="checkbox" name="only_format_display" />
+1 -1
View File
@@ -398,7 +398,7 @@ function runRegexCallback(args, value) {
for (const script of scripts) {
if (script.scriptName.toLowerCase() === scriptName.toLowerCase()) {
if (script.disabled) {
toastr.warning(`Regex script "${scriptName}" is disabled.`);
toastr.warning(t`Regex script "${scriptName}" is disabled.`);
return value;
}
+309 -1
View File
@@ -1,5 +1,6 @@
import { getRequestHeaders } from '../../script.js';
import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
import { t } from '../i18n.js';
import { oai_settings } from '../openai.js';
import { SECRET_KEYS, secret_state } from '../secrets.js';
import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@@ -273,3 +274,310 @@ export async function getWebLlmContextSize() {
const model = await engine.getCurrentModelInfo();
return model?.context_size;
}
/**
* It uses the profiles to send a generate request to the API. Doesn't support streaming.
*/
export class ConnectionManagerRequestService {
static defaultSendRequestParams = {
extractData: true,
includePreset: true,
includeInstruct: true,
};
static getAllowedTypes() {
return {
openai: t`Chat Completion`,
textgenerationwebui: t`Text Completion`,
};
}
/**
* @param {string} profileId
* @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
* @param {number} maxTokens
* @param {{extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true
* @returns {Promise<import('../custom-request.js').ExtractedData | any>} Extracted data or the raw response
*/
static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
const { extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };
const context = SillyTavern.getContext();
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
throw new Error('Connection Manager is not available');
}
const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
const selectedApiMap = this.validateProfile(profile);
try {
switch (selectedApiMap.selected) {
case 'openai': {
if (!selectedApiMap.source) {
throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);
}
const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
return await context.ChatCompletionService.processRequest({
messages,
max_tokens: maxTokens,
model: profile.model,
chat_completion_source: selectedApiMap.source,
custom_url: profile['api-url'],
}, {
presetName: includePreset ? profile.preset : undefined,
}, extractData);
}
case 'textgenerationwebui': {
if (!selectedApiMap.type) {
throw new Error(`API type ${selectedApiMap.selected} does not support text completions`);
}
return await context.TextCompletionService.processRequest({
prompt,
max_tokens: maxTokens,
model: profile.model,
api_type: selectedApiMap.type,
api_server: profile['api-url'],
}, {
instructName: includeInstruct ? profile.instruct : undefined,
presetName: includePreset ? profile.preset : undefined,
}, extractData);
}
default: {
throw new Error(`Unknown API type ${selectedApiMap.selected}`);
}
}
} catch (error) {
throw new Error('API request failed', { cause: error });
}
}
/**
* Respects allowed types.
* @returns {import('./connection-manager/index.js').ConnectionProfile[]}
*/
static getSupportedProfiles() {
const context = SillyTavern.getContext();
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
throw new Error('Connection Manager is not available');
}
const profiles = context.extensionSettings.connectionManager.profiles;
return profiles.filter((p) => this.isProfileSupported(p));
}
/**
* @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
* @returns {boolean}
*/
static isProfileSupported(profile) {
if (!profile) {
return false;
}
const apiMap = CONNECT_API_MAP[profile.api];
if (!Object.hasOwn(this.getAllowedTypes(), apiMap.selected)) {
return false;
}
// Some providers not need model, like koboldcpp. But I don't want to check by provider.
switch (apiMap.selected) {
case 'openai':
return !!apiMap.source;
case 'textgenerationwebui':
return !!apiMap.type;
}
return false;
}
/**
* @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
* @return {import('../../script.js').ConnectAPIMap}
* @throws {Error}
*/
static validateProfile(profile) {
if (!profile) {
throw new Error('Could not find profile.');
}
if (!profile.api) {
throw new Error('Select a connection profile that has an API');
}
const context = SillyTavern.getContext();
const selectedApiMap = context.CONNECT_API_MAP[profile.api];
if (!selectedApiMap) {
throw new Error(`Unknown API type ${profile.api}`);
}
if (!Object.hasOwn(this.getAllowedTypes(), selectedApiMap.selected)) {
throw new Error(`API type ${selectedApiMap.selected} is not supported. Supported types: ${Object.values(this.getAllowedTypes()).join(', ')}`);
}
return selectedApiMap;
}
/**
* Create profiles dropdown and updates select element accordingly. Use onChange, onCreate, unUpdate, onDelete callbacks for custom behaviour. e.g updating extension settings.
* @param {string} selector
* @param {string} initialSelectedProfileId
* @param {(profile?: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onChange - 3 cases. 1- When user selects new profile. 2- When user deletes selected profile. 3- When user updates selected profile.
* @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onCreate
* @param {(oldProfile: import('./connection-manager/index.js').ConnectionProfile, newProfile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} unUpdate
* @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onDelete
*/
static handleDropdown(
selector,
initialSelectedProfileId,
onChange = () => { },
onCreate = () => { },
unUpdate = () => { },
onDelete = () => { },
) {
const context = SillyTavern.getContext();
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
throw new Error('Connection Manager is not available');
}
/**
* @type {JQuery<HTMLSelectElement>}
*/
const dropdown = $(selector);
if (!dropdown || !dropdown.length) {
throw new Error(`Could not find dropdown with selector ${selector}`);
}
dropdown.empty();
// Create default option using document.createElement
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = 'Select a Connection Profile';
defaultOption.dataset.i18n = 'Select a Connection Profile';
dropdown.append(defaultOption);
const profiles = context.extensionSettings.connectionManager.profiles;
// Create optgroups using document.createElement
const groups = {};
for (const [apiType, groupLabel] of Object.entries(this.getAllowedTypes())) {
const optgroup = document.createElement('optgroup');
optgroup.label = groupLabel;
groups[apiType] = optgroup;
}
const sortedProfilesByGroup = {};
for (const apiType of Object.keys(this.getAllowedTypes())) {
sortedProfilesByGroup[apiType] = [];
}
for (const profile of profiles) {
if (this.isProfileSupported(profile)) {
const apiMap = CONNECT_API_MAP[profile.api];
if (sortedProfilesByGroup[apiMap.selected]) {
sortedProfilesByGroup[apiMap.selected].push(profile);
}
}
}
// Sort each group alphabetically and add to dropdown
for (const [apiType, groupProfiles] of Object.entries(sortedProfilesByGroup)) {
if (groupProfiles.length === 0) continue;
groupProfiles.sort((a, b) => a.name.localeCompare(b.name));
const group = groups[apiType];
for (const profile of groupProfiles) {
const option = document.createElement('option');
option.value = profile.id;
option.textContent = profile.name;
group.appendChild(option);
}
}
for (const group of Object.values(groups)) {
if (group.children.length > 0) {
dropdown.append(group);
}
}
const selectedProfile = profiles.find((p) => p.id === initialSelectedProfileId);
if (selectedProfile) {
dropdown.val(selectedProfile.id);
}
context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_CREATED, async (profile) => {
const isSupported = this.isProfileSupported(profile);
if (!isSupported) {
return;
}
const group = groups[CONNECT_API_MAP[profile.api].selected];
const option = document.createElement('option');
option.value = profile.id;
option.textContent = profile.name;
group.appendChild(option);
await onCreate(profile);
});
context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_UPDATED, async (oldProfile, newProfile) => {
const currentSelected = dropdown.val();
const isSelectedProfile = currentSelected === oldProfile.id;
await unUpdate(oldProfile, newProfile);
if (!this.isProfileSupported(newProfile)) {
if (isSelectedProfile) {
dropdown.val('');
dropdown.trigger('change');
}
return;
}
const group = groups[CONNECT_API_MAP[newProfile.api].selected];
const oldOption = group.querySelector(`option[value="${oldProfile.id}"]`);
if (oldOption) {
oldOption.remove();
}
const option = document.createElement('option');
option.value = newProfile.id;
option.textContent = newProfile.name;
group.appendChild(option);
if (isSelectedProfile) {
// Ackchyually, we don't need to reselect but what if id changes? It is not possible for now I couldn't stop myself.
dropdown.val(newProfile.id);
dropdown.trigger('change');
}
});
context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_DELETED, async (profile) => {
const currentSelected = dropdown.val();
const isSelectedProfile = currentSelected === profile.id;
if (!this.isProfileSupported(profile)) {
return;
}
const group = groups[CONNECT_API_MAP[profile.api].selected];
const optionToRemove = group.querySelector(`option[value="${profile.id}"]`);
if (optionToRemove) {
optionToRemove.remove();
}
if (isSelectedProfile) {
dropdown.val('');
dropdown.trigger('change');
}
await onDelete(profile);
});
dropdown.on('change', async () => {
const profileId = dropdown.val();
const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
await onChange(profile);
});
}
}
@@ -2337,10 +2337,10 @@ function processReply(str) {
str = str.replaceAll('“', '');
str = str.replaceAll('\n', ', ');
str = str.normalize('NFD');
// Strip out non-alphanumeric characters barring model syntax exceptions
str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]\-'|#]+/g, ' ');
str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one
str = str.trim();
@@ -3234,7 +3234,7 @@ function getNovelParams() {
extension_settings.sd.scheduler = 'karras';
}
if (extension_settings.sd.sampler === 'ddim' ||
if (extension_settings.sd.sampler === 'ddim' ||
['nai-diffusion-4-curated-preview', 'nai-diffusion-4-full'].includes(extension_settings.sd.model)) {
sm = false;
sm_dyn = false;
@@ -3772,7 +3772,6 @@ async function addSDGenButtons() {
$('#sd_wand_container').append(buttonHtml);
$(document.body).append(dropdownHtml);
const messageButton = $('.sd_message_gen');
const button = $('#sd_gen');
const dropdown = $('#sd_dropdown');
dropdown.hide();
@@ -24,7 +24,7 @@ $('button').click(function () {
async function doTokenCounter() {
const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
const html = await renderExtensionTemplateAsync('token-counter', 'window', { tokenizerName });
const dialog = $(html);
const countDebounced = debounce(async () => {
@@ -183,8 +183,6 @@ class GptSovitsV2Provider {
let prompt_text = replaceSpeaker(voiceId);
const streaming = this.settings.streaming;
const params = {
text: inputText,
prompt_text: prompt_text,
+3 -3
View File
@@ -1,5 +1,5 @@
import { cancelTtsPlay, eventSource, event_types, getCurrentChatId, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
import { EdgeTtsProvider } from './edge.js';
import { ElevenLabsTtsProvider } from './elevenlabs.js';
@@ -1207,8 +1207,8 @@ jQuery(async function () {
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);
eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded);
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent);
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent);
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => onMessageEvent(messageId));
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, (messageId) => onMessageEvent(messageId));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'speak',
callback: async (args, value) => {
+11 -9
View File
@@ -46,7 +46,6 @@ import {
hideSwipeButtons,
chat_metadata,
updateChatMetadata,
isStreamingEnabled,
getThumbnailUrl,
getRequestHeaders,
setMenuType,
@@ -435,16 +434,18 @@ export function getGroupCharacterCards(groupId, characterId) {
* @param {string} value Value to replace
* @param {string} fieldName Name of the field
* @param {string} characterName Name of the character
* @param {boolean} trim Whether to trim the value
* @returns {string} Replaced text
* */
function customBaseChatReplace(value, fieldName, characterName) {
function customBaseChatReplace(value, fieldName, characterName, trim) {
if (!value) {
return '';
}
// We should do the custom field name replacement first, and then run it through the normal macro engine with provided names
value = value.replace(/<FIELDNAME>/gi, fieldName);
return baseChatReplace(value.trim(), name1, characterName);
value = trim ? value.trim() : value;
return baseChatReplace(value, name1, characterName);
}
/**
@@ -467,13 +468,12 @@ export function getGroupCharacterCards(groupId, characterId) {
}
// Prepare and replace prefixes
const prefix = customBaseChatReplace(group.generation_mode_join_prefix, fieldName, characterName);
const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName);
const separator = power_user.instruct.wrap ? '\n' : '';
const prefix = customBaseChatReplace(group.generation_mode_join_prefix, fieldName, characterName, false);
const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName, false);
// Also run the macro replacement on the actual content
value = customBaseChatReplace(value, fieldName, characterName);
value = customBaseChatReplace(value, fieldName, characterName, true);
return `${prefix ? prefix + separator : ''}${value}${suffix ? separator + suffix : ''}`;
return `${prefix}${value}${suffix}`;
}
const scenarioOverride = chat_metadata['scenario'];
@@ -696,7 +696,7 @@ export function getGroupBlock(group) {
template.find('.group_fav_icon').css('display', 'none');
template.addClass(group.fav ? 'is_fav' : '');
template.find('.ch_fav').val(group.fav);
template.find('.group_select_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`);
template.find('.group_select_counter').text(count + ' ' + (count != 1 ? t`characters` : t`character`));
template.find('.group_select_block_list').text(namesList.join(', '));
// Display inline tags
@@ -904,6 +904,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
groupChatQueueOrder.set(characters[activatedMembers[i]].avatar, i + 1);
}
}
await eventSource.emit(event_types.GROUP_WRAPPER_STARTED, { selected_group, type });
// now the real generation begins: cycle through every activated character
for (const chId of activatedMembers) {
throwIfAborted();
@@ -942,6 +943,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
setCharacterName('');
activateSendButtons();
showSwipeButtons();
await eventSource.emit(event_types.GROUP_WRAPPER_FINISHED, { selected_group, type });
}
return Promise.resolve(textResult);
+4 -3
View File
@@ -10,6 +10,7 @@ import { SECRET_KEYS, writeSecret } from './secrets.js';
import { delay } from './utils.js';
import { isMobile } from './RossAscends-mods.js';
import { autoSelectInstructPreset } from './instruct-mode.js';
import { t } from './i18n.js';
export {
horde_settings,
@@ -169,7 +170,7 @@ async function adjustHordeGenerationParams(max_context_length, max_length) {
}
}
console.log(maxContextLength, maxLength);
$('#adjustedHordeParams').text(`Context: ${maxContextLength}, Response: ${maxLength}`);
$('#adjustedHordeParams').text(t`Context` + `: ${maxContextLength}, ` + t`Response` + `: ${maxLength}`);
return { maxContextLength, maxLength };
}
@@ -177,7 +178,7 @@ function setContextSizePreview() {
if (horde_settings.models.length) {
adjustHordeGenerationParams(max_context, amount_gen);
} else {
$('#adjustedHordeParams').text('Context: --, Response: --');
$('#adjustedHordeParams').text(t`Context` + ': --, ' + t`Response` + ': --');
}
}
@@ -404,7 +405,7 @@ jQuery(function () {
if (horde_settings.models.length) {
adjustHordeGenerationParams(max_context, amount_gen);
} else {
$('#adjustedHordeParams').text('Context: --, Response: --');
$('#adjustedHordeParams').text(t`Context` + ': --, ' + t`Response` + ': --');
}
saveSettingsDebounced();
+44 -37
View File
@@ -320,59 +320,61 @@ export const force_output_sequence = {
* @param {string} name1 User name.
* @param {string} name2 Character name.
* @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
* @param {InstructSettings} customInstruct Custom instruct mode settings.
* @returns {string} Formatted instruct mode chat message.
*/
export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence) {
let includeNames = isNarrator ? false : power_user.instruct.names_behavior === names_behavior_types.ALWAYS;
export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence, customInstruct = null) {
const instruct = structuredClone(customInstruct ?? power_user.instruct);
let includeNames = isNarrator ? false : instruct.names_behavior === names_behavior_types.ALWAYS;
if (!isNarrator && power_user.instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {
if (!isNarrator && instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {
includeNames = true;
}
function getPrefix() {
if (isNarrator) {
return power_user.instruct.system_same_as_user ? power_user.instruct.input_sequence : power_user.instruct.system_sequence;
return instruct.system_same_as_user ? instruct.input_sequence : instruct.system_sequence;
}
if (isUser) {
if (forceOutputSequence === force_output_sequence.FIRST) {
return power_user.instruct.first_input_sequence || power_user.instruct.input_sequence;
return instruct.first_input_sequence || instruct.input_sequence;
}
if (forceOutputSequence === force_output_sequence.LAST) {
return power_user.instruct.last_input_sequence || power_user.instruct.input_sequence;
return instruct.last_input_sequence || instruct.input_sequence;
}
return power_user.instruct.input_sequence;
return instruct.input_sequence;
}
if (forceOutputSequence === force_output_sequence.FIRST) {
return power_user.instruct.first_output_sequence || power_user.instruct.output_sequence;
return instruct.first_output_sequence || instruct.output_sequence;
}
if (forceOutputSequence === force_output_sequence.LAST) {
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
return instruct.last_output_sequence || instruct.output_sequence;
}
return power_user.instruct.output_sequence;
return instruct.output_sequence;
}
function getSuffix() {
if (isNarrator) {
return power_user.instruct.system_same_as_user ? power_user.instruct.input_suffix : power_user.instruct.system_suffix;
return instruct.system_same_as_user ? instruct.input_suffix : instruct.system_suffix;
}
if (isUser) {
return power_user.instruct.input_suffix;
return instruct.input_suffix;
}
return power_user.instruct.output_suffix;
return instruct.output_suffix;
}
let prefix = getPrefix() || '';
let suffix = getSuffix() || '';
if (power_user.instruct.macro) {
if (instruct.macro) {
prefix = substituteParams(prefix, name1, name2);
prefix = prefix.replace(/{{name}}/gi, name || 'System');
@@ -380,11 +382,11 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
suffix = suffix.replace(/{{name}}/gi, name || 'System');
}
if (!suffix && power_user.instruct.wrap) {
if (!suffix && instruct.wrap) {
suffix = '\n';
}
const separator = power_user.instruct.wrap ? '\n' : '';
const separator = instruct.wrap ? '\n' : '';
// Don't include the name if it's empty
const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
@@ -396,23 +398,26 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
/**
* Formats instruct mode system prompt.
* @param {string} systemPrompt System prompt string.
* @param {InstructSettings} customInstruct Custom instruct mode settings.
* @returns {string} Formatted instruct mode system prompt.
*/
export function formatInstructModeSystemPrompt(systemPrompt) {
export function formatInstructModeSystemPrompt(systemPrompt, customInstruct = null) {
if (!systemPrompt) {
return '';
}
const separator = power_user.instruct.wrap ? '\n' : '';
const instruct = structuredClone(customInstruct ?? power_user.instruct);
if (power_user.instruct.system_sequence_prefix) {
const separator = instruct.wrap ? '\n' : '';
if (instruct.system_sequence_prefix) {
// TODO: Replace with a proper 'System' prompt entity name input
const prefix = power_user.instruct.system_sequence_prefix.replace(/{{name}}/gi, 'System');
const prefix = instruct.system_sequence_prefix.replace(/{{name}}/gi, 'System');
systemPrompt = prefix + separator + systemPrompt;
}
if (power_user.instruct.system_sequence_suffix) {
systemPrompt = systemPrompt + separator + power_user.instruct.system_sequence_suffix;
if (instruct.system_sequence_suffix) {
systemPrompt = systemPrompt + separator + instruct.system_sequence_suffix;
}
return systemPrompt;
@@ -504,30 +509,32 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
* @param {string} name2 Character name.
* @param {boolean} isQuiet Is quiet mode generation.
* @param {boolean} isQuietToLoud Is quiet to loud generation.
* @param {InstructSettings} customInstruct Custom instruct settings.
* @returns {string} Formatted instruct mode last prompt line.
*/
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud) {
const includeNames = name && (power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud);
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud, customInstruct = null) {
const instruct = structuredClone(customInstruct ?? power_user.instruct);
const includeNames = name && (instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud);
function getSequence() {
// User impersonation prompt
if (isImpersonate) {
return power_user.instruct.input_sequence;
return instruct.input_sequence;
}
// Neutral / system / quiet prompt
// Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise
if (isQuiet && !isQuietToLoud) {
return power_user.instruct.last_system_sequence || power_user.instruct.output_sequence;
return instruct.last_system_sequence || instruct.output_sequence;
}
// Quiet in-character prompt
if (isQuiet && isQuietToLoud) {
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
return instruct.last_output_sequence || instruct.output_sequence;
}
// Default AI response
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
return instruct.last_output_sequence || instruct.output_sequence;
}
let sequence = getSequence() || '';
@@ -536,21 +543,21 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
// A hack for Mistral's formatting that has a normal output sequence ending with a space
if (
includeNames &&
power_user.instruct.last_output_sequence &&
power_user.instruct.output_sequence &&
sequence === power_user.instruct.last_output_sequence &&
/\s$/.test(power_user.instruct.output_sequence) &&
!/\s$/.test(power_user.instruct.last_output_sequence)
instruct.last_output_sequence &&
instruct.output_sequence &&
sequence === instruct.last_output_sequence &&
/\s$/.test(instruct.output_sequence) &&
!/\s$/.test(instruct.last_output_sequence)
) {
nameFiller = power_user.instruct.output_sequence.slice(-1);
nameFiller = instruct.output_sequence.slice(-1);
}
if (power_user.instruct.macro) {
if (instruct.macro) {
sequence = substituteParams(sequence, name1, name2);
sequence = sequence.replace(/{{name}}/gi, name || 'System');
}
const separator = power_user.instruct.wrap ? '\n' : '';
const separator = instruct.wrap ? '\n' : '';
let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence);
// Quiet prompt already has a newline at the end
@@ -562,7 +569,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
text += (includeNames ? promptBias : (separator + promptBias.trimStart()));
}
return (power_user.instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);
return (instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);
}
/**
+1
View File
@@ -795,6 +795,7 @@ export function parseNovelAILogprobs(data) {
// Add the chosen token to `merged` if it's not already there. This can
// happen if the chosen token was not among the top 10 most likely ones.
// eslint-disable-next-line no-unused-vars
const [[chosenId], [_, chosenAfter]] = data.chosen[0];
if (!merged.some(([id]) => id === chosenId)) {
merged.push([chosenId, chosenAfter]);
+14 -12
View File
@@ -705,16 +705,18 @@ export function parseExampleIntoIndividual(messageExampleString, appendNamesForG
return result;
}
function formatWorldInfo(value) {
export function formatWorldInfo(value, { wiFormat = null } = {}) {
if (!value) {
return '';
}
if (!oai_settings.wi_format.trim()) {
const format = wiFormat ?? oai_settings.wi_format;
if (!format.trim()) {
return value;
}
return stringFormat(oai_settings.wi_format, value);
return stringFormat(format, value);
}
/**
@@ -952,7 +954,7 @@ async function populateDialogueExamples(prompts, chatCompletion, messageExamples
* @param {number} position - Prompt position in the extensions object.
* @returns {string|false} - The prompt position for prompt collection.
*/
function getPromptPosition(position) {
export function getPromptPosition(position) {
if (position == extension_prompt_types.BEFORE_PROMPT) {
return 'start';
}
@@ -969,7 +971,7 @@ function getPromptPosition(position) {
* @param {number} role Role of the prompt.
* @returns {string} Mapped role.
*/
function getPromptRole(role) {
export function getPromptRole(role) {
switch (role) {
case extension_prompt_roles.SYSTEM:
return 'system';
@@ -2018,6 +2020,7 @@ async function sendOpenAIRequest(type, messages, signal) {
'reasoning_effort': String(oai_settings.reasoning_effort),
'enable_web_search': Boolean(oai_settings.enable_web_search),
'request_images': Boolean(oai_settings.request_images),
'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing,
};
if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
@@ -2048,7 +2051,7 @@ async function sendOpenAIRequest(type, messages, signal) {
delete generate_data.stop;
delete generate_data.logprobs;
}
if (isOAI && oai_settings.openai_model.includes('gpt-4.5-preview') || isOpenRouter && oai_settings.openrouter_model.includes('gpt-4.5-preview')) {
if (isOAI && oai_settings.openai_model.includes('gpt-4.5') || isOpenRouter && oai_settings.openrouter_model.includes('gpt-4.5')) {
delete generate_data.logprobs;
}
@@ -2098,7 +2101,6 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['custom_include_body'] = oai_settings.custom_include_body;
generate_data['custom_exclude_body'] = oai_settings.custom_exclude_body;
generate_data['custom_include_headers'] = oai_settings.custom_include_headers;
generate_data['custom_prompt_post_processing'] = oai_settings.custom_prompt_post_processing;
}
if (isCohere) {
@@ -3476,7 +3478,7 @@ async function getStatusOpen() {
let status;
if ('ai' in window) {
status = 'Valid';
status = t`Valid`;
}
else {
showWindowExtensionError();
@@ -3525,7 +3527,7 @@ async function getStatusOpen() {
const canBypass = (oai_settings.chat_completion_source === chat_completion_sources.OPENAI && oai_settings.bypass_status_check) || oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
if (canBypass) {
setOnlineStatus('Status check bypassed');
setOnlineStatus(t`Status check bypassed`);
}
try {
@@ -3547,7 +3549,7 @@ async function getStatusOpen() {
saveModelList(responseData.data);
}
if (!('error' in responseData)) {
setOnlineStatus('Valid');
setOnlineStatus(t`Valid`);
}
} catch (error) {
console.error(error);
@@ -4435,9 +4437,9 @@ async function onModelChange() {
if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (oai_settings.mistralai_model.includes('codestral-mamba')) {
} else if (['codestral-latest', 'codestral-mamba-2407', 'codestral-2411-rc5', 'codestral-2412', 'codestral-2501'].includes(oai_settings.mistralai_model)) {
$('#openai_max_context').attr('max', max_256k);
} else if (['mistral-large-2407', 'mistral-large-2411', 'mistral-large-latest'].includes(oai_settings.mistralai_model)) {
} else if (['mistral-large-2407', 'mistral-large-2411', 'mistral-large-pixtral-2411', 'mistral-large-latest'].includes(oai_settings.mistralai_model)) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.mistralai_model.includes('mistral-nemo')) {
$('#openai_max_context').attr('max', max_128k);
+1 -1
View File
@@ -800,7 +800,7 @@ async function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
chat_metadata['persona'] = user_avatar;
console.log(`Auto locked persona to ${user_avatar}`);
if (toastPersonaNameChange && power_user.persona_show_notifications) {
toastr.success(`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`);
toastr.success(t`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`);
}
saveMetadataDebounced();
updatePersonaUIStates();
+16 -6
View File
@@ -55,6 +55,7 @@ import { POPUP_TYPE, callGenericPopup } from './popup.js';
import { loadSystemPrompts } from './sysprompt.js';
import { fuzzySearchCategories } from './filters.js';
import { accountStorage } from './util/AccountStorage.js';
import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
export {
loadPowerUserSettings,
@@ -218,7 +219,9 @@ let power_user = {
system_sequence: '',
system_suffix: '',
last_system_sequence: '',
first_input_sequence: '',
first_output_sequence: '',
last_input_sequence: '',
last_output_sequence: '',
system_sequence_prefix: '',
system_sequence_suffix: '',
@@ -255,6 +258,7 @@ let power_user = {
},
reasoning: {
name: DEFAULT_REASONING_TEMPLATE,
auto_parse: false,
add_to_prompts: false,
auto_expand: false,
@@ -1622,6 +1626,7 @@ async function loadPowerUserSettings(settings, data) {
await loadInstructMode(data);
await loadContextSettings();
await loadSystemPrompts(data);
await loadReasoningTemplates(data);
loadMaxContextUnlocked();
switchWaifuMode();
switchSpoilerMode();
@@ -1983,15 +1988,21 @@ export function fuzzySearchGroups(searchValue, fuzzySearchCaches = null) {
/**
* Renders a story string template with the given parameters.
* @param {object} params Template parameters.
* @param {object} [options] Additional options.
* @param {string} [options.customStoryString] Custom story string template.
* @param {InstructSettings} [options.customInstructSettings] Custom instruct settings.
* @returns {string} The rendered story string.
*/
export function renderStoryString(params) {
export function renderStoryString(params, { customStoryString = null, customInstructSettings = null } = {}) {
try {
const storyString = customStoryString ?? power_user.context.story_string;
const instructSettings = structuredClone(customInstructSettings ?? power_user.instruct);
// Validate and log possible warnings/errors
validateStoryString(power_user.context.story_string, params);
validateStoryString(storyString, params);
// compile the story string template into a function, with no HTML escaping
const compiledTemplate = Handlebars.compile(power_user.context.story_string, { noEscape: true });
const compiledTemplate = Handlebars.compile(storyString, { noEscape: true });
// render the story string template with the given params
let output = compiledTemplate(params);
@@ -2004,7 +2015,7 @@ export function renderStoryString(params) {
// add a newline to the end of the story string if it doesn't have one
if (output.length > 0 && !output.endsWith('\n')) {
if (!power_user.instruct.enabled || power_user.instruct.wrap) {
if (!instructSettings.enabled || instructSettings.wrap) {
output += '\n';
}
}
@@ -4227,14 +4238,13 @@ $(document).ready(() => {
],
callback: (args, value) => {
const force = isTrueBoolean(String(args?.force ?? false));
value = String(value ?? '').trim();
// Skip processing if no value and not forced
if (!force && !value) {
return power_user.user_prompt_bias;
}
power_user.user_prompt_bias = value;
power_user.user_prompt_bias = String(value ?? '');
$('#start_reply_with').val(power_user.user_prompt_bias);
saveSettingsDebounced();
+44 -2
View File
@@ -21,7 +21,7 @@ import { groups, selected_group } from './group-chats.js';
import { instruct_presets } from './instruct-mode.js';
import { kai_settings } from './kai-settings.js';
import { convertNovelPreset } from './nai-settings.js';
import { openai_settings, openai_setting_names, oai_settings } from './openai.js';
import { openai_settings, openai_setting_names } from './openai.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { context_presets, getContextSettings, power_user } from './power-user.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -38,6 +38,7 @@ import {
} from './textgen-settings.js';
import { download, parseJsonFile, waitUntilCondition } from './utils.js';
import { t } from './i18n.js';
import { reasoning_templates } from './reasoning.js';
const presetManagers = {};
@@ -168,6 +169,20 @@ class PresetManager {
},
isValid: (data) => PresetManager.isPossiblyTextCompletionData(data),
},
'reasoning': {
name: 'Reasoning Formatting',
getData: () => {
const manager = getPresetManager('reasoning');
const name = manager.getSelectedPresetName();
return manager.getPresetSettings(name);
},
setData: (data) => {
const manager = getPresetManager('reasoning');
const name = data.name;
return manager.savePreset(name, data);
},
isValid: (data) => PresetManager.isPossiblyReasoningData(data),
},
};
static isPossiblyInstructData(data) {
@@ -190,6 +205,11 @@ class PresetManager {
return data && textCompletionProps.every(prop => Object.keys(data).includes(prop));
}
static isPossiblyReasoningData(data) {
const reasoningProps = ['name', 'prefix', 'suffix', 'separator'];
return data && reasoningProps.every(prop => Object.keys(data).includes(prop));
}
/**
* Imports master settings from JSON data.
* @param {object} data Data to import
@@ -227,6 +247,12 @@ class PresetManager {
return await getPresetManager('textgenerationwebui').savePreset(fileName, data);
}
// 5. Reasoning Template
if (this.isPossiblyReasoningData(data)) {
toastr.info(t`Importing as reasoning template...`, t`Reasoning template detected`);
return await getPresetManager('reasoning').savePreset(data.name, data);
}
const validSections = [];
for (const [key, section] of Object.entries(this.masterSections)) {
if (key in data && section.isValid(data[key])) {
@@ -478,6 +504,10 @@ class PresetManager {
presets = system_prompts;
preset_names = system_prompts.map(x => x.name);
break;
case 'reasoning':
presets = reasoning_templates;
preset_names = reasoning_templates.map(x => x.name);
break;
default:
console.warn(`Unknown API ID ${api}`);
}
@@ -490,7 +520,7 @@ class PresetManager {
}
isAdvancedFormatting() {
return this.apiId == 'context' || this.apiId == 'instruct' || this.apiId == 'sysprompt';
return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId);
}
updateList(name, preset) {
@@ -553,6 +583,11 @@ class PresetManager {
sysprompt_preset['name'] = name || power_user.sysprompt.preset;
return sysprompt_preset;
}
case 'reasoning': {
const reasoning_preset = structuredClone(power_user.reasoning);
reasoning_preset['name'] = name || power_user.reasoning.preset;
return reasoning_preset;
}
default:
console.warn(`Unknown API ID ${apiId}`);
return {};
@@ -599,6 +634,13 @@ class PresetManager {
'include_reasoning',
'global_banned_tokens',
'send_banned_tokens',
// Reasoning exclusions
'auto_parse',
'add_to_prompts',
'auto_expand',
'show_hidden',
'max_additions',
];
const settings = Object.assign({}, getSettingsByApiId(this.apiId));
+188 -23
View File
@@ -7,14 +7,46 @@ import { getCurrentLocale, t, translate } from './i18n.js';
import { MacrosParser } from './macros.js';
import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
import { Popup } from './popup.js';
import { power_user } from './power-user.js';
import { performFuzzySearch, power_user } from './power-user.js';
import { getPresetManager } from './preset-manager.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
import { copyText, escapeRegex, isFalseBoolean, isTrueBoolean, setDatasetProperty, trimSpaces } from './utils.js';
/**
* @typedef {object} ReasoningTemplate
* @property {string} name - The name of the template
* @property {string} prefix - Reasoning prefix
* @property {string} suffix - Reasoning suffix
* @property {string} separator - Reasoning separator
*/
/**
* @type {ReasoningTemplate[]} List of reasoning templates
*/
export const reasoning_templates = [];
export const DEFAULT_REASONING_TEMPLATE = 'DeepSeek';
/**
* @type {Record<string, JQuery<HTMLElement>>} List of UI elements for reasoning settings
* @readonly
*/
const UI = {
$select: $('#reasoning_select'),
$suffix: $('#reasoning_suffix'),
$prefix: $('#reasoning_prefix'),
$separator: $('#reasoning_separator'),
$autoParse: $('#reasoning_auto_parse'),
$autoExpand: $('#reasoning_auto_expand'),
$showHidden: $('#reasoning_show_hidden'),
$addToPrompts: $('#reasoning_add_to_prompts'),
$maxAdditions: $('#reasoning_max_additions'),
};
/**
* Enum representing the type of the reasoning for a message (where it came from)
@@ -57,19 +89,24 @@ function toggleReasoningAutoExpand() {
* @param {object} data Response data
* @returns {string} Extracted reasoning
*/
export function extractReasoningFromData(data) {
switch (main_api) {
export function extractReasoningFromData(data, {
mainApi = null,
ignoreShowThoughts = false,
textGenType = null,
chatCompletionSource = null,
} = {}) {
switch (mainApi ?? main_api) {
case 'textgenerationwebui':
switch (textgenerationwebui_settings.type) {
switch (textGenType ?? textgenerationwebui_settings.type) {
case textgen_types.OPENROUTER:
return data?.choices?.[0]?.reasoning ?? '';
}
break;
case 'openai':
if (!oai_settings.show_thoughts) break;
if (!ignoreShowThoughts && !oai_settings.show_thoughts) break;
switch (oai_settings.chat_completion_source) {
switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
case chat_completion_sources.DEEPSEEK:
return data?.choices?.[0]?.message?.reasoning_content ?? '';
case chat_completion_sources.OPENROUTER:
@@ -664,57 +701,102 @@ export class PromptReasoning {
}
function loadReasoningSettings() {
$('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
$('#reasoning_add_to_prompts').on('change', function () {
UI.$addToPrompts.prop('checked', power_user.reasoning.add_to_prompts);
UI.$addToPrompts.on('change', function () {
power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#reasoning_prefix').val(power_user.reasoning.prefix);
$('#reasoning_prefix').on('input', function () {
UI.$prefix.val(power_user.reasoning.prefix);
UI.$prefix.on('input', function () {
power_user.reasoning.prefix = String($(this).val());
saveSettingsDebounced();
});
$('#reasoning_suffix').val(power_user.reasoning.suffix);
$('#reasoning_suffix').on('input', function () {
UI.$suffix.val(power_user.reasoning.suffix);
UI.$suffix.on('input', function () {
power_user.reasoning.suffix = String($(this).val());
saveSettingsDebounced();
});
$('#reasoning_separator').val(power_user.reasoning.separator);
$('#reasoning_separator').on('input', function () {
UI.$separator.val(power_user.reasoning.separator);
UI.$separator.on('input', function () {
power_user.reasoning.separator = String($(this).val());
saveSettingsDebounced();
});
$('#reasoning_max_additions').val(power_user.reasoning.max_additions);
$('#reasoning_max_additions').on('input', function () {
UI.$maxAdditions.val(power_user.reasoning.max_additions);
UI.$maxAdditions.on('input', function () {
power_user.reasoning.max_additions = Number($(this).val());
saveSettingsDebounced();
});
$('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
$('#reasoning_auto_parse').on('change', function () {
UI.$autoParse.prop('checked', power_user.reasoning.auto_parse);
UI.$autoParse.on('change', function () {
power_user.reasoning.auto_parse = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand);
$('#reasoning_auto_expand').on('change', function () {
UI.$autoExpand.prop('checked', power_user.reasoning.auto_expand);
UI.$autoExpand.on('change', function () {
power_user.reasoning.auto_expand = !!$(this).prop('checked');
toggleReasoningAutoExpand();
saveSettingsDebounced();
});
toggleReasoningAutoExpand();
$('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
$('#reasoning_show_hidden').on('change', function () {
UI.$showHidden.prop('checked', power_user.reasoning.show_hidden);
UI.$showHidden.on('change', function () {
power_user.reasoning.show_hidden = !!$(this).prop('checked');
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
saveSettingsDebounced();
});
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
UI.$select.on('change', async function () {
const name = String($(this).val());
const template = reasoning_templates.find(p => p.name === name);
if (!template) {
return;
}
UI.$prefix.val(template.prefix);
UI.$suffix.val(template.suffix);
UI.$separator.val(template.separator);
power_user.reasoning.name = name;
power_user.reasoning.prefix = template.prefix;
power_user.reasoning.suffix = template.suffix;
power_user.reasoning.separator = template.separator;
saveSettingsDebounced();
});
}
function selectReasoningTemplateCallback(args, name) {
if (!name) {
return power_user.reasoning.name ?? '';
}
const quiet = isTrueBoolean(args?.quiet);
const templateNames = reasoning_templates.map(preset => preset.name);
let foundName = templateNames.find(x => x.toLowerCase() === name.toLowerCase());
if (!foundName) {
const result = performFuzzySearch('reasoning-templates', templateNames, [], name);
if (result.length === 0) {
!quiet && toastr.warning(`Reasoning template "${name}" not found`);
return '';
}
foundName = result[0].item;
}
UI.$select.val(foundName).trigger('change');
!quiet && toastr.success(`Reasoning template "${foundName}" selected`);
return foundName;
}
function registerReasoningSlashCommands() {
@@ -848,6 +930,42 @@ function registerReasoningSlashCommands() {
: parsedReasoning.reasoning;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reasoning-template',
aliases: ['reasoning-formatting', 'reasoning-preset'],
callback: selectReasoningTemplateCallback,
returns: 'template name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'quiet',
description: 'Suppress the toast message on template change',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'reasoning template name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: () => reasoning_templates.map(x => new SlashCommandEnumValue(x.name, null, enumTypes.enum, enumIcons.preset)),
}),
],
helpString: `
<div>
Selects a reasoning template by name, using fuzzy search to find the closest match.
Gets the current template if no name is provided.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/reasoning-template DeepSeek</code></pre>
</li>
</ul>
</div>
`,
}));
}
function registerReasoningMacros() {
@@ -1207,6 +1325,53 @@ function registerReasoningAppEvents() {
}
}
/**
* Loads reasoning templates from the settings data.
* @param {object} data Settings data
* @param {ReasoningTemplate[]} data.reasoning Reasoning templates
* @returns {Promise<void>}
*/
export async function loadReasoningTemplates(data) {
if (data.reasoning !== undefined) {
reasoning_templates.splice(0, reasoning_templates.length, ...data.reasoning);
}
for (const template of reasoning_templates) {
$('<option>').val(template.name).text(template.name).appendTo(UI.$select);
}
// No template name, need to migrate
if (power_user.reasoning.name === undefined) {
const defaultTemplate = reasoning_templates.find(p => p.name === DEFAULT_REASONING_TEMPLATE);
if (defaultTemplate) {
// If the reasoning settings were modified - migrate them to a custom template
if (power_user.reasoning.prefix !== defaultTemplate.prefix || power_user.reasoning.suffix !== defaultTemplate.suffix || power_user.reasoning.separator !== defaultTemplate.separator) {
/** @type {ReasoningTemplate} */
const data = {
name: '[Migrated] Custom',
prefix: power_user.reasoning.prefix,
suffix: power_user.reasoning.suffix,
separator: power_user.reasoning.separator,
};
await getPresetManager('reasoning')?.savePreset(data.name, data);
power_user.reasoning.name = data.name;
} else {
power_user.reasoning.name = defaultTemplate.name;
}
} else {
// Template not found (deleted or content check skipped - leave blank)
power_user.reasoning.name = '';
}
saveSettingsDebounced();
}
UI.$select.val(power_user.reasoning.name);
}
/**
* Initializes reasoning settings and event handlers.
*/
export function initReasoning() {
loadReasoningSettings();
setReasoningEventHandlers();
+2 -19
View File
@@ -9,6 +9,7 @@ import { power_user } from './power-user.js';
//import { getSortableDelay, onlyUnique } from './utils.js';
//import { getCfgPrompt } from './cfg-scale.js';
import { setting_names } from './textgen-settings.js';
import { renderTemplateAsync } from './templates.js';
const TGsamplerNames = setting_names;
@@ -25,25 +26,7 @@ async function showSamplerSelectPopup() {
const html = $(document.createElement('div'));
html.attr('id', 'sampler_view_list')
.addClass('flex-container flexFlowColumn');
html.append(`
<div class="title_restorable flexFlowColumn alignItemsBaseline">
<div class="flex-container justifyCenter">
<h3>Sampler Select</h3>
<div class="flex-container alignItemsBaseline">
<div id="resetSelectedSamplers" class="menu_button menu_button_icon" title="Reset custom sampler selection">
<i class="fa-solid fa-recycle"></i>
</div>
</div>
<!--<div class="flex-container alignItemsBaseline">
<div class="menu_button menu_button_icon" title="Create a new sampler">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Create">Create</span>
</div>
</div>-->
</div>
<small>Here you can toggle the display of individual samplers. (WIP)</small>
</div>
<hr>`);
html.append(await renderTemplateAsync('samplerSelector'));
const listContainer = $('<div id="apiSamplersList" class="flex-container flexNoGap"></div>');
const APISamplers = await listSamplers(main_api);
+2 -1
View File
@@ -1,5 +1,6 @@
import { DOMPurify } from '../lib.js';
import { callPopup, getRequestHeaders } from '../script.js';
import { t } from './i18n.js';
export const SECRET_KEYS = {
HORDE: 'api_key_horde',
@@ -104,7 +105,7 @@ async function viewSecrets() {
});
if (response.status == 403) {
callPopup('<h3>Forbidden</h3><p>To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.</p>', 'text');
callPopup('<h3>' + t`Forbidden` + '</h3><p>' + t`To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.` + '</p>', 'text');
return;
}
+5 -3
View File
@@ -69,12 +69,14 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
import { accountStorage } from './util/AccountStorage.js';
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
};
@@ -4345,7 +4347,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
* @prop {boolean} [handleParserErrors] (true) Whether to handle parser errors (show toast on error) or throw.
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
* @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
* @prop {import('./slash-commands/SlashCommandParser.js').ParserFlags} [parserFlags] (null) Parser flags to apply
* @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
* @prop {SlashCommandDebugController} [debugController] (null) Controller used to control debug execution
* @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
@@ -4355,7 +4357,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
/**
* @typedef ExecuteSlashCommandsOnChatInputOptions
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
* @prop {import('./slash-commands/SlashCommandParser.js').ParserFlags} [parserFlags] (null) Parser flags to apply
* @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
* @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
*/
@@ -3,16 +3,12 @@ import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandDebugController } from './SlashCommandDebugController.js';
import { PARSER_FLAG } from './SlashCommandParser.js';
import { SlashCommandScope } from './SlashCommandScope.js';
/**
* @typedef {{
* _scope:SlashCommandScope,
* _parserFlags:{[id:PARSER_FLAG]:boolean},
* _parserFlags:import('./SlashCommandParser.js').ParserFlags,
* _abortController:SlashCommandAbortController,
* _debugController:SlashCommandDebugController,
* _hasUnnamedArgument:boolean,
@@ -1,9 +1,6 @@
import { AutoCompleteNameResult } from '../autocomplete/AutoCompleteNameResult.js';
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
import { AutoCompleteSecondaryNameResult } from '../autocomplete/AutoCompleteSecondaryNameResult.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAutoCompleteOption.js';
import { SlashCommandEnumAutoCompleteOption } from './SlashCommandEnumAutoCompleteOption.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
@@ -2,7 +2,6 @@ import { substituteParams } from '../../script.js';
import { delay, escapeRegex, uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandBreak } from './SlashCommandBreak.js';
import { SlashCommandBreakController } from './SlashCommandBreakController.js';
import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
@@ -14,21 +13,19 @@ import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgument
import { SlashCommandScope } from './SlashCommandScope.js';
export class SlashCommandClosure {
/**@type {SlashCommandScope}*/ scope;
/**@type {boolean}*/ executeNow = false;
// @ts-ignore
/**@type {SlashCommandNamedArgumentAssignment[]}*/ argumentList = [];
// @ts-ignore
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
/**@type {SlashCommandExecutor[]}*/ executorList = [];
/**@type {SlashCommandAbortController}*/ abortController;
/**@type {SlashCommandBreakController}*/ breakController;
/**@type {SlashCommandDebugController}*/ debugController;
/**@type {(done:number, total:number)=>void}*/ onProgress;
/**@type {string}*/ rawText;
/**@type {string}*/ fullText;
/**@type {string}*/ parserContext;
/**@type {string}*/ #source = uuidv4();
/** @type {SlashCommandScope} */ scope;
/** @type {boolean} */ executeNow = false;
/** @type {SlashCommandNamedArgumentAssignment[]} */ argumentList = [];
/** @type {SlashCommandNamedArgumentAssignment[]} */ providedArgumentList = [];
/** @type {SlashCommandExecutor[]} */ executorList = [];
/** @type {SlashCommandAbortController} */ abortController;
/** @type {SlashCommandBreakController} */ breakController;
/** @type {SlashCommandDebugController} */ debugController;
/** @type {(done:number, total:number)=>void} */ onProgress;
/** @type {string} */ rawText;
/** @type {string} */ fullText;
/** @type {string} */ parserContext;
/** @type {string} */ #source = uuidv4();
get source() { return this.#source; }
set source(value) {
this.#source = value;
@@ -2,20 +2,20 @@ import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
export class SlashCommandDebugController {
/**@type {SlashCommandClosure[]} */ stack = [];
/**@type {SlashCommandExecutor[]} */ cmdStack = [];
/**@type {boolean[]} */ stepStack = [];
/**@type {boolean} */ isStepping = false;
/**@type {boolean} */ isSteppingInto = false;
/**@type {boolean} */ isSteppingOut = false;
/** @type {SlashCommandClosure[]} */ stack = [];
/** @type {SlashCommandExecutor[]} */ cmdStack = [];
/** @type {boolean[]} */ stepStack = [];
/** @type {boolean} */ isStepping = false;
/** @type {boolean} */ isSteppingInto = false;
/** @type {boolean} */ isSteppingOut = false;
/**@type {object} */ namedArguments;
/**@type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */ unnamedArguments;
/** @type {object} */ namedArguments;
/** @type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */ unnamedArguments;
/**@type {Promise<boolean>} */ continuePromise;
/**@type {(boolean)=>void} */ continueResolver;
/** @type {Promise<boolean>} */ continuePromise;
/** @type {(boolean)=>void} */ continueResolver;
/**@type {(closure:SlashCommandClosure, executor:SlashCommandExecutor)=>Promise<boolean>} */ onBreakPoint;
/** @type {(closure:SlashCommandClosure, executor:SlashCommandExecutor)=>Promise<boolean>} */ onBreakPoint;
@@ -1,7 +1,3 @@
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandScope } from './SlashCommandScope.js';
/**
* @typedef {'enum' | 'command' | 'namedArgument' | 'variable' | 'qr' | 'macro' | 'number' | 'name'} EnumType
*/
@@ -1,11 +1,7 @@
// eslint-disable-next-line no-unused-vars
import { uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
// eslint-disable-next-line no-unused-vars
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
// eslint-disable-next-line no-unused-vars
import { PARSER_FLAG } from './SlashCommandParser.js';
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
export class SlashCommandExecutor {
@@ -28,11 +24,10 @@ export class SlashCommandExecutor {
arg.value.source = value;
}
}
/**@type {SlashCommand}*/ command;
// @ts-ignore
/**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];
/**@type {SlashCommandUnnamedArgumentAssignment[]}*/ unnamedArgumentList = [];
/**@type {{[id:PARSER_FLAG]:boolean}} */ parserFlags;
/** @type {SlashCommand} */ command;
/** @type {SlashCommandNamedArgumentAssignment[]} */ namedArgumentList = [];
/** @type {SlashCommandUnnamedArgumentAssignment[]} */ unnamedArgumentList = [];
/** @type {import('./SlashCommandParser.js').ParserFlags} */ parserFlags;
get commandCount() {
return 1
@@ -1,10 +1,10 @@
import { SlashCommandClosure } from './SlashCommandClosure.js';
export class SlashCommandNamedArgumentAssignment {
/**@type {number}*/ start;
/**@type {number}*/ end;
/**@type {string}*/ name;
/**@type {string|SlashCommandClosure}*/ value;
/** @type {number} */ start;
/** @type {number} */ end;
/** @type {string} */ name;
/** @type {string|SlashCommandClosure} */ value;
constructor() {
@@ -1,11 +1,10 @@
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
export class SlashCommandNamedArgumentAutoCompleteOption extends AutoCompleteOption {
/**@type {SlashCommandNamedArgument}*/ arg;
/**@type {SlashCommand}*/ cmd;
/** @type {SlashCommandNamedArgument} */ arg;
/** @type {SlashCommand} */ cmd;
/**
* @param {SlashCommandNamedArgument} arg
@@ -8,11 +8,9 @@ import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandParserError } from './SlashCommandParserError.js';
import { AutoCompleteNameResult } from '../autocomplete/AutoCompleteNameResult.js';
import { SlashCommandQuickReplyAutoCompleteOption } from './SlashCommandQuickReplyAutoCompleteOption.js';
// eslint-disable-next-line no-unused-vars
import { SlashCommandScope } from './SlashCommandScope.js';
import { SlashCommandVariableAutoCompleteOption } from './SlashCommandVariableAutoCompleteOption.js';
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
// eslint-disable-next-line no-unused-vars
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNameResult.js';
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
@@ -28,16 +26,18 @@ import { t } from '../i18n.js';
/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
/**@readonly*/
/**@enum {Number}*/
/**
* @enum {Number}
* @readonly
* @typedef {{[id:PARSER_FLAG]:boolean}} ParserFlags
*/
export const PARSER_FLAG = {
'STRICT_ESCAPING': 1,
'REPLACE_GETVAR': 2,
};
export class SlashCommandParser {
// @ts-ignore
/**@type {Object.<string, SlashCommand>}*/ static commands = {};
/** @type {Object.<string, SlashCommand>} */ static commands = {};
/**
* @deprecated Use SlashCommandParser.addCommandObject() instead.
@@ -101,26 +101,25 @@ export class SlashCommandParser {
get commands() {
return SlashCommandParser.commands;
}
// @ts-ignore
/**@type {Object.<string, string>}*/ helpStrings = {};
/**@type {boolean}*/ verifyCommandNames = true;
/**@type {string}*/ text;
/**@type {number}*/ index;
/**@type {SlashCommandAbortController}*/ abortController;
/**@type {SlashCommandDebugController}*/ debugController;
/**@type {SlashCommandScope}*/ scope;
/**@type {SlashCommandClosure}*/ closure;
/** @type {Object.<string, string>} */ helpStrings = {};
/** @type {boolean} */ verifyCommandNames = true;
/** @type {string} */ text;
/** @type {number} */ index;
/** @type {SlashCommandAbortController} */ abortController;
/** @type {SlashCommandDebugController} */ debugController;
/** @type {SlashCommandScope} */ scope;
/** @type {SlashCommandClosure} */ closure;
/**@type {Object.<PARSER_FLAG,boolean>}*/ flags = {};
/** @type {Object.<PARSER_FLAG,boolean>} */ flags = {};
/**@type {boolean}*/ jumpedEscapeSequence = false;
/** @type {boolean} */ jumpedEscapeSequence = false;
/**@type {{start:number, end:number}[]}*/ closureIndex;
/**@type {{start:number, end:number, name:string}[]}*/ macroIndex;
/**@type {SlashCommandExecutor[]}*/ commandIndex;
/**@type {SlashCommandScope[]}*/ scopeIndex;
/** @type {{start:number, end:number}[]} */ closureIndex;
/** @type {{start:number, end:number, name:string}[]} */ macroIndex;
/** @type {SlashCommandExecutor[]} */ commandIndex;
/** @type {SlashCommandScope[]} */ scopeIndex;
/**@type {string}*/ parserContext;
/** @type {string} */ parserContext;
get userIndex() { return this.index; }
@@ -2,21 +2,21 @@ import { SlashCommandClosure } from './SlashCommandClosure.js';
import { convertValueType } from '../utils.js';
export class SlashCommandScope {
/**@type {string[]}*/ variableNames = [];
/** @type {string[]} */ variableNames = [];
get allVariableNames() {
const names = [...this.variableNames, ...(this.parent?.allVariableNames ?? [])];
return names.filter((it,idx)=>idx == names.indexOf(it));
}
// @ts-ignore
/**@type {object.<string, string|SlashCommandClosure>}*/ variables = {};
/** @type {object.<string, string|SlashCommandClosure>} */ variables = {};
// @ts-ignore
/**@type {object.<string, string|SlashCommandClosure>}*/ macros = {};
/**@type {{key:string, value:string|SlashCommandClosure}[]} */
/** @type {object.<string, string|SlashCommandClosure>} */ macros = {};
/** @type {{key:string, value:string|SlashCommandClosure}[]} */
get macroList() {
return [...Object.keys(this.macros).map(key=>({ key, value:this.macros[key] })), ...(this.parent?.macroList ?? [])];
}
/**@type {SlashCommandScope}*/ parent;
/**@type {string}*/ #pipe;
/** @type {SlashCommandScope} */ parent;
/** @type {string} */ #pipe;
get pipe() {
return this.#pipe ?? this.parent?.pipe;
}
@@ -1,9 +1,9 @@
import { SlashCommandClosure } from './SlashCommandClosure.js';
export class SlashCommandUnnamedArgumentAssignment {
/**@type {number}*/ start;
/**@type {number}*/ end;
/**@type {string|SlashCommandClosure}*/ value;
/** @type {number} */ start;
/** @type {number} */ end;
/** @type {string|SlashCommandClosure} */ value;
constructor() {
+6 -1
View File
@@ -49,6 +49,7 @@ import {
clearChat,
unshallowCharacter,
deleteLastMessage,
getCharacterCardFields,
} from '../script.js';
import {
extension_settings,
@@ -78,8 +79,9 @@ import { ToolManager } from './tool-calling.js';
import { accountStorage } from './util/AccountStorage.js';
import { timestampToMoment, uuidv4 } from './utils.js';
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
import { convertCharacterBook, getWorldInfoPrompt, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
import { ChatCompletionService, TextCompletionService } from './custom-request.js';
import { ConnectionManagerRequestService } from './extensions/shared.js';
import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';
export function getContext() {
@@ -188,6 +190,7 @@ export function getContext() {
textCompletionSettings: textgenerationwebui_settings,
powerUserSettings: power_user,
getCharacters,
getCharacterCardFields,
uuidv4,
humanizedDateTime,
updateMessageBlock,
@@ -206,6 +209,7 @@ export function getContext() {
saveWorldInfo,
updateWorldInfoList,
convertCharacterBook,
getWorldInfoPrompt,
CONNECT_API_MAP,
getTextGenServer,
extractMessageFromData,
@@ -215,6 +219,7 @@ export function getContext() {
clearChat,
ChatCompletionService,
TextCompletionService,
ConnectionManagerRequestService,
updateReasoningUI,
parseReasoningFromString,
unshallowCharacter,
-1
View File
@@ -13,7 +13,6 @@ import {
DEFAULT_PRINT_TIMEOUT,
printCharacters,
} from '../script.js';
// eslint-disable-next-line no-unused-vars
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
+2 -2
View File
@@ -1,9 +1,9 @@
<div data-type="assistant_note">
<div>
<b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>
<span>Click the button to save it as a file.</span>
<span data-i18n="Click the button to save it as a file.">Click the button to save it as a file.</span>
</div>
<div class="assistant_note_export menu_button menu_button_icon" title="Export as JSONL">
<div class="assistant_note_export menu_button menu_button_icon" data-i18n="[title]Export as JSONL" title="Export as JSONL">
<i class="fa-solid fa-file-export"></i>
</div>
</div>
+2 -1
View File
@@ -20,7 +20,8 @@
<li><tt>&lcub;&lcub;summary&rcub;&rcub;</tt> <span data-i18n="help_macros_summary">the latest chat summary generated by the "Summarize" extension (if available).</span></li>
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> <span data-i18n="help_macros_15">your current Persona username</span></li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> <span data-i18n="help_macros_16">the Character's name</span></li>
<li><tt>&lcub;&lcub;char_version&rcub;&rcub;</tt> <span data-i18n="help_macros_17">the Character's version number</span></li>
<li><tt>&lcub;&lcub;version&rcub;&rcub;</tt> <span data-i18n="help_macros_17">the Character's version number</span></li>
<li><tt>&lcub;&lcub;charDepthPrompt&rcub;&rcub;</tt> <span data-i18n="help_macros_charDepthPrompt">the Character's @ Depth Note</span></li>
<li><tt>&lcub;&lcub;group&rcub;&rcub;</tt> <span data-i18n="help_macros_18">a comma-separated list of group member names (including muted) or the character name in solo chats. Alias: &lcub;&lcub;charIfNotGroup&rcub;&rcub;</span></li>
<li><tt>&lcub;&lcub;groupNotMuted&rcub;&rcub;</tt> <span data-i18n="help_groupNotMuted">the same as &lcub;&lcub;group&rcub;&rcub;, but excludes muted members</span></li>
<li><tt>&lcub;&lcub;model&rcub;&rcub;</tt> <span data-i18n="help_macros_19">a text generation model name for the currently selected API. </span><b data-i18n="Can be inaccurate!">Can be inaccurate!</b></li>
@@ -0,0 +1,18 @@
<div class="title_restorable flexFlowColumn alignItemsBaseline">
<div class="flex-container justifyCenter">
<h3 data-i18n="Sampler Select">Sampler Select</h3>
<div class="flex-container alignItemsBaseline">
<div id="resetSelectedSamplers" class="menu_button menu_button_icon" data-i18n="[title]Reset custom sampler selection" title="Reset custom sampler selection">
<i class="fa-solid fa-recycle"></i>
</div>
</div>
<!--<div class="flex-container alignItemsBaseline">
<div class="menu_button menu_button_icon" title="Create a new sampler">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Create">Create</span>
</div>
</div>-->
</div>
<small data-i18n="Here you can toggle the display of individual samplers. (WIP)">Here you can toggle the display of individual samplers. (WIP)</small>
</div>
<hr>
+1 -1
View File
@@ -86,7 +86,7 @@ const OOBA_DEFAULT_ORDER = [
'encoder_repetition_penalty',
'no_repeat_ngram',
];
const APHRODITE_DEFAULT_ORDER = [
export const APHRODITE_DEFAULT_ORDER = [
'dry',
'penalties',
'no_repeat_ngram',
+2 -2
View File
@@ -9,7 +9,7 @@ import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
@@ -583,7 +583,7 @@ export function evalBoolean(rule, a, b) {
* Executes a slash command from a string (may be enclosed in quotes) and returns the result.
* @param {string} command Command to execute. May contain escaped macro and batch separators.
* @param {SlashCommandScope} [scope] The scope to use.
* @param {{[id:PARSER_FLAG]:boolean}} [parserFlags] The parser flags to use.
* @param {import('./slash-commands/SlashCommandParser.js').ParserFlags} [parserFlags] The parser flags to use.
* @param {SlashCommandAbortController} [abortController] The abort controller to use.
* @returns {Promise<SlashCommandClosureResult>} Closure execution result
*/
+24 -8
View File
@@ -1,6 +1,6 @@
import { Fuse } from '../lib.js';
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
import { saveSettings, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce, findChar, onlyUnique } from './utils.js';
import { extension_settings, getContext } from './extensions.js';
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
@@ -753,10 +753,17 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
/**
* Gets the world info based on chat messages.
* @param {string[]} chat The chat messages to scan, in reverse order.
* @param {number} maxContext The maximum context size of the generation.
* @param {boolean} isDryRun If true, the function will not emit any events.
* @typedef {{worldInfoString: string, worldInfoBefore: string, worldInfoAfter: string, worldInfoExamples: any[], worldInfoDepth: any[]}} WIPromptResult
* @param {string[]} chat - The chat messages to scan, in reverse order.
* @param {number} maxContext - The maximum context size of the generation.
* @param {boolean} isDryRun - If true, the function will not emit any events.
* @typedef {object} WIPromptResult
* @property {string} worldInfoString - Complete world info string
* @property {string} worldInfoBefore - World info that goes before the prompt
* @property {string} worldInfoAfter - World info that goes after the prompt
* @property {Array} worldInfoExamples - Array of example entries
* @property {Array} worldInfoDepth - Array of depth entries
* @property {Array} anBefore - Array of entries before Author's Note
* @property {Array} anAfter - Array of entries after Author's Note
* @returns {Promise<WIPromptResult>} The world info string and depth.
*/
export async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
@@ -778,6 +785,8 @@ export async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
worldInfoAfter,
worldInfoExamples: activatedWorldInfo.EMEntries ?? [],
worldInfoDepth: activatedWorldInfo.WIDepthEntries ?? [],
anBefore: activatedWorldInfo.ANBeforeEntries ?? [],
anAfter: activatedWorldInfo.ANAfterEntries ?? [],
};
}
@@ -3862,7 +3871,14 @@ function parseDecorators(content) {
* @param {string[]} chat The chat messages to scan, in reverse order.
* @param {number} maxContext The maximum context size of the generation.
* @param {boolean} isDryRun Whether to perform a dry run.
* @typedef {{ worldInfoBefore: string, worldInfoAfter: string, EMEntries: any[], WIDepthEntries: any[], allActivatedEntries: Set<any> }} WIActivated
* @typedef {object} WIActivated
* @property {string} worldInfoBefore The world info before the chat.
* @property {string} worldInfoAfter The world info after the chat.
* @property {any[]} EMEntries The entries for examples.
* @property {any[]} WIDepthEntries The depth entries.
* @property {any[]} ANBeforeEntries The entries before Author's Note.
* @property {any[]} ANAfterEntries The entries after Author's Note.
* @property {Set<any>} allActivatedEntries All entries.
* @returns {Promise<WIActivated>} The world info activated.
*/
export async function checkWorldInfo(chat, maxContext, isDryRun) {
@@ -3906,7 +3922,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
timedEffects.checkTimedEffects();
if (sortedEntries.length === 0) {
return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], ANBeforeEntries: [], ANAfterEntries: [], allActivatedEntries: new Set() };
}
/** @type {number[]} Represents the delay levels for entries that are delayed until recursion */
@@ -4355,7 +4371,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`);
return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, ANBeforeEntries: ANTopEntries, ANAfterEntries: ANBottomEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
}
/**
+1
View File
@@ -43,6 +43,7 @@ export const USER_DIRECTORY_TEMPLATE = Object.freeze({
vectors: 'vectors',
backups: 'backups',
sysprompt: 'sysprompt',
reasoning: 'reasoning',
});
/**
+10 -9
View File
@@ -49,7 +49,7 @@ const API_COHERE_V2 = 'https://api.cohere.ai/v2';
const API_PERPLEXITY = 'https://api.perplexity.ai';
const API_GROQ = 'https://api.groq.com/openai/v1';
const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
const API_01AI = 'https://api.01.ai/v1';
const API_01AI = 'https://api.lingyiwanwu.com/v1';
const API_BLOCKENTROPY = 'https://api.blockentropy.ai/v1';
const API_AI21 = 'https://api.ai21.com/studio/v1';
const API_NANOGPT = 'https://nano-gpt.com/api/v1';
@@ -1048,6 +1048,15 @@ router.post('/generate', function (request, response) {
let bodyParams;
const isTextCompletion = Boolean(request.body.model && TEXT_COMPLETION_MODELS.includes(request.body.model)) || typeof request.body.messages === 'string';
const postProcessTypes = [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENROUTER];
if (Array.isArray(request.body.messages) && postProcessTypes.includes(request.body.chat_completion_source) && request.body.custom_prompt_post_processing) {
console.info('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
request.body.messages = postProcessPrompt(
request.body.messages,
request.body.custom_prompt_post_processing,
getPromptNames(request));
}
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {
apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();
apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);
@@ -1121,14 +1130,6 @@ router.post('/generate', function (request, response) {
mergeObjectWithYaml(bodyParams, request.body.custom_include_body);
mergeObjectWithYaml(headers, request.body.custom_include_headers);
if (request.body.custom_prompt_post_processing) {
console.info('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
request.body.messages = postProcessPrompt(
request.body.messages,
request.body.custom_prompt_post_processing,
getPromptNames(request));
}
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) {
apiUrl = API_PERPLEXITY;
apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);
+72 -6
View File
@@ -21,6 +21,7 @@ import {
const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true, 'boolean');
const maxTotalChatBackups = Number(getConfigValue('backups.chat.maxTotalBackups', -1, 'number'));
const throttleInterval = Number(getConfigValue('backups.chat.throttleInterval', 10_000, 'number'));
const checkIntegrity = !!getConfigValue('backups.chat.checkIntegrity', true, 'boolean');
/**
* Saves a chat to the backups directory.
@@ -292,15 +293,81 @@ function importRisuChat(userName, characterName, jsonData) {
return chat.map(obj => JSON.stringify(obj)).join('\n');
}
/**
* Reads the first line of a file asynchronously.
* @param {string} filePath Path to the file
* @returns {Promise<string>} The first line of the file
*/
function readFirstLine(filePath) {
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
const rl = readline.createInterface({ input: stream });
return new Promise((resolve, reject) => {
let resolved = false;
rl.on('line', line => {
resolved = true;
rl.close();
stream.close();
resolve(line);
});
rl.on('error', error => {
resolved = true;
reject(error);
});
// Handle empty files
stream.on('end', () => {
if (!resolved) {
resolved = true;
resolve('');
}
});
});
}
/**
* Checks if the chat being saved has the same integrity as the one being loaded.
* @param {string} filePath Path to the chat file
* @param {string} integritySlug Integrity slug
* @returns {Promise<boolean>} Whether the chat is intact
*/
async function checkChatIntegrity(filePath, integritySlug) {
// If the chat file doesn't exist, assume it's intact
if (!fs.existsSync(filePath)) {
return true;
}
// Parse the first line of the chat file as JSON
const firstLine = await readFirstLine(filePath);
const jsonData = tryParse(firstLine);
const chatIntegrity = jsonData?.chat_metadata?.integrity;
// If the chat has no integrity metadata, assume it's intact
if (!chatIntegrity) {
return true;
}
// Check if the integrity matches
return chatIntegrity === integritySlug;
}
export const router = express.Router();
router.post('/save', validateAvatarUrlMiddleware, function (request, response) {
router.post('/save', validateAvatarUrlMiddleware, async function (request, response) {
try {
const directoryName = String(request.body.avatar_url).replace('.png', '');
const chatData = request.body.chat;
const jsonlData = chatData.map(JSON.stringify).join('\n');
const fileName = `${String(request.body.file_name)}.jsonl`;
const filePath = path.join(request.user.directories.chats, directoryName, sanitize(fileName));
if (checkIntegrity && !request.body.force) {
const integritySlug = chatData?.[0]?.chat_metadata?.integrity;
const isIntact = await checkChatIntegrity(filePath, integritySlug);
if (!isIntact) {
console.error(`Chat integrity check failed for ${filePath}`);
return response.status(400).send({ error: 'integrity' });
}
}
writeFileAtomicSync(filePath, jsonlData, 'utf8');
getBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData);
return response.send({ result: 'ok' });
@@ -716,12 +783,11 @@ router.post('/search', validateAvatarUrlMiddleware, function (request, response)
continue;
}
// Search through messages
// Search through title and messages of the chat
const fragments = query.trim().toLowerCase().split(/\s+/).filter(x => x);
const hasMatch = messages.some(message => {
const text = message?.mes?.toLowerCase();
return text && fragments.every(fragment => text.includes(fragment));
});
const text = [path.parse(chatFile.path).name,
...messages.map(message => message?.mes)].join('\n').toLowerCase();
const hasMatch = fragments.every(fragment => text.includes(fragment));
if (hasMatch) {
results.push({
+4 -1
View File
@@ -48,6 +48,7 @@ export const CONTENT_TYPES = {
MOVING_UI: 'moving_ui',
QUICK_REPLIES: 'quick_replies',
SYSPROMPT: 'sysprompt',
REASONING: 'reasoning',
};
/**
@@ -61,7 +62,7 @@ export function getDefaultPresets(directories) {
const presets = [];
for (const contentItem of contentIndex) {
if (contentItem.type.endsWith('_preset') || contentItem.type === 'instruct' || contentItem.type === 'context' || contentItem.type === 'sysprompt') {
if (contentItem.type.endsWith('_preset') || ['instruct', 'context', 'sysprompt', 'reasoning'].includes(contentItem.type)) {
contentItem.name = path.parse(contentItem.filename).name;
contentItem.folder = getTargetByType(contentItem.type, directories);
presets.push(contentItem);
@@ -299,6 +300,8 @@ function getTargetByType(type, directories) {
return directories.quickreplies;
case CONTENT_TYPES.SYSPROMPT:
return directories.sysprompt;
case CONTENT_TYPES.REASONING:
return directories.reasoning;
default:
return null;
}
+1 -1
View File
@@ -116,7 +116,7 @@ router.post('/caption-image', async (request, response) => {
}
if (request.body.api === 'zerooneai') {
apiUrl = 'https://api.01.ai/v1/chat/completions';
apiUrl = 'https://api.lingyiwanwu.com/v1/chat/completions';
}
if (request.body.api === 'groq') {
+2
View File
@@ -30,6 +30,8 @@ function getPresetSettingsByAPI(apiId, directories) {
return { folder: directories.context, extension: '.json' };
case 'sysprompt':
return { folder: directories.sysprompt, extension: '.json' };
case 'reasoning':
return { folder: directories.reasoning, extension: '.json' };
default:
return { folder: null, extension: null };
}
+2
View File
@@ -254,6 +254,7 @@ router.post('/get', (request, response) => {
const instruct = readAndParseFromDirectory(request.user.directories.instruct);
const context = readAndParseFromDirectory(request.user.directories.context);
const sysprompt = readAndParseFromDirectory(request.user.directories.sysprompt);
const reasoning = readAndParseFromDirectory(request.user.directories.reasoning);
response.send({
settings,
@@ -272,6 +273,7 @@ router.post('/get', (request, response) => {
instruct,
context,
sysprompt,
reasoning,
enable_extensions: ENABLE_EXTENSIONS,
enable_extensions_auto_update: ENABLE_EXTENSIONS_AUTO_UPDATE,
enable_accounts: ENABLE_ACCOUNTS,
+1 -1
View File
@@ -411,7 +411,7 @@ export function getTokenizerModel(requestModel) {
return 'gpt-4o';
}
if (requestModel.includes('gpt-4.5-preview')) {
if (requestModel.includes('gpt-4.5')) {
return 'gpt-4o';
}
+1
View File
@@ -95,6 +95,7 @@ const STORAGE_KEYS = {
* @property {string} vectors - The directory where the vectors are stored
* @property {string} backups - The directory where the backups are stored
* @property {string} sysprompt - The directory where the system prompt data is stored
* @property {string} reasoning - The directory where the reasoning templates are stored
*/
/**