mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-10 00:50:11 +01:00
Merge branch 'staging' into staging
This commit is contained in:
commit
fc158ca176
2
.github/close-label.yml
vendored
Normal file
2
.github/close-label.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
🐛 Bug: ✅ Fixed
|
||||
🦄 Feature Request: ✅ Implemented
|
62
.github/issue-auto-comments.yml
vendored
Normal file
62
.github/issue-auto-comments.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
comment:
|
||||
footer: |
|
||||
---
|
||||
> I am a bot, and this is an automated message 🤖
|
||||
labels:
|
||||
- name: ✖️ Invalid
|
||||
labeled:
|
||||
issue:
|
||||
action: close
|
||||
body: >
|
||||
Hello @{{ issue.user.login }} your ticket has been marked as invalid.
|
||||
Please ensure you follow the issue template, provide all requested info,
|
||||
and be sure to check the docs + previous issues prior to raising tickets.
|
||||
pr:
|
||||
body: Thank you @{{ pull_request.user.login }} for suggesting this. Please follow the pull request templates.
|
||||
action: close
|
||||
|
||||
- name: 👩💻 Good First Issue
|
||||
labeled:
|
||||
issue:
|
||||
body: >
|
||||
This issue has been marked as a good first issue for first-time contributors to implement!
|
||||
This is a great way to support the project, while also improving your skills, you'll also be credited as a contributor once your PR is merged.
|
||||
If you're new to SillyTavern [here are a collection of resources](https://docs.sillytavern.app/)
|
||||
If you need any support at all, feel free to reach out via [Discord](https://discord.gg/sillytavern).
|
||||
|
||||
- name: ❌ wontfix
|
||||
labeled:
|
||||
issue:
|
||||
action: close
|
||||
body: >
|
||||
This ticked has been marked as 'wontfix', which usually means it is out-of-scope, or not feasible at this time.
|
||||
You can still fork the project and make the changes yourself.
|
||||
|
||||
- name: ✅ Fixed
|
||||
labeled:
|
||||
issue:
|
||||
body: >
|
||||
Hello @{{ issue.user.login }}! It looks like all or part of this issue has now been implemented.
|
||||
|
||||
|
||||
- name: ‼️ High Priority
|
||||
labeled:
|
||||
issue:
|
||||
body: >
|
||||
This ticket has been marked as high priority, and has been bumped to the top of the priority list.
|
||||
You should expect an implementation to be pushed out soon. Thank you for your patience.
|
||||
|
||||
- name: 💀 Spam
|
||||
labeled:
|
||||
issue:
|
||||
action: close
|
||||
locking: lock
|
||||
lock_reason: spam
|
||||
body: >
|
||||
This issue has been identified as spam, and is now locked.
|
||||
Users who repeatedly raise spam issues may be blocked or reported.
|
||||
|
||||
- name: ⛔ Don't Merge
|
||||
labeled:
|
||||
pr:
|
||||
body: This PR has been temporarily blocked from merging.
|
28
.github/workflows/add-comment-from-tag.yml
vendored
Normal file
28
.github/workflows/add-comment-from-tag.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
# Based on a label applied to an issue, the bot will add a comment with some additional info
|
||||
|
||||
name: 🎯 Auto-Reply to Labeled Tickets
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Label Commenter
|
||||
uses: peaceiris/actions-label-commenter@v1
|
||||
with:
|
||||
config_file: .github/issue-auto-comments.yml
|
||||
github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
16
.github/workflows/check-merge-conflicts.yml
vendored
Normal file
16
.github/workflows/check-merge-conflicts.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
# Detect and label pull requests that have merge conflicts
|
||||
name: 🏗️ Check Merge Conflicts
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- staging
|
||||
jobs:
|
||||
check-conflicts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mschilde/auto-label-merge-conflicts@master
|
||||
with:
|
||||
CONFLICT_LABEL_NAME: "🚫 Merge Conflicts"
|
||||
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
MAX_RETRIES: 5
|
||||
WAIT_MS: 5000
|
82
.github/workflows/close-stale-issues.yml
vendored
Normal file
82
.github/workflows/close-stale-issues.yml
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
# Closes any issues that no longer have user interaction
|
||||
name: 🎯 Close Stale Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Runs every day at midnight UTC
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Comment on, then close issues that haven't been updated for ages
|
||||
- name: Close Stale Issues
|
||||
uses: actions/stale@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 360
|
||||
days-before-close: 5
|
||||
operations-per-run: 30
|
||||
remove-stale-when-updated: true
|
||||
enable-statistics: true
|
||||
stale-issue-message: >
|
||||
This issue has gone 3 months without an update. To keep the ticket open, please indicate that it is still relevant in a comment below.
|
||||
Otherwise it will be closed in 5 working days.
|
||||
stale-pr-message: >
|
||||
This PR is stale because it has been open 6 weeks with no activity. Either remove the stale label or comment below with a short update,
|
||||
otherwise this PR will be closed in 5 days.
|
||||
close-issue-message: >
|
||||
This issue was automatically closed because it has been stalled for over 1 year with no activity.
|
||||
close-pr-message: >
|
||||
This pull request was automatically closed because it has been stalled for over 1 year with no activity.
|
||||
stale-issue-label: '⚰️ Stale'
|
||||
close-issue-label: '🕸️ Inactive'
|
||||
stale-pr-label: '⚰️ Stale'
|
||||
close-pr-label: '🕸️ Inactive'
|
||||
exempt-issue-labels: '📌 Keep Open'
|
||||
exempt-pr-labels: '📌 Keep Open'
|
||||
labels-to-add-when-unstale: '📌 Keep Open'
|
||||
|
||||
# Comment on, then close issues that required a response from the user, but didn't get one
|
||||
- name: Close Issues without Response
|
||||
uses: actions/stale@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 5
|
||||
days-before-close: 3
|
||||
operations-per-run: 30
|
||||
remove-stale-when-updated: true
|
||||
stale-issue-message: >
|
||||
Hi! Looks like additional info is required for this issue to be addressed.
|
||||
Don't forget to provide this within the next few days to keep your ticket open.
|
||||
close-issue-message: 'Issue closed due to no response from user.'
|
||||
only-labels: '🚏 Awaiting User Response'
|
||||
labels-to-remove-when-unstale: '🚏 Awaiting User Response, 🛑 No Response'
|
||||
stale-issue-label: '🛑 No Response'
|
||||
close-issue-label: '🕸️ Inactive'
|
||||
exempt-issue-labels: '📌 Keep Open'
|
||||
exempt-pr-labels: '📌 Keep Open'
|
||||
|
||||
# Comment on issues that we should have replied to
|
||||
- name: Notify Repo Owner to Respond
|
||||
uses: actions/stale@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 7
|
||||
days-before-close: 365
|
||||
operations-per-run: 30
|
||||
remove-stale-when-updated: true
|
||||
stale-issue-message: Hey SillyTavern, - Don't forget to respond!
|
||||
stale-pr-message: Hey SillyTavern, - Don't forget to respond!
|
||||
only-labels: '👤 Awaiting Maintainer Response'
|
||||
labels-to-remove-when-unstale: '👤 Awaiting Maintainer Response'
|
||||
close-issue-message: 'Closed due to no response from repo author for over a year'
|
||||
close-pr-message: 'Closed due to no response from repo author for over a year'
|
||||
stale-issue-label: '👤 Awaiting Maintainer Response'
|
||||
stale-pr-label: '👤 Awaiting Maintainer Response'
|
||||
close-issue-label: '🕸️ Inactive'
|
||||
close-pr-label: '🕸️ Inactive'
|
||||
exempt-issue-labels: '📌 Keep Open'
|
||||
exempt-pr-labels: '📌 Keep Open'
|
39
.github/workflows/get-pr-size.yml
vendored
Normal file
39
.github/workflows/get-pr-size.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# Adds a comment to new PRs, showing the compressed size and size difference of new code
|
||||
# And also labels the PR based on the number of lines changes
|
||||
|
||||
name: 🌈 Check PR Size
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
# Find and comment with compressed size
|
||||
- name: Get Compressed Size
|
||||
uses: preactjs/compressed-size-action@v2
|
||||
with:
|
||||
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
pattern: './dist/**/*.{js,css,html}'
|
||||
strip-hash: '\\b\\w{8}\\.'
|
||||
exclude: '**/node_modules/**'
|
||||
minimum-change-threshold: 100
|
||||
# Check number of lines of code added
|
||||
- name: Label based on Lines of Code
|
||||
uses: codelytv/pr-size-labeler@v1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
xs_max_size: '10'
|
||||
s_max_size: '100'
|
||||
m_max_size: '500'
|
||||
l_max_size: '1000'
|
||||
s_label: '🟩 PR - Small'
|
||||
m_label: '🟨 PR - Medium'
|
||||
l_label: '🟧 PR - Large'
|
||||
xl_label: '🟥 PR - XL'
|
||||
fail_if_xl: 'false'
|
||||
message_if_xl: >
|
||||
It looks like this PR is very large (over 1000 lines).
|
||||
Try to avoid addressing multiple issues in a single PR, and
|
||||
in the future consider breaking large tasks down into smaller steps.
|
||||
This it to make reviewing, testing, reverting and general quality management easier.
|
17
.github/workflows/manage-pending-labels-closed.yml
vendored
Normal file
17
.github/workflows/manage-pending-labels-closed.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# When a new comment is added to an issue, if it had the Stale or Awaiting User Response labels, then those labels will be removed
|
||||
|
||||
name: 🎯 Remove Pending Labels on Close
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
jobs:
|
||||
remove-labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remove Labels when Closed
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: remove-labels
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: '🚏 Awaiting User Response,⚰️ Stale,👤 Awaiting Maintainer Response'
|
42
.github/workflows/manage-pending-labels.yml
vendored
Normal file
42
.github/workflows/manage-pending-labels.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# When a new comment is added to an issue, if it had the Stale or Awaiting User Response labels, then those labels will be removed
|
||||
|
||||
name: 🎯 Add/ Remove Awaiting Response Labels
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
remove-stale:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.comment.author_association != 'COLLABORATOR' && github.event.comment.author_association != 'OWNER' }}
|
||||
steps:
|
||||
- name: Remove Stale labels when Updated
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: remove-labels
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: '🚏 Awaiting User Response,⚰️ Stale'
|
||||
|
||||
add-awaiting-author:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{!github.event.issue.pull_request && github.event.comment.author_association != 'COLLABORATOR' && github.event.comment.author_association != 'OWNER' && github.event.issue.state == 'open' }}
|
||||
steps:
|
||||
- name: Add Awaiting Author labels when Updated
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: add-labels
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: '👤 Awaiting Maintainer Response'
|
||||
|
||||
remove-awaiting-author:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.comment.author_association == 'OWNER' }}
|
||||
steps:
|
||||
- name: Remove Awaiting Author labels when Updated
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: remove-labels
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: '👤 Awaiting Maintainer Response'
|
@ -539,6 +539,10 @@
|
||||
"filename": "presets/context/Llama 3 Instruct.json",
|
||||
"type": "context"
|
||||
},
|
||||
{
|
||||
"filename": "presets/context/Phi.json",
|
||||
"type": "context"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/Adventure.json",
|
||||
"type": "instruct"
|
||||
@ -631,6 +635,10 @@
|
||||
"filename": "presets/instruct/Llama 3 Instruct.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/Phi.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/moving-ui/Default.json",
|
||||
"type": "moving_ui"
|
||||
@ -642,5 +650,21 @@
|
||||
{
|
||||
"filename": "presets/quick-replies/Default.json",
|
||||
"type": "quick_replies"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/Llama-3-Instruct-Names.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/instruct/ChatML-Names.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/context/Llama-3-Instruct-Names.json",
|
||||
"type": "context"
|
||||
},
|
||||
{
|
||||
"filename": "presets/context/ChatML-Names.json",
|
||||
"type": "context"
|
||||
}
|
||||
]
|
||||
|
12
default/content/presets/context/ChatML-Names.json
Normal file
12
default/content/presets/context/ChatML-Names.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"story_string": "<|im_start|>system\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|im_end|>",
|
||||
"example_separator": "",
|
||||
"chat_start": "",
|
||||
"use_stop_strings": false,
|
||||
"allow_jailbreak": false,
|
||||
"always_force_name2": true,
|
||||
"trim_sentences": false,
|
||||
"include_newline": false,
|
||||
"single_line": false,
|
||||
"name": "ChatML-Names"
|
||||
}
|
12
default/content/presets/context/Llama-3-Instruct-Names.json
Normal file
12
default/content/presets/context/Llama-3-Instruct-Names.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"story_string": "<|start_header_id|>system<|end_header_id|>\n\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|eot_id|>",
|
||||
"example_separator": "",
|
||||
"chat_start": "",
|
||||
"use_stop_strings": false,
|
||||
"allow_jailbreak": false,
|
||||
"always_force_name2": true,
|
||||
"trim_sentences": false,
|
||||
"include_newline": false,
|
||||
"single_line": false,
|
||||
"name": "Llama-3-Instruct-Names"
|
||||
}
|
12
default/content/presets/context/Phi.json
Normal file
12
default/content/presets/context/Phi.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"story_string": "<|system|>\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|end|>\n",
|
||||
"example_separator": "",
|
||||
"chat_start": "",
|
||||
"use_stop_strings": false,
|
||||
"allow_jailbreak": false,
|
||||
"always_force_name2": true,
|
||||
"trim_sentences": false,
|
||||
"include_newline": false,
|
||||
"single_line": false,
|
||||
"name": "Phi"
|
||||
}
|
24
default/content/presets/instruct/ChatML-Names.json
Normal file
24
default/content/presets/instruct/ChatML-Names.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.",
|
||||
"input_sequence": "<|im_start|>[{{name}}]",
|
||||
"output_sequence": "<|im_start|>[{{name}}]",
|
||||
"last_output_sequence": "",
|
||||
"system_sequence": "<|im_start|>system",
|
||||
"stop_sequence": "<|im_end|>",
|
||||
"wrap": true,
|
||||
"macro": true,
|
||||
"names": false,
|
||||
"names_force_groups": false,
|
||||
"activation_regex": "",
|
||||
"system_sequence_prefix": "",
|
||||
"system_sequence_suffix": "",
|
||||
"first_output_sequence": "",
|
||||
"skip_examples": false,
|
||||
"output_suffix": "<|im_end|>\n",
|
||||
"input_suffix": "<|im_end|>\n",
|
||||
"system_suffix": "<|im_end|>\n",
|
||||
"user_alignment_message": "",
|
||||
"system_same_as_user": false,
|
||||
"last_system_sequence": "",
|
||||
"name": "ChatML-Names"
|
||||
}
|
24
default/content/presets/instruct/Llama-3-Instruct-Names.json
Normal file
24
default/content/presets/instruct/Llama-3-Instruct-Names.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"system_prompt": "You are an expert actor that can fully immerse yourself into any role given. You do not break character for any reason, even if someone tries addressing you as an AI or language model. Currently your role is {{char}}, which is described in detail below. As {{char}}, continue the exchange with {{user}}.",
|
||||
"input_sequence": "<|start_header_id|>[{{name}}]<|end_header_id|>\n\n",
|
||||
"output_sequence": "<|start_header_id|>[{{name}}]<|end_header_id|>\n\n",
|
||||
"last_output_sequence": "",
|
||||
"system_sequence": "<|start_header_id|>system<|end_header_id|>\n\n",
|
||||
"stop_sequence": "<|eot_id|>",
|
||||
"wrap": false,
|
||||
"macro": true,
|
||||
"names": false,
|
||||
"names_force_groups": false,
|
||||
"activation_regex": "",
|
||||
"system_sequence_prefix": "",
|
||||
"system_sequence_suffix": "",
|
||||
"first_output_sequence": "",
|
||||
"skip_examples": false,
|
||||
"output_suffix": "<|eot_id|>",
|
||||
"input_suffix": "<|eot_id|>",
|
||||
"system_suffix": "<|eot_id|>",
|
||||
"user_alignment_message": "",
|
||||
"system_same_as_user": true,
|
||||
"last_system_sequence": "",
|
||||
"name": "Llama-3-Instruct-Names"
|
||||
}
|
24
default/content/presets/instruct/Phi.json
Normal file
24
default/content/presets/instruct/Phi.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.",
|
||||
"input_sequence": "<|user|>\n",
|
||||
"output_sequence": "<|assistant|>\n",
|
||||
"first_output_sequence": "",
|
||||
"last_output_sequence": "",
|
||||
"system_sequence_prefix": "",
|
||||
"system_sequence_suffix": "",
|
||||
"stop_sequence": "<|end|>",
|
||||
"wrap": false,
|
||||
"macro": true,
|
||||
"names": true,
|
||||
"names_force_groups": true,
|
||||
"activation_regex": "",
|
||||
"skip_examples": false,
|
||||
"output_suffix": "<|end|>\n",
|
||||
"input_suffix": "<|end|>\n",
|
||||
"system_sequence": "<|system|>\n",
|
||||
"system_suffix": "<|end|>\n",
|
||||
"user_alignment_message": "",
|
||||
"last_system_sequence": "",
|
||||
"system_same_as_user": false,
|
||||
"name": "Phi"
|
||||
}
|
6
public/css/brands.min.css
vendored
Normal file
6
public/css/brands.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -204,3 +204,7 @@ input.extension_missing[type="checkbox"] {
|
||||
#extensionsMenu>#translate_chat {
|
||||
order: 7;
|
||||
}
|
||||
|
||||
#extensionsMenu>#translate_input_message {
|
||||
order: 8;
|
||||
}
|
||||
|
8488
public/css/fontawesome.css
vendored
8488
public/css/fontawesome.css
vendored
File diff suppressed because it is too large
Load Diff
9
public/css/fontawesome.min.css
vendored
Normal file
9
public/css/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -309,6 +309,10 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
body.waifuMode .zoomed_avatar_container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body.waifuMode .zoomed_avatar {
|
||||
width: fit-content;
|
||||
max-height: calc(60vh - 60px);
|
||||
|
@ -1,24 +0,0 @@
|
||||
:root,
|
||||
:host {
|
||||
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype");
|
||||
}
|
||||
|
||||
.fas,
|
||||
.fa-solid {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
6
public/css/solid.min.css
vendored
Normal file
6
public/css/solid.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
|
@ -19,7 +19,8 @@ body.no-timer .mes_timer,
|
||||
body.no-timestamps .timestamp,
|
||||
body.no-tokenCount .tokenCounterDisplay,
|
||||
body.no-mesIDDisplay .mesIDDisplay,
|
||||
body.no-modelIcons .icon-svg {
|
||||
body.no-modelIcons .icon-svg,
|
||||
body.hideChatAvatars .mesAvatarWrapper .avatar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@ -123,10 +124,16 @@ body.charListGrid #rm_print_characters_block .bogus_folder_select_back .avatar {
|
||||
}
|
||||
|
||||
/* Hack for keeping the spacing */
|
||||
/*
|
||||
body.charListGrid #rm_print_characters_block .ch_add_placeholder {
|
||||
display: flex !important;
|
||||
opacity: 0;
|
||||
}
|
||||
*/
|
||||
|
||||
body.charListGrid #rm_print_characters_block .ch_additional_info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*big avatars mode page-wide changes*/
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@
|
||||
"LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Перед этим обязательно выберите подходящий токенизатор.\nПоследовательности, которых не должно быть на выходе.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счетчик токенов, если не уверены.",
|
||||
"Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]",
|
||||
"Classifier Free Guidance. More helpful tip coming soon": "Classifier Free Guidance. Чуть позже опишем более подробно",
|
||||
"Scale": "Масштаб",
|
||||
"Scale": "Scale",
|
||||
"GBNF Grammar": "Грамматика GBNF",
|
||||
"Usage Stats": "Статистика исп.",
|
||||
"Click for stats!": "Нажмите для получения статистики!",
|
||||
@ -97,7 +97,7 @@
|
||||
"Sequences you don't want to appear in the output. One per line.": "Строки, которых не должно быть в выходном тексте. По одной на строчку.",
|
||||
"AI Module": "Модуль ИИ",
|
||||
"Changes the style of the generated text.": "Изменяет стиль создаваемого текста.",
|
||||
"Used if CFG Scale is unset globally, per chat or character": "Используется, если масштаб CFG не установлен глобально, для каждого чата или персонажа.",
|
||||
"Used if CFG Scale is unset globally, per chat or character": "Используется, если CFG Scale не установлен глобально, для каждого чата или персонажа.",
|
||||
"Inserts jailbreak as a last system message.": "Вставлять JailBreak последним системным сообщением.",
|
||||
"This tells the AI to ignore its usual content restrictions.": "Сообщает AI о необходимости игнорировать стандартные ограничения контента.",
|
||||
"NSFW Encouraged": "Поощрять NSFW",
|
||||
@ -262,7 +262,7 @@
|
||||
"Auto-Continue": "Авто-продолжение",
|
||||
"Collapse Consecutive Newlines": "Сворачивать последовательные новые строки",
|
||||
"Allow for Chat Completion APIs": "Разрешить для API Chat Completion",
|
||||
"Target length (tokens)": "Целевая длина (токены)",
|
||||
"Target length (tokens)": "Целевая длина (в токенах)",
|
||||
"Keep Example Messages in Prompt": "Сохранять примеры сообщений в промпте",
|
||||
"Remove Empty New Lines from Output": "Удалять пустые строчки из вывода",
|
||||
"Disabled for all models": "Выключено для всех моделей",
|
||||
@ -300,11 +300,11 @@
|
||||
"Chat Style": "Стиль чата",
|
||||
"Default": "По умолчанию",
|
||||
"Bubbles": "Пузыри",
|
||||
"No Blur Effect": "Отключить эффект размытия",
|
||||
"No Text Shadows": "Отключить тень от текста",
|
||||
"No Blur Effect": "Отключить размытие",
|
||||
"No Text Shadows": "Отключить тень текста",
|
||||
"Waifu Mode": "Рeжим Вайфу",
|
||||
"Message Timer": "Таймер сообщений",
|
||||
"Model Icon": "Показать значки модели",
|
||||
"Model Icon": "Значки моделей",
|
||||
"# of messages (0 = disabled)": "# сообщений (0 = отключено)",
|
||||
"Advanced Character Search": "Расширенный поиск по персонажам",
|
||||
"Allow {{char}}: in bot messages": "Показывать {{char}}: в ответах",
|
||||
@ -314,7 +314,7 @@
|
||||
"Lorebook Import Dialog": "Показывать окно импорта лорбука",
|
||||
"MUI Preset": "Пресет MUI:",
|
||||
"If set in the advanced character definitions, this field will be displayed in the characters list.": "Если это поле задано в расширенных параметрах персонажа, оно будет отображаться в списке персонажей.",
|
||||
"Relaxed API URLS": "Смягченные URL-адреса API",
|
||||
"Relaxed API URLS": "Смягчённые адреса API",
|
||||
"Custom CSS": "Пользовательский CSS",
|
||||
"Default (oobabooga)": "По умолчанию (oobabooga)",
|
||||
"Mancer Model": "Модель Mancer",
|
||||
@ -381,7 +381,7 @@
|
||||
"text": "текст",
|
||||
"Delete": "Удалить",
|
||||
"Cancel": "Отменить",
|
||||
"Advanced Defininitions": "Продвинутое описание",
|
||||
"Advanced Defininitions": "Расширенное описание",
|
||||
"Personality summary": "Сводка по личности",
|
||||
"A brief description of the personality": "Краткое описание личности",
|
||||
"Scenario": "Сценарий",
|
||||
@ -431,7 +431,7 @@
|
||||
"JSON": "JSON",
|
||||
"presets": "Пресеты",
|
||||
"Message Sound": "Звук сообщения",
|
||||
"Author's Note": "Пометки автора",
|
||||
"Author's Note": "Заметки автора",
|
||||
"Send Jailbreak": "Отправлять джейлбрейк",
|
||||
"Replace empty message": "Заменять пустые сообщения",
|
||||
"Send this text instead of nothing when the text box is empty.": "Этот текст будет отправлен в случае отсутствия текста на отправку.",
|
||||
@ -475,7 +475,7 @@
|
||||
"--- Pick to Edit ---": "--- Выберите для редактирования ---",
|
||||
"or": "или",
|
||||
"New": "Новый",
|
||||
"Priority": "Приритет",
|
||||
"Priority": "Приоритет",
|
||||
"Custom": "Пользовательский",
|
||||
"Title A-Z": "Название от A до Z",
|
||||
"Title Z-A": "Название от Z до A",
|
||||
@ -528,7 +528,7 @@
|
||||
"UI Border": "Границы UI",
|
||||
"Chat Style:": "Стиль чата",
|
||||
"Chat Width (PC)": "Ширина чата (для ПК)",
|
||||
"Chat Timestamps": "Временные метки в чате",
|
||||
"Chat Timestamps": "Метки времени в чате",
|
||||
"Tags as Folders": "Теги как папки",
|
||||
"Chat Truncation": "Усечение чата",
|
||||
"(0 = unlimited)": "(0 = неограниченное)",
|
||||
@ -559,8 +559,8 @@
|
||||
"Disables animations and transitions": "Отключение анимаций и переходов.",
|
||||
"removes blur from window backgrounds": "Убрать размытие с фона окон, чтобы ускорить рендеринг.",
|
||||
"Remove text shadow effect": "Удаление эффекта тени от текста.",
|
||||
"Reduce chat height, and put a static sprite behind the chat window": "Уменьшитm высоту чата и поместить статичный спрайт за окном чата.",
|
||||
"Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список контекстных элементов 'Действия с сообщением' для сообщений чата, а не прятать их за '...'.",
|
||||
"Reduce chat height, and put a static sprite behind the chat window": "Уменьшить высоту чата и поместить статичный спрайт за окном чата.",
|
||||
"Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список действий с сообщением, а не прятать их за '...'.",
|
||||
"Alternative UI for numeric sampling parameters with fewer steps": "Альтернативный пользовательский интерфейс для числовых параметров выборки с меньшим количеством шагов.",
|
||||
"Entirely unrestrict all numeric sampling parameters": "Полностью разграничить все числовые параметры выборки.",
|
||||
"Time the AI's message generation, and show the duration in the chat log": "Время генерации сообщений ИИ и его показ в журнале чата.",
|
||||
@ -600,7 +600,7 @@
|
||||
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.",
|
||||
"If the generated message is shorter than this, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",
|
||||
"Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",
|
||||
"Auto-Expand Message Actions": "Развернуть контекстные элементы",
|
||||
"Auto-Expand Message Actions": "Развернуть действия",
|
||||
"Not Connected": "Не подключено",
|
||||
"Persona Management": "Управление персоной",
|
||||
"Persona Description": "Описание персоны",
|
||||
@ -629,16 +629,15 @@
|
||||
"Most chats": "Больше всего чатов",
|
||||
"Least chats": "Меньше всего чатов",
|
||||
"Back": "Назад",
|
||||
"Prompt Overrides (For OpenAI/Claude/Scale APIs, Window/OpenRouter, and Instruct mode)": "Перезапись промпта (Для OpenAI/Claude/Scale API, Window/OpenRouter, и режима Instruct)",
|
||||
"Insert {{original}} into either box to include the respective default prompt from system settings.": "Введите {{original}} в любое поле, чтобы использовать соответствующий промпт из системных настроек",
|
||||
"Prompt Overrides": "Индивидуальный промпт",
|
||||
"(For OpenAI/Claude/Scale APIs, Window/OpenRouter, and Instruct Mode)": "(для API OpenAI/Claude/Scale, Window/OpenRouter, а также режима Instruct)",
|
||||
"Insert {{original}} into either box to include the respective default prompt from system settings.": "Введите {{original}} в любое поле, чтобы вставить соответствующий промпт из системных настроек",
|
||||
"Main Prompt": "Основной промпт",
|
||||
"Jailbreak": "Джейлбрейк",
|
||||
"Creator's Metadata (Not sent with the AI prompt)": "Метаданные (не отправляются ИИ)",
|
||||
"Everything here is optional": "Все поля необязательные",
|
||||
"Created by": "Автор",
|
||||
"Character Version": "Версия персонажа",
|
||||
"Tags to Embed": "Встраиваемые теги",
|
||||
"How often the character speaks in group chats!": "Как часто персонаж говорит в групповых чатах",
|
||||
"Important to set the character's writing style.": "Серьёзно влияет на стиль письма персонажа.",
|
||||
"ATTENTION!": "ВНИМАНИЕ!",
|
||||
"Samplers Order": "Порядок сэмплеров",
|
||||
@ -655,7 +654,7 @@
|
||||
"Use 'Unlocked Context' to enable chunked generation.": "Использовать 'Неограниченный контекст' для активации кусочной генерации",
|
||||
"It extends the context window in exchange for reply generation speed.": "Увеличивает размер контекста в обмен на скорость генерации.",
|
||||
"Continue": "Продолжить",
|
||||
"CFG Scale": "Масштаб CFG",
|
||||
"CFG Scale": "CFG Scale",
|
||||
"Editing:": "Изменения",
|
||||
"AI reply prefix": "Префикс для ответа ИИ",
|
||||
"Custom Stopping Strings": "Стоп-строки",
|
||||
@ -671,9 +670,9 @@
|
||||
"Chat Name (Optional)": "Название чата (необязательно)",
|
||||
"Filter...": "Фильтры...",
|
||||
"Search...": "Поиск...",
|
||||
"Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "Все содержание этой ячейки будет заменять стандартный Промт",
|
||||
"Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "Все содержание этой ячейки будет заменять стандартный Джейлбрейк",
|
||||
"(Botmaker's name / Contact Info)": "(Имя автора / Контакты)",
|
||||
"Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "Все содержимое этого поля будет заменять стандартный промпт",
|
||||
"Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "Все содержимое этого поля будет заменять стандартный джейлбрейк",
|
||||
"(Botmaker's name / Contact Info)": "(Имя автора, контакты)",
|
||||
"(If you want to track character versions)": "Если вы хотите отслеживать версии персонажа",
|
||||
"(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(Описание персонажа, советы по использованию, список моделей, на которых он тестировался. Информация будет отображаться в списке персонажей)",
|
||||
"(Write a comma-separated list of tags)": "(Список тегов через запятую)",
|
||||
@ -713,12 +712,12 @@
|
||||
"Restore defaul note": "Восстановить стандартную заметку",
|
||||
"API Connections": "Соединения с API",
|
||||
"Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "Может помочь с плохими ответами ставя в очередь только подтвержденных работников. Может замедлить время ответа.",
|
||||
"Clear your API key": "Очистите свой ключ от API",
|
||||
"Clear your API key": "Стереть ключ от API",
|
||||
"Refresh models": "Обновить модели",
|
||||
"Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Получите свой OpenRouter API токен используя OAuth. У вас будет открыта вкладка openrouter.ai",
|
||||
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Проверка работоспособности вашего соединения с API. Знайте, что оно будет отправлено от вашего лица.",
|
||||
"Create New": "Создать новое",
|
||||
"Edit": "Изменить",
|
||||
"Edit": "Редактировать",
|
||||
"Locked = World Editor will stay open": "Закреплено = Редактор мира останется открытым",
|
||||
"Entries can activate other entries by mentioning their keywords": "Записи могут активировать другие записи, если в них содержатся ключевые слова",
|
||||
"Lookup for the entry keys in the context will respect the case": "Большая буква имеет значение при активации ключевого слова",
|
||||
@ -847,7 +846,7 @@
|
||||
"Underlined Text": "Подчёркнутый",
|
||||
"Token Probabilities": "Вероятности токенов",
|
||||
"Close chat": "Закрыть чат",
|
||||
"Manage chat files": "Управление файлами чата",
|
||||
"Manage chat files": "Управление чатами",
|
||||
"Import Extension From Git Repo": "Импортировать расширение из Git Repository",
|
||||
"Install extension": "Установить расширение",
|
||||
"Manage extensions": "Управление расширениями",
|
||||
@ -863,12 +862,12 @@
|
||||
"When this is off, responses will be displayed all at once when they are complete.": "Если параметр выключен, ответы будут отображаться сразу целиком, и только после полного завершения генерации.",
|
||||
"Quick Prompts Edit": "Быстрое редактирование промптов",
|
||||
"Enable OpenAI completion streaming": "Включить стриминг OpenAI",
|
||||
"Main": "Главное",
|
||||
"Main": "Основной",
|
||||
"Utility Prompts": "Служебные промпты",
|
||||
"Add character names": "Добавить имена персонажей",
|
||||
"Send names in the message objects. Helps the model to associate messages with characters.": "Отправить имена в объектах сообщений. Помогает модели ассоциировать сообщения с персонажами.",
|
||||
"Continue prefill": "Префилл для продолжения",
|
||||
"Continue sends the last message as assistant role instead of system message with instruction.": "Продолжение отправляет последнее сообщение в роли ассистента, а не системное сообщение с инструкцией.",
|
||||
"Continue sends the last message as assistant role instead of system message with instruction.": "Продолжение отправляет последнее сообщение в роли ассистента, вместо системного сообщения с инструкцией.",
|
||||
"Squash system messages": "Склеивать сообщения системыы",
|
||||
"Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "Объединяет последовательные системные сообщения в одно (за исключением примеров диалогов). Может улучшить согласованность для некоторых моделей.",
|
||||
"Send inline images": "Отправлять встроенные изображения",
|
||||
@ -973,12 +972,128 @@
|
||||
"Most tokens have a leading space.": "У большинства токенов в начале пробел.",
|
||||
"Prompts": "Промпты",
|
||||
"Text or token ids": "Текст или [идентификаторы токенов]",
|
||||
"World Info Format Template": "Шаблон форматирования информации о мире",
|
||||
"World Info Format Template": "Шаблон оформления информации о мире",
|
||||
"Wraps activated World Info entries before inserting into the prompt.": "Дополняет информацию об активном на данный момент мире перед её отправкой в промпт.",
|
||||
"Doesn't work? Try adding": "Не работает? Попробуйте добавить в конце",
|
||||
"at the end!": "!",
|
||||
"Authorize": "Авторизоваться",
|
||||
"No persona description": "[Нет описания]",
|
||||
"Not connected to API!": "Нет соединения с API!",
|
||||
"Type a message, or /? for help": "Введите сообщение, или /? для получения справки по командам"
|
||||
"Type a message, or /? for help": "Введите сообщение, или /? для получения справки по командам",
|
||||
"Welcome to SillyTavern!": "Добро пожаловать в SillyTavern!",
|
||||
"Won't be shared with the character card on export.": "Не попадут в карточку персонажа при экспорте.",
|
||||
"Web-search": "Веб-поиск",
|
||||
"Persona Name:": "Имя персоны:",
|
||||
"User first message": "Первое сообщение пользователя",
|
||||
"extension_token_counter": "Токенов:",
|
||||
"Character's Note": "Заметка о персонаже",
|
||||
"(Text to be inserted in-chat @ designated depth and role)": "Этот текст будет вставлен в чат на заданную глубину и с определённой ролью",
|
||||
"@ Depth": "Глубина",
|
||||
"Role": "Роль",
|
||||
"System": "Система",
|
||||
"User": "Пользователь",
|
||||
"Assistant": "Ассистент",
|
||||
"How often the character speaks in": "Как часто персонаж говорит в",
|
||||
"group chats!": "групповых чатах!",
|
||||
"Creator's Metadata": "Метаданные",
|
||||
"(Not sent with the AI Prompt)": "(не отправляются ИИ)",
|
||||
"New Chat": "Новый чат",
|
||||
"Import Chat": "Импорт чата",
|
||||
"Chat Lore": "Лор чата",
|
||||
"Chat Lorebook for": "Лорбук для чата",
|
||||
"A selected World Info will be bound to this chat.": "Выбранный мир будет привязан к этому чату. При генерации ответа ИИ он будет совмещён с записями из глобального лорбука и лорбука персонажа.",
|
||||
"Missing key": "❌ Ключа нет",
|
||||
"Key saved": "✔️ Ключ сохранён",
|
||||
"Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.": "Использовать токенайзер для моделей Jurassic, эффективнее GPT-токенайзера",
|
||||
"Use system prompt (Gemini 1.5 pro+ only)": "Использовать системный промпт (только для Gemini 1.5 pro и выше)",
|
||||
"Experimental feature. May not work for all backends.": "Экспериментальная возможность, на некоторых бэкендах может не работать.",
|
||||
"Avatar Hover Magnification": "Зум аватарки по наведению",
|
||||
"Enable magnification for zoomed avatar display.": "Добавляет возможность приближать увеличенную версию аватарки.",
|
||||
"Unique to this chat": "Только для текущего чата",
|
||||
"Checkpoints inherit the Note from their parent, and can be changed individually after that.": "Чекпоинты наследуют заметки от родительского чата, но впоследствие их всегда можно изменить.",
|
||||
"Include in World Info Scanning": "Учитывать при сканировании Информации о мире",
|
||||
"Before Main Prompt / Story String": "Перед основным промптом / строкой истории",
|
||||
"After Main Prompt / Story String": "После основного промпта / строки истории",
|
||||
"In-chat @ Depth": "Встав. на глуб.",
|
||||
"as": "роль:",
|
||||
"Insertion Frequency": "Частота вставки",
|
||||
"(0 = Disable, 1 = Always)": "(0 = никогда, 1 = всегда)",
|
||||
"User inputs until next insertion:": "Ваших сообщений до след. вставки:",
|
||||
"Character Author's Note (Private)": "Заметки автора персонажа (личные)",
|
||||
"Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.": "Автоматически применятся к этому персонажу в качестве заметок автора. Будут использоваться в группах, но при активном групповом чате к редактированию недоступны.",
|
||||
"Use character author's note": "Использовать заметки автора персонажа",
|
||||
"Replace Author's Note": "Вместо заметок автора",
|
||||
"Top of Author's Note": "Сверху от заметок автора",
|
||||
"Bottom of Author's Note": "Снизу от заметок автора",
|
||||
"Default Author's Note": "Стандартные заметки автора",
|
||||
"Will be automatically added as the Author's Note for all new chats.": "Будут автоматически добавляться во все новые чаты в качестве Заметок автора",
|
||||
"1 = disabled": "1 = откл.",
|
||||
"write short replies, write replies using past tense": "пиши короткие ответы, пиши в настоящем времени",
|
||||
"Positive Prompt": "Положительный промпт",
|
||||
"Character CFG": "CFG для персонажа",
|
||||
"Will be automatically added as the CFG for this character.": "Автоматически применится к персонажу как его CFG.",
|
||||
"Global CFG": "Глобальный CFG",
|
||||
"Will be used as the default CFG options for every chat unless overridden.": "Будет применяться как стандартный CFG для всех чатов, если не указаны индивидуальные настройки.",
|
||||
"CFG Prompt Cascading": "Совмещение CFG-промптов",
|
||||
"Combine positive/negative prompts from other boxes.": "Комбинировать различные положительные и негативные промпты.",
|
||||
"For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "К примеру, если отметить галочки с чатом, персонажем и глобальной настройкой, то все эти негативы соберутся в одну строку, разделённую запятыми.",
|
||||
"Always Include": "Всегда применять",
|
||||
"Chat Negatives": "Негативы от чата",
|
||||
"Character Negatives": "Негативы от персонажа",
|
||||
"Global Negatives": "Глобальные негативы",
|
||||
"Custom Separator:": "Кастомный разделитель:",
|
||||
"Insertion Depth:": "Глубина вставки:",
|
||||
"Chat CFG": "CFG для чата",
|
||||
"Chat backgrounds generated with the": "Здесь будут появляться фоны, сгенерированные расширением",
|
||||
"extension will appear here.": ".",
|
||||
"Prevent further recursion (this entry will not activate others)": "Пресечь дальнейшую рекурсию (эта запись не будет активировать другие)",
|
||||
"Alert if your world info is greater than the allocated budget.": "Оповещать, если ваш мир выходит за выделенный бюджет.",
|
||||
"Convert to Persona": "Преобразовать в персону",
|
||||
"Link to Source": "Ссылка на источник",
|
||||
"Replace / Update": "Заменить / Обновить",
|
||||
"Smoothing Curve": "Кривая сглаживания",
|
||||
"Message Actions": "Действия с сообщением",
|
||||
"SillyTavern is aimed at advanced users.": "SillyTavern рассчитана на продвинутых пользователей.",
|
||||
"If you're new to this, enable the simplified UI mode below.": "Если вы новичок, советуем включить упрощённый UI.",
|
||||
"Enable simple UI mode": "Включить упрощённый UI",
|
||||
"welcome_message_part_1": "Ознакомьтесь с",
|
||||
"welcome_message_part_2": "официальной документацией",
|
||||
"welcome_message_part_3": ".",
|
||||
"welcome_message_part_4": "Введите",
|
||||
"welcome_message_part_5": "в чате, чтобы получить справку по командам и макросам.",
|
||||
"welcome_message_part_6": "Заходите на наш",
|
||||
"Discord server": "Discord-сервер,",
|
||||
"welcome_message_part_7": "там публикуется много разной полезной информации, в том числе анонсы.",
|
||||
"Before you get started, you must select a persona name.": "Для начала вам следует выбрать имя своей персоны.",
|
||||
"welcome_message_part_8": "Его можно будет изменить в любое время через иконку",
|
||||
"welcome_message_part_9": ".",
|
||||
"UI Language:": "Язык интерфейса:",
|
||||
"Ignore EOS Token": "Игнорировать EOS-токен",
|
||||
"Ignore the EOS Token even if it generates.": "Игнорировать EOS-токен, даже если он сгенерировался.",
|
||||
"Hide Muted Member Sprites": "Скрыть спрайты заглушенных участников",
|
||||
"Group generation handling mode": "Генерировать ответы путём...",
|
||||
"Swap character cards": "Подмены карточки персонажа",
|
||||
"Join character cards (exclude muted)": "Совмещения карточек (кроме заглушенных)",
|
||||
"Join character cards (include muted)": "Совмещения карточек (включая заглушенных)",
|
||||
"Click to allow/forbid the use of external media for this group.": "Нажмите, чтобы разрешить/запретить использование внешних медиа в этой группе.",
|
||||
"Scenario Format Template": "Шаблон оформления сценария",
|
||||
"scenario_format_template_part_1": "Используйте",
|
||||
"scenario_format_template_part_2": "чтобы указать, куда именно вставляется основное содержимое.",
|
||||
"Personality Format Template": "Шаблон оформления характера",
|
||||
"Group Nudge Prompt Template": "Шаблон промпта-подсказки для групп",
|
||||
"Sent at the end of the group chat history to force reply from a specific character.": "Добавляется в конец истории сообщений в групповом чате, чтобы запросить ответ от конкретного персонажа.",
|
||||
"Set at the beginning of the chat history to indicate that a new chat is about to start.": "Добавляется в начале истории сообщений в качестве указания на то, что дальше начнётся новый чат.",
|
||||
"New Group Chat": "Новый групповой чат",
|
||||
"Set at the beginning of the chat history to indicate that a new group chat is about to start.": "Добавляется в начале истории сообщений в качестве указания на то, что дальше начнётся новый групповой чат.",
|
||||
"New Example Chat": "Новый образец чата",
|
||||
"Set at the beginning of Dialogue examples to indicate that a new example chat is about to start.": "Добавляется в начале примеров диалогов в качестве указания на то, что дальше начнётся новый чат-пример.",
|
||||
"Continue nudge": "Подсказка для продолжения",
|
||||
"Set at the end of the chat history when the continue button is pressed.": "Добавляется в конец истории чата, когда отправлен запрос на продолжение текущего сообщения.",
|
||||
"Prompts": "Промпты",
|
||||
"Your Persona": "Ваша персона",
|
||||
"Continue Postfix": "Постфикс для продолжения",
|
||||
"Space": "Пробел",
|
||||
"Newline": "Новая строка",
|
||||
"Double Newline": "Две новые строки",
|
||||
"The next chunk of the continued message will be appended using this as a separator.": "Используется в качестве разделителя между уже имеющимся сообщением и его новым отрывком, при генерации продолжения"
|
||||
}
|
||||
|
151
public/script.js
151
public/script.js
@ -152,6 +152,7 @@ import {
|
||||
Stopwatch,
|
||||
isValidUrl,
|
||||
ensureImageFormatSupported,
|
||||
flashHighlight,
|
||||
} from './scripts/utils.js';
|
||||
|
||||
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
|
||||
@ -293,6 +294,17 @@ export {
|
||||
renderTemplate,
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for page to load before continuing the app initialization.
|
||||
*/
|
||||
await new Promise((resolve) => {
|
||||
if (document.readyState === 'complete') {
|
||||
resolve();
|
||||
} else {
|
||||
window.addEventListener('load', resolve);
|
||||
}
|
||||
});
|
||||
|
||||
showLoader();
|
||||
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
|
||||
document.getElementById('preloader').remove();
|
||||
@ -449,6 +461,7 @@ export const event_types = {
|
||||
CHARACTER_DUPLICATED: 'character_duplicated',
|
||||
SMOOTH_STREAM_TOKEN_RECEIVED: 'smooth_stream_token_received',
|
||||
FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
|
||||
WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
|
||||
};
|
||||
|
||||
export const eventSource = new EventEmitter();
|
||||
@ -3779,6 +3792,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
|
||||
// Fetches the combined prompt for both negative and positive prompts
|
||||
const cfgGuidanceScale = getGuidanceScale();
|
||||
const useCfgPrompt = cfgGuidanceScale && cfgGuidanceScale.value !== 1;
|
||||
|
||||
// For prompt bit itemization
|
||||
let mesSendString = '';
|
||||
@ -3786,7 +3800,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
function getCombinedPrompt(isNegative) {
|
||||
// Only return if the guidance scale doesn't exist or the value is 1
|
||||
// Also don't return if constructing the neutral prompt
|
||||
if (isNegative && (!cfgGuidanceScale || cfgGuidanceScale?.value === 1)) {
|
||||
if (isNegative && !useCfgPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3798,13 +3812,10 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
// Deep clone
|
||||
let finalMesSend = structuredClone(mesSend);
|
||||
|
||||
let cfgPrompt = {};
|
||||
if (cfgGuidanceScale && cfgGuidanceScale?.value !== 1) {
|
||||
cfgPrompt = getCfgPrompt(cfgGuidanceScale, isNegative);
|
||||
}
|
||||
|
||||
if (cfgPrompt && cfgPrompt?.value) {
|
||||
if (cfgPrompt?.depth === 0) {
|
||||
if (useCfgPrompt) {
|
||||
const cfgPrompt = getCfgPrompt(cfgGuidanceScale, isNegative);
|
||||
if (cfgPrompt.value) {
|
||||
if (cfgPrompt.depth === 0) {
|
||||
finalMesSend[finalMesSend.length - 1].message +=
|
||||
/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
|
||||
? cfgPrompt.value
|
||||
@ -3816,6 +3827,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add prompt bias after everything else
|
||||
// Always run with continue
|
||||
@ -3894,24 +3906,20 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
return !data.combinedPrompt ? combine() : data.combinedPrompt;
|
||||
}
|
||||
|
||||
// Get the negative prompt first since it has the unmodified mesSend array
|
||||
let negativePrompt = main_api == 'textgenerationwebui' ? getCombinedPrompt(true) : undefined;
|
||||
let finalPrompt = getCombinedPrompt(false);
|
||||
|
||||
// Include the entire guidance scale object
|
||||
const cfgValues = cfgGuidanceScale && cfgGuidanceScale?.value !== 1 ? ({ guidanceScale: cfgGuidanceScale, negativePrompt: negativePrompt }) : null;
|
||||
|
||||
let maxLength = Number(amount_gen); // how many tokens the AI will be requested to generate
|
||||
let thisPromptBits = [];
|
||||
|
||||
// TODO: Make this a switch
|
||||
let generate_data;
|
||||
switch (main_api) {
|
||||
case 'koboldhorde':
|
||||
case 'kobold':
|
||||
if (main_api == 'koboldhorde' && horde_settings.auto_adjust_response_length) {
|
||||
maxLength = Math.min(maxLength, adjustedParams.maxLength);
|
||||
maxLength = Math.max(maxLength, MIN_LENGTH); // prevent validation errors
|
||||
}
|
||||
|
||||
let generate_data;
|
||||
if (main_api == 'koboldhorde' || main_api == 'kobold') {
|
||||
generate_data = {
|
||||
prompt: finalPrompt,
|
||||
gui_settings: true,
|
||||
@ -3926,15 +3934,19 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context;
|
||||
generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type);
|
||||
}
|
||||
}
|
||||
else if (main_api == 'textgenerationwebui') {
|
||||
break;
|
||||
case 'textgenerationwebui': {
|
||||
const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: getCombinedPrompt(true) } : null;
|
||||
generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
|
||||
break;
|
||||
}
|
||||
else if (main_api == 'novel') {
|
||||
case 'novel': {
|
||||
const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale } : null;
|
||||
const presetSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
|
||||
generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, isContinue, cfgValues, type);
|
||||
break;
|
||||
}
|
||||
else if (main_api == 'openai') {
|
||||
case 'openai': {
|
||||
let [prompt, counts] = await prepareOpenAIMessages({
|
||||
name2: name2,
|
||||
charDescription: description,
|
||||
@ -3956,6 +3968,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
}, dryRun);
|
||||
generate_data = { prompt: prompt };
|
||||
|
||||
// TODO: move these side-effects somewhere else, so this switch-case solely sets generate_data
|
||||
// counts will return false if the user has not enabled the token breakdown feature
|
||||
if (counts) {
|
||||
parseTokenCounts(counts, thisPromptBits);
|
||||
@ -3964,6 +3977,8 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
if (!dryRun) {
|
||||
setInContextMessages(openai_messages_count, type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
@ -4010,16 +4025,14 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
userPersona: (power_user.persona_description || ''),
|
||||
};
|
||||
|
||||
thisPromptBits = additionalPromptStuff;
|
||||
|
||||
//console.log(thisPromptBits);
|
||||
const itemizedIndex = itemizedPrompts.findIndex((item) => item.mesId === thisPromptBits['mesId']);
|
||||
//console.log(additionalPromptStuff);
|
||||
const itemizedIndex = itemizedPrompts.findIndex((item) => item.mesId === additionalPromptStuff.mesId);
|
||||
|
||||
if (itemizedIndex !== -1) {
|
||||
itemizedPrompts[itemizedIndex] = thisPromptBits;
|
||||
itemizedPrompts[itemizedIndex] = additionalPromptStuff;
|
||||
}
|
||||
else {
|
||||
itemizedPrompts.push(thisPromptBits);
|
||||
itemizedPrompts.push(additionalPromptStuff);
|
||||
}
|
||||
|
||||
console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);
|
||||
@ -6798,10 +6811,7 @@ function select_rm_info(type, charId, previousCharId = null) {
|
||||
|
||||
const scrollOffset = element.offset().top - element.parent().offset().top;
|
||||
element.parent().scrollTop(scrollOffset);
|
||||
element.addClass('flash animated');
|
||||
setTimeout(function () {
|
||||
element.removeClass('flash animated');
|
||||
}, 5000);
|
||||
flashHighlight(element, 5000);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -6827,10 +6837,7 @@ function select_rm_info(type, charId, previousCharId = null) {
|
||||
const element = $(selector);
|
||||
const scrollOffset = element.offset().top - element.parent().offset().top;
|
||||
element.parent().scrollTop(scrollOffset);
|
||||
$(element).addClass('flash animated');
|
||||
setTimeout(function () {
|
||||
$(element).removeClass('flash animated');
|
||||
}, 5000);
|
||||
flashHighlight(element, 5000);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -7099,57 +7106,49 @@ function onScenarioOverrideRemoveClick() {
|
||||
* @returns
|
||||
*/
|
||||
function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
|
||||
function getOkButtonText() {
|
||||
if (['avatarToCrop'].includes(popup_type)) {
|
||||
return okButton ?? 'Accept';
|
||||
} else if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) {
|
||||
$dialoguePopupCancel.css('display', 'none');
|
||||
return okButton ?? 'Ok';
|
||||
} else if (['delete_extension'].includes(popup_type)) {
|
||||
return okButton ?? 'Ok';
|
||||
} else if (['new_chat', 'confirm'].includes(popup_type)) {
|
||||
return okButton ?? 'Yes';
|
||||
} else if (['input'].includes(popup_type)) {
|
||||
return okButton ?? 'Save';
|
||||
}
|
||||
return okButton ?? 'Delete';
|
||||
}
|
||||
|
||||
dialogueCloseStop = true;
|
||||
if (type) {
|
||||
popup_type = type;
|
||||
}
|
||||
|
||||
$('#dialogue_popup').toggleClass('wide_dialogue_popup', !!wide);
|
||||
$('#dialogue_popup').toggleClass('large_dialogue_popup', !!large);
|
||||
$('#dialogue_popup').toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling);
|
||||
$('#dialogue_popup').toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
|
||||
const $dialoguePopup = $('#dialogue_popup');
|
||||
const $dialoguePopupCancel = $('#dialogue_popup_cancel');
|
||||
const $dialoguePopupOk = $('#dialogue_popup_ok');
|
||||
const $dialoguePopupInput = $('#dialogue_popup_input');
|
||||
const $dialoguePopupText = $('#dialogue_popup_text');
|
||||
const $shadowPopup = $('#shadow_popup');
|
||||
|
||||
$('#dialogue_popup_cancel').css('display', 'inline-block');
|
||||
switch (popup_type) {
|
||||
case 'avatarToCrop':
|
||||
$('#dialogue_popup_ok').text(okButton ?? 'Accept');
|
||||
break;
|
||||
case 'text':
|
||||
case 'alternate_greeting':
|
||||
case 'char_not_selected':
|
||||
$('#dialogue_popup_ok').text(okButton ?? 'Ok');
|
||||
$('#dialogue_popup_cancel').css('display', 'none');
|
||||
break;
|
||||
case 'delete_extension':
|
||||
$('#dialogue_popup_ok').text(okButton ?? 'Ok');
|
||||
break;
|
||||
case 'new_chat':
|
||||
case 'confirm':
|
||||
$('#dialogue_popup_ok').text(okButton ?? 'Yes');
|
||||
break;
|
||||
case 'del_group':
|
||||
case 'rename_chat':
|
||||
case 'del_chat':
|
||||
default:
|
||||
$('#dialogue_popup_ok').text(okButton ?? 'Delete');
|
||||
}
|
||||
$dialoguePopup.toggleClass('wide_dialogue_popup', !!wide)
|
||||
.toggleClass('large_dialogue_popup', !!large)
|
||||
.toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling)
|
||||
.toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
|
||||
|
||||
$('#dialogue_popup_input').val(inputValue);
|
||||
$('#dialogue_popup_input').attr('rows', rows ?? 1);
|
||||
$dialoguePopupCancel.css('display', 'inline-block');
|
||||
$dialoguePopupOk.text(getOkButtonText());
|
||||
$dialoguePopupInput.toggle(popup_type === 'input').val(inputValue).attr('rows', rows ?? 1);
|
||||
$dialoguePopupText.empty().append(text);
|
||||
$shadowPopup.css('display', 'block');
|
||||
|
||||
if (popup_type == 'input') {
|
||||
$('#dialogue_popup_input').css('display', 'block');
|
||||
$('#dialogue_popup_ok').text(okButton ?? 'Save');
|
||||
}
|
||||
else {
|
||||
$('#dialogue_popup_input').css('display', 'none');
|
||||
$dialoguePopupInput.trigger('focus');
|
||||
}
|
||||
|
||||
$('#dialogue_popup_text').empty().append(text);
|
||||
$('#shadow_popup').css('display', 'block');
|
||||
if (popup_type == 'input') {
|
||||
$('#dialogue_popup_input').focus();
|
||||
}
|
||||
if (popup_type == 'avatarToCrop') {
|
||||
// unset existing data
|
||||
crop_data = undefined;
|
||||
@ -7165,7 +7164,8 @@ function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, a
|
||||
},
|
||||
});
|
||||
}
|
||||
$('#shadow_popup').transition({
|
||||
|
||||
$shadowPopup.transition({
|
||||
opacity: 1,
|
||||
duration: animation_duration,
|
||||
easing: animation_easing,
|
||||
@ -10515,6 +10515,7 @@ jQuery(async function () {
|
||||
<li>Chub Lorebook (Direct Link or ID)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
|
||||
<li>JanitorAI Character (Direct Link or UUID)<br>Example: <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
|
||||
<li>Pygmalion.chat Character (Direct Link or UUID)<br>Example: <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
|
||||
<li>AICharacterCard.com Character (Direct Link or ID)<br>Example: <tt>AICC/aicharcards/the-game-master</tt></li>
|
||||
<li>More coming soon...</li>
|
||||
<ul>`;
|
||||
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });
|
||||
|
@ -776,7 +776,7 @@ class PromptManager {
|
||||
const promptOrder = this.getPromptOrderForCharacter(character);
|
||||
const index = promptOrder.findIndex(entry => entry.identifier === prompt.identifier);
|
||||
|
||||
if (-1 === index) promptOrder.push({ identifier: prompt.identifier, enabled: false });
|
||||
if (-1 === index) promptOrder.unshift({ identifier: prompt.identifier, enabled: false });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1286,7 +1286,7 @@ class PromptManager {
|
||||
} else if (!entry.enabled && entry.identifier === 'main') {
|
||||
// Some extensions require main prompt to be present for relative inserts.
|
||||
// So we make a GMO-free vegan replacement.
|
||||
const prompt = this.getPromptById(entry.identifier);
|
||||
const prompt = structuredClone(this.getPromptById(entry.identifier));
|
||||
prompt.content = '';
|
||||
if (prompt) promptCollection.add(this.preparePrompt(prompt));
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
|
||||
import { saveMetadataDebounced } from './extensions.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { stringFormat } from './utils.js';
|
||||
import { flashHighlight, stringFormat } from './utils.js';
|
||||
|
||||
const BG_METADATA_KEY = 'custom_background';
|
||||
const LIST_METADATA_KEY = 'chat_backgrounds';
|
||||
@ -453,8 +453,7 @@ function highlightNewBackground(bg) {
|
||||
const newBg = $(`.bg_example[bgfile="${bg}"]`);
|
||||
const scrollOffset = newBg.offset().top - newBg.parent().offset().top;
|
||||
$('#Backgrounds').scrollTop(scrollOffset);
|
||||
newBg.addClass('flash animated');
|
||||
setTimeout(() => newBg.removeClass('flash animated'), 2000);
|
||||
flashHighlight(newBg);
|
||||
}
|
||||
|
||||
function onBackgroundFilterInput() {
|
||||
|
@ -53,11 +53,11 @@ import { ScraperManager } from './scrapers.js';
|
||||
* @returns {Promise<string>} Converted file text
|
||||
*/
|
||||
|
||||
const fileSizeLimit = 1024 * 1024 * 10; // 10 MB
|
||||
const fileSizeLimit = 1024 * 1024 * 100; // 100 MB
|
||||
const ATTACHMENT_SOURCE = {
|
||||
GLOBAL: 'global',
|
||||
CHAT: 'chat',
|
||||
CHARACTER: 'character',
|
||||
CHAT: 'chat',
|
||||
};
|
||||
|
||||
/**
|
||||
@ -670,6 +670,55 @@ async function editAttachment(attachment, source, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an attachment to the user's device.
|
||||
* @param {FileAttachment} attachment Attachment to download
|
||||
*/
|
||||
async function downloadAttachment(attachment) {
|
||||
const fileText = attachment.text || (await getFileAttachment(attachment.url));
|
||||
const blob = new Blob([fileText], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = attachment.name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a file attachment to a different source.
|
||||
* @param {FileAttachment} attachment Attachment to moves
|
||||
* @param {string} source Source of the attachment
|
||||
* @param {function} callback Success callback
|
||||
* @returns {Promise<void>} A promise that resolves when the attachment is moved.
|
||||
*/
|
||||
async function moveAttachment(attachment, source, callback) {
|
||||
let selectedTarget = source;
|
||||
const targets = getAvailableTargets();
|
||||
const template = $(await renderExtensionTemplateAsync('attachments', 'move-attachment', { name: attachment.name, targets }));
|
||||
template.find('.moveAttachmentTarget').val(source).on('input', function () {
|
||||
selectedTarget = String($(this).val());
|
||||
});
|
||||
|
||||
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Move', cancelButton: 'Cancel' });
|
||||
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
console.debug('Move attachment cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedTarget === source) {
|
||||
console.debug('Move attachment cancelled: same source and target');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await getFileAttachment(attachment.url);
|
||||
const file = new File([content], attachment.name, { type: 'text/plain' });
|
||||
await deleteAttachment(attachment, source, () => { }, false);
|
||||
await uploadFileAttachmentToServer(file, selectedTarget);
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an attachment from the server and the chat.
|
||||
* @param {FileAttachment} attachment Attachment to delete
|
||||
@ -765,6 +814,8 @@ async function openAttachmentManager() {
|
||||
attachmentTemplate.find('.viewAttachmentButton').on('click', () => openFilePopup(attachment));
|
||||
attachmentTemplate.find('.editAttachmentButton').on('click', () => editAttachment(attachment, source, renderAttachments));
|
||||
attachmentTemplate.find('.deleteAttachmentButton').on('click', () => deleteAttachment(attachment, source, renderAttachments));
|
||||
attachmentTemplate.find('.downloadAttachmentButton').on('click', () => downloadAttachment(attachment));
|
||||
attachmentTemplate.find('.moveAttachmentButton').on('click', () => moveAttachment(attachment, source, renderAttachments));
|
||||
template.find(sources[source]).append(attachmentTemplate);
|
||||
}
|
||||
}
|
||||
@ -869,6 +920,50 @@ async function openAttachmentManager() {
|
||||
template.find('.chatAttachmentsName').text(chatName);
|
||||
}
|
||||
|
||||
function addDragAndDrop() {
|
||||
$(document.body).on('dragover', '.dialogue_popup', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.target).closest('.dialogue_popup').addClass('dragover');
|
||||
});
|
||||
|
||||
$(document.body).on('dragleave', '.dialogue_popup', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.target).closest('.dialogue_popup').removeClass('dragover');
|
||||
});
|
||||
|
||||
$(document.body).on('drop', '.dialogue_popup', async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.target).closest('.dialogue_popup').removeClass('dragover');
|
||||
|
||||
const files = Array.from(event.originalEvent.dataTransfer.files);
|
||||
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
|
||||
const targets = getAvailableTargets();
|
||||
|
||||
const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets }));
|
||||
targetSelectTemplate.find('.droppedFilesTarget').on('input', function () {
|
||||
selectedTarget = String($(this).val());
|
||||
});
|
||||
const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' });
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
console.log('File upload cancelled');
|
||||
return;
|
||||
}
|
||||
for (const file of files) {
|
||||
await uploadFileAttachmentToServer(file, selectedTarget);
|
||||
}
|
||||
renderAttachments();
|
||||
});
|
||||
}
|
||||
|
||||
function removeDragAndDrop() {
|
||||
$(document.body).off('dragover', '.shadow_popup');
|
||||
$(document.body).off('dragleave', '.shadow_popup');
|
||||
$(document.body).off('drop', '.shadow_popup');
|
||||
}
|
||||
|
||||
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
|
||||
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
|
||||
let filterString = '';
|
||||
@ -894,9 +989,32 @@ async function openAttachmentManager() {
|
||||
const cleanupFn = await renderButtons();
|
||||
await verifyAttachments();
|
||||
await renderAttachments();
|
||||
addDragAndDrop();
|
||||
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
|
||||
|
||||
cleanupFn();
|
||||
removeDragAndDrop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of available targets for attachments.
|
||||
* @returns {string[]} List of available targets
|
||||
*/
|
||||
function getAvailableTargets() {
|
||||
const targets = Object.values(ATTACHMENT_SOURCE);
|
||||
|
||||
const isNotCharacter = this_chid === undefined || selected_group;
|
||||
const isNotInChat = getCurrentChatId() === undefined;
|
||||
|
||||
if (isNotCharacter) {
|
||||
targets.splice(targets.indexOf(ATTACHMENT_SOURCE.CHARACTER), 1);
|
||||
}
|
||||
|
||||
if (isNotInChat) {
|
||||
targets.splice(targets.indexOf(ATTACHMENT_SOURCE.CHAT), 1);
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1168,6 +1286,7 @@ jQuery(function () {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = String(bro.val());
|
||||
textarea.classList.add('height100p', 'wide100p');
|
||||
bro.hasClass('monospace') && textarea.classList.add('monospace');
|
||||
textarea.addEventListener('input', function () {
|
||||
bro.val(textarea.value).trigger('input');
|
||||
});
|
||||
|
8
public/scripts/extensions/attachments/files-dropped.html
Normal file
8
public/scripts/extensions/attachments/files-dropped.html
Normal file
@ -0,0 +1,8 @@
|
||||
<div class="flex-container justifyCenter alignItemsBaseline">
|
||||
<span>Save <span class="droppedFilesCount">{{count}}</span> file(s) to...</span>
|
||||
<select class="droppedFilesTarget">
|
||||
{{#each targets}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
@ -1,6 +1,9 @@
|
||||
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
|
||||
jQuery(async () => {
|
||||
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
||||
$('#extensionsMenu').prepend(buttons);
|
||||
|
||||
registerSlashCommand('db', () => document.getElementById('manageAttachments')?.click(), ['databank', 'data-bank'], '– open the data bank', true, true);
|
||||
});
|
||||
|
@ -7,8 +7,13 @@
|
||||
<div data-i18n="These files will be available for extensions that support attachments (e.g. Vector Storage).">
|
||||
These files will be available for extensions that support attachments (e.g. Vector Storage).
|
||||
</div>
|
||||
<div data-i18n="Supported file types: Plain Text, PDF, Markdown, HTML, EPUB." class="marginTopBot5">
|
||||
<div class="marginTopBot5">
|
||||
<span data-i18n="Supported file types: Plain Text, PDF, Markdown, HTML, EPUB." >
|
||||
Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.
|
||||
</span>
|
||||
<span data-i18n="Drag and drop files here to upload.">
|
||||
Drag and drop files here to upload.
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-container marginTopBot5">
|
||||
<input type="search" id="attachmentSearch" class="attachmentSearch text_pole margin0 flex1" placeholder="Search...">
|
||||
@ -102,7 +107,9 @@
|
||||
<small class="attachmentListItemCreated"></small>
|
||||
<small class="attachmentListItemSize"></small>
|
||||
<div class="viewAttachmentButton right_menu_button fa-solid fa-magnifying-glass" title="View attachment content"></div>
|
||||
<div class="moveAttachmentButton right_menu_button fa-solid fa-arrows-alt" title="Move attachment"></div>
|
||||
<div class="editAttachmentButton right_menu_button fa-solid fa-pencil" title="Edit attachment"></div>
|
||||
<div class="downloadAttachmentButton right_menu_button fa-solid fa-download" title="Download attachment"></div>
|
||||
<div class="deleteAttachmentButton right_menu_button fa-solid fa-trash" title="Delete attachment"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div class="flex-container justifyCenter alignItemsBaseline">
|
||||
<span>Move <strong class="moveAttachmentName">{{name}}</strong> to...</span>
|
||||
<select class="moveAttachmentTarget">
|
||||
{{#each targets}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
@ -1270,13 +1270,10 @@ async function getExpressionsList() {
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async function resolveExpressionsList() {
|
||||
// get something for offline mode (default images)
|
||||
if (!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) {
|
||||
return DEFAULT_EXPRESSIONS;
|
||||
}
|
||||
|
||||
// See if we can retrieve a specific expression list from the API
|
||||
try {
|
||||
if (extension_settings.expressions.api == EXPRESSION_API.extras) {
|
||||
// Check Extras api first, if enabled and that module active
|
||||
if (extension_settings.expressions.api == EXPRESSION_API.extras && modules.includes('classify')) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/classify/labels';
|
||||
|
||||
@ -1291,7 +1288,10 @@ async function getExpressionsList() {
|
||||
expressionsList = data.labels;
|
||||
return expressionsList;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// If running the local classify model (not using the LLM), we ask that one
|
||||
if (extension_settings.expressions.api == EXPRESSION_API.local) {
|
||||
const apiResult = await fetch('/api/extra/classify/labels', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
@ -1303,11 +1303,12 @@ async function getExpressionsList() {
|
||||
return expressionsList;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
|
||||
// If there was no specific list, or an error, just return the default expressions
|
||||
return DEFAULT_EXPRESSIONS;
|
||||
}
|
||||
|
||||
const result = await resolveExpressionsList();
|
||||
|
@ -94,7 +94,7 @@ async function loadRegexScripts() {
|
||||
await onRegexEditorOpenClick(scriptHtml.attr('id'));
|
||||
});
|
||||
scriptHtml.find('.export_regex').on('click', async function () {
|
||||
const fileName = `${script.scriptName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`;
|
||||
const fileName = `${script.scriptName.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase()}.json`;
|
||||
const fileData = JSON.stringify(script, null, 4);
|
||||
download(fileData, fileName, 'application/json');
|
||||
});
|
||||
|
@ -1657,6 +1657,10 @@ async function loadNovelModels() {
|
||||
value: 'safe-diffusion',
|
||||
text: 'NAI Diffusion Anime V1 (Curated)',
|
||||
},
|
||||
{
|
||||
value: 'nai-diffusion-furry-3',
|
||||
text: 'NAI Diffusion Furry V3',
|
||||
},
|
||||
{
|
||||
value: 'nai-diffusion-furry',
|
||||
text: 'NAI Diffusion Furry',
|
||||
|
@ -424,6 +424,24 @@ function createEventHandler(translateFunction, shouldTranslateFunction) {
|
||||
};
|
||||
}
|
||||
|
||||
async function onTranslateInputMessageClick() {
|
||||
const textarea = document.getElementById('send_textarea');
|
||||
|
||||
if (!(textarea instanceof HTMLTextAreaElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!textarea.value) {
|
||||
toastr.warning('Enter a message first');
|
||||
return;
|
||||
}
|
||||
|
||||
const toast = toastr.info('Input Message is translating', 'Please wait...');
|
||||
const translatedText = await translate(textarea.value, extension_settings.translate.internal_language);
|
||||
textarea.value = translatedText;
|
||||
toastr.clear(toast);
|
||||
}
|
||||
|
||||
// Prevents the chat from being translated in parallel
|
||||
let translateChatExecuting = false;
|
||||
|
||||
@ -555,10 +573,16 @@ jQuery(() => {
|
||||
<div id="translate_chat" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-language extensionsMenuExtensionButton" /></div>
|
||||
Translate Chat
|
||||
</div>`;
|
||||
</div>
|
||||
<div id="translate_input_message" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-keyboard extensionsMenuExtensionButton" /></div>
|
||||
Translate Input
|
||||
</div>
|
||||
`;
|
||||
$('#extensionsMenu').append(buttonHtml);
|
||||
$('#extensions_settings2').append(html);
|
||||
$('#translate_chat').on('click', onTranslateChatClick);
|
||||
$('#translate_input_message').on('click', onTranslateInputMessageClick);
|
||||
$('#translation_clear').on('click', onTranslationsClearClick);
|
||||
|
||||
for (const [key, value] of Object.entries(languageCodes)) {
|
||||
|
@ -23,6 +23,7 @@ import { collapseNewlines } from '../../power-user.js';
|
||||
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { getDataBankAttachments, getFileAttachment } from '../../chats.js';
|
||||
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
|
||||
import { getSortedEntries } from '../../world-info.js';
|
||||
|
||||
const MODULE_NAME = 'vectors';
|
||||
|
||||
@ -66,6 +67,11 @@ const settings = {
|
||||
file_position_db: extension_prompt_types.IN_PROMPT,
|
||||
file_depth_db: 4,
|
||||
file_depth_role_db: extension_prompt_roles.SYSTEM,
|
||||
|
||||
// For World Info
|
||||
enabled_world_info: false,
|
||||
enabled_for_all: false,
|
||||
max_entries: 5,
|
||||
};
|
||||
|
||||
const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
|
||||
@ -281,8 +287,10 @@ async function synchronizeChat(batchSize = 5) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache object for storing hash values
|
||||
const hashCache = {};
|
||||
/**
|
||||
* @type {Map<string, number>} Cache object for storing hash values
|
||||
*/
|
||||
const hashCache = new Map();
|
||||
|
||||
/**
|
||||
* Gets the hash value for a given string
|
||||
@ -291,15 +299,15 @@ const hashCache = {};
|
||||
*/
|
||||
function getStringHash(str) {
|
||||
// Check if the hash is already in the cache
|
||||
if (Object.hasOwn(hashCache, str)) {
|
||||
return hashCache[str];
|
||||
if (hashCache.has(str)) {
|
||||
return hashCache.get(str);
|
||||
}
|
||||
|
||||
// Calculate the hash value
|
||||
const hash = calculateHash(str);
|
||||
|
||||
// Store the hash in the cache
|
||||
hashCache[str] = hash;
|
||||
hashCache.set(str, hash);
|
||||
|
||||
return hash;
|
||||
}
|
||||
@ -472,6 +480,10 @@ async function rearrangeChat(chat) {
|
||||
await processFiles(chat);
|
||||
}
|
||||
|
||||
if (settings.enabled_world_info) {
|
||||
await activateWorldInfo(chat);
|
||||
}
|
||||
|
||||
if (!settings.enabled_chats) {
|
||||
return;
|
||||
}
|
||||
@ -845,6 +857,7 @@ async function purgeVectorIndex(collectionId) {
|
||||
function toggleSettings() {
|
||||
$('#vectors_files_settings').toggle(!!settings.enabled_files);
|
||||
$('#vectors_chats_settings').toggle(!!settings.enabled_chats);
|
||||
$('#vectors_world_info_settings').toggle(!!settings.enabled_world_info);
|
||||
$('#together_vectorsModel').toggle(settings.source === 'togetherai');
|
||||
$('#openai_vectorsModel').toggle(settings.source === 'openai');
|
||||
$('#cohere_vectorsModel').toggle(settings.source === 'cohere');
|
||||
@ -934,6 +947,111 @@ async function onPurgeFilesClick() {
|
||||
}
|
||||
}
|
||||
|
||||
async function activateWorldInfo(chat) {
|
||||
if (!settings.enabled_world_info) {
|
||||
console.debug('Vectors: Disabled for World Info');
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await getSortedEntries();
|
||||
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
console.debug('Vectors: No WI entries found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Group entries by "world" field
|
||||
const groupedEntries = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip orphaned entries. Is it even possible?
|
||||
if (!entry.world) {
|
||||
console.debug('Vectors: Skipped orphaned WI entry', entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip disabled entries
|
||||
if (entry.disable) {
|
||||
console.debug('Vectors: Skipped disabled WI entry', entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip entries without content
|
||||
if (!entry.content) {
|
||||
console.debug('Vectors: Skipped WI entry without content', entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip non-vectorized entries
|
||||
if (!entry.vectorized && !settings.enabled_for_all) {
|
||||
console.debug('Vectors: Skipped non-vectorized WI entry', entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(groupedEntries, entry.world)) {
|
||||
groupedEntries[entry.world] = [];
|
||||
}
|
||||
|
||||
groupedEntries[entry.world].push(entry);
|
||||
}
|
||||
|
||||
const collectionIds = [];
|
||||
|
||||
if (Object.keys(groupedEntries).length === 0) {
|
||||
console.debug('Vectors: No WI entries to synchronize');
|
||||
return;
|
||||
}
|
||||
|
||||
// Synchronize collections
|
||||
for (const world in groupedEntries) {
|
||||
const collectionId = `world_${getStringHash(world)}`;
|
||||
const hashesInCollection = await getSavedHashes(collectionId);
|
||||
const newEntries = groupedEntries[world].filter(x => !hashesInCollection.includes(getStringHash(x.content)));
|
||||
const deletedHashes = hashesInCollection.filter(x => !groupedEntries[world].some(y => getStringHash(y.content) === x));
|
||||
|
||||
if (newEntries.length > 0) {
|
||||
console.log(`Vectors: Found ${newEntries.length} new WI entries for world ${world}`);
|
||||
await insertVectorItems(collectionId, newEntries.map(x => ({ hash: getStringHash(x.content), text: x.content, index: x.uid })));
|
||||
}
|
||||
|
||||
if (deletedHashes.length > 0) {
|
||||
console.log(`Vectors: Deleted ${deletedHashes.length} old hashes for world ${world}`);
|
||||
await deleteVectorItems(collectionId, deletedHashes);
|
||||
}
|
||||
|
||||
collectionIds.push(collectionId);
|
||||
}
|
||||
|
||||
// Perform a multi-query
|
||||
const queryText = await getQueryText(chat);
|
||||
|
||||
if (queryText.length === 0) {
|
||||
console.debug('Vectors: No text to query for WI');
|
||||
return;
|
||||
}
|
||||
|
||||
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.max_entries);
|
||||
const activatedHashes = Object.values(queryResults).flatMap(x => x.hashes).filter(onlyUnique);
|
||||
const activatedEntries = [];
|
||||
|
||||
// Activate entries found in the query results
|
||||
for (const entry of entries) {
|
||||
const hash = getStringHash(entry.content);
|
||||
|
||||
if (activatedHashes.includes(hash)) {
|
||||
activatedEntries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (activatedEntries.length === 0) {
|
||||
console.debug('Vectors: No activated WI entries found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Vectors: Activated ${activatedEntries.length} WI entries`, activatedEntries);
|
||||
await eventSource.emit(event_types.WORLDINFO_FORCE_ACTIVATE, activatedEntries);
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
if (!extension_settings.vectors) {
|
||||
extension_settings.vectors = settings;
|
||||
@ -1134,6 +1252,25 @@ jQuery(async () => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_enabled_world_info').prop('checked', settings.enabled_world_info).on('input', () => {
|
||||
settings.enabled_world_info = !!$('#vectors_enabled_world_info').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
toggleSettings();
|
||||
});
|
||||
|
||||
$('#vectors_enabled_for_all').prop('checked', settings.enabled_for_all).on('input', () => {
|
||||
settings.enabled_for_all = !!$('#vectors_enabled_for_all').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_max_entries').val(settings.max_entries).on('input', () => {
|
||||
settings.max_entries = Number($('#vectors_max_entries').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
const validSecret = !!secret_state[SECRET_KEYS.NOMICAI];
|
||||
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
|
||||
$('#api_key_nomicai').attr('placeholder', placeholder);
|
||||
|
@ -97,6 +97,46 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>
|
||||
World Info settings
|
||||
</h4>
|
||||
|
||||
<label class="checkbox_label" for="vectors_enabled_world_info" title="Enable activation of World Info entries based on vector similarity.">
|
||||
<input id="vectors_enabled_world_info" type="checkbox" class="checkbox">
|
||||
Enabled for World Info
|
||||
</label>
|
||||
|
||||
<div id="vectors_world_info_settings" class="marginTopBot5">
|
||||
<div class="flex-container">
|
||||
<label for="vectors_enabled_for_all" class="checkbox_label">
|
||||
<input id="vectors_enabled_for_all" type="checkbox" />
|
||||
<span>Enabled for all entries</span>
|
||||
</label>
|
||||
<ul class="margin0">
|
||||
<li>
|
||||
<small>Checked: all entries except ❌ status can be activated.</small>
|
||||
</li>
|
||||
<li>
|
||||
<small>Unchecked: only entries with 🔗 status can be activated.</small>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="flex1">
|
||||
<!-- Vacant for future use -->
|
||||
</div>
|
||||
<div class="flex1" title="Maximum number of entries to be activated">
|
||||
<label for="vectors_max_entries" >
|
||||
<small>Max Entries</small>
|
||||
</label>
|
||||
<input id="vectors_max_entries" type="number" class="text_pole widthUnset" min="1" max="9999" />
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<!-- Vacant for future use -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>
|
||||
File vectorization settings
|
||||
</h4>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { registerDebugFunction } from './power-user.js';
|
||||
import { updateSecretDisplay } from './secrets.js';
|
||||
|
||||
const storageKey = 'language';
|
||||
const overrideLanguage = localStorage.getItem(storageKey);
|
||||
@ -12,10 +13,8 @@ const localeData = await getLocaleData(localeFile);
|
||||
* @returns {Promise<Record<string, string>>} Locale data
|
||||
*/
|
||||
async function getLocaleData(language) {
|
||||
let supportedLang = langs.find(x => x.lang === language);
|
||||
|
||||
let supportedLang = findLang(language);
|
||||
if (!supportedLang) {
|
||||
console.warn(`Unsupported language: ${language}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -30,11 +29,24 @@ async function getLocaleData(language) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function findLang(language) {
|
||||
var supportedLang = langs.find(x => x.lang === language);
|
||||
|
||||
if (!supportedLang) {
|
||||
console.warn(`Unsupported language: ${language}`);
|
||||
}
|
||||
return supportedLang;
|
||||
}
|
||||
|
||||
async function getMissingTranslations() {
|
||||
const missingData = [];
|
||||
|
||||
for (const language of langs) {
|
||||
const localeData = await getLocaleData(language);
|
||||
// Determine locales to search for untranslated strings
|
||||
const isNotSupported = !findLang(localeFile);
|
||||
const langsToProcess = (isNotSupported || localeFile == 'en') ? langs : [findLang(localeFile)];
|
||||
|
||||
for (const language of langsToProcess) {
|
||||
const localeData = await getLocaleData(language.lang);
|
||||
$(document).find('[data-i18n]').each(function () {
|
||||
const keys = $(this).data('i18n').split(';'); // Multi-key entries are ; delimited
|
||||
for (const key of keys) {
|
||||
@ -42,12 +54,12 @@ async function getMissingTranslations() {
|
||||
if (attributeMatch) { // attribute-tagged key
|
||||
const localizedValue = localeData?.[attributeMatch[2]];
|
||||
if (!localizedValue) {
|
||||
missingData.push({ key, language, value: $(this).attr(attributeMatch[1]) });
|
||||
missingData.push({ key, language: language.lang, value: $(this).attr(attributeMatch[1]) });
|
||||
}
|
||||
} else { // No attribute tag, treat as 'text'
|
||||
const localizedValue = localeData?.[key];
|
||||
if (!localizedValue) {
|
||||
missingData.push({ key, language, value: $(this).text().trim() });
|
||||
missingData.push({ key, language: language.lang, value: $(this).text().trim() });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,6 +142,7 @@ function addLanguagesToDropdown() {
|
||||
export function initLocales() {
|
||||
applyLocale();
|
||||
addLanguagesToDropdown();
|
||||
updateSecretDisplay();
|
||||
|
||||
$('#ui_language_select').on('change', async function () {
|
||||
const language = String($(this).val());
|
||||
@ -143,6 +156,6 @@ export function initLocales() {
|
||||
location.reload();
|
||||
});
|
||||
|
||||
registerDebugFunction('getMissingTranslations', 'Get missing translations', 'Detects missing localization data and dumps the data into the browser console.', getMissingTranslations);
|
||||
registerDebugFunction('getMissingTranslations', 'Get missing translations', 'Detects missing localization data in the current locale and dumps the data into the browser console. If the current locale is English, searches all other locales.', getMissingTranslations);
|
||||
registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale);
|
||||
}
|
||||
|
@ -3250,7 +3250,8 @@ async function onExportPresetClick() {
|
||||
delete preset.proxy_password;
|
||||
|
||||
const presetJsonString = JSON.stringify(preset, null, 4);
|
||||
download(presetJsonString, oai_settings.preset_settings_openai, 'application/json');
|
||||
const presetFileName = `${oai_settings.preset_settings_openai}.json`;
|
||||
download(presetJsonString, presetFileName, 'application/json');
|
||||
}
|
||||
|
||||
async function onLogitBiasPresetImportFileChange(e) {
|
||||
@ -3298,7 +3299,8 @@ function onLogitBiasPresetExportClick() {
|
||||
}
|
||||
|
||||
const presetJsonString = JSON.stringify(oai_settings.bias_presets[oai_settings.bias_preset_selected], null, 4);
|
||||
download(presetJsonString, oai_settings.bias_preset_selected, 'application/json');
|
||||
const presetFileName = `${oai_settings.bias_preset_selected}.json`;
|
||||
download(presetJsonString, presetFileName, 'application/json');
|
||||
}
|
||||
|
||||
async function onDeletePresetClick() {
|
||||
|
@ -178,6 +178,7 @@ let power_user = {
|
||||
timestamps_enabled: true,
|
||||
timestamp_model_icon: false,
|
||||
mesIDDisplay_enabled: false,
|
||||
hideChatAvatars_enabled: false,
|
||||
max_context_unlocked: false,
|
||||
message_token_count_enabled: false,
|
||||
expand_message_actions: false,
|
||||
@ -295,6 +296,7 @@ const storage_keys = {
|
||||
timestamps_enabled: 'TimestampsEnabled',
|
||||
timestamp_model_icon: 'TimestampModelIcon',
|
||||
mesIDDisplay_enabled: 'mesIDDisplayEnabled',
|
||||
hideChatAvatars_enabled: 'hideChatAvatarsEnabled',
|
||||
message_token_count_enabled: 'MessageTokenCountEnabled',
|
||||
expand_message_actions: 'ExpandMessageActions',
|
||||
enableZenSliders: 'enableZenSliders',
|
||||
@ -464,6 +466,17 @@ function switchMesIDDisplay() {
|
||||
$('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
|
||||
}
|
||||
|
||||
function switchHideChatAvatars() {
|
||||
const value = localStorage.getItem(storage_keys.hideChatAvatars_enabled);
|
||||
power_user.hideChatAvatars_enabled = value === null ? false : value == 'true';
|
||||
/*console.log(`
|
||||
localstorage value:${value},
|
||||
poweruser after:${power_user.hideChatAvatars_enabled}`)
|
||||
*/
|
||||
$('body').toggleClass('hideChatAvatars', power_user.hideChatAvatars_enabled);
|
||||
$('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
|
||||
}
|
||||
|
||||
function switchMessageActions() {
|
||||
const value = localStorage.getItem(storage_keys.expand_message_actions);
|
||||
power_user.expand_message_actions = value === null ? false : value == 'true';
|
||||
@ -1269,6 +1282,13 @@ async function applyTheme(name) {
|
||||
switchMesIDDisplay();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'hideChatAvatars_enabled',
|
||||
action: async () => {
|
||||
localStorage.setItem(storage_keys.hideChatAvatars_enabled, Boolean(power_user.hideChatAvatars_enabled));
|
||||
switchHideChatAvatars();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'expand_message_actions',
|
||||
action: async () => {
|
||||
@ -1391,6 +1411,7 @@ switchTimer();
|
||||
switchTimestamps();
|
||||
switchIcons();
|
||||
switchMesIDDisplay();
|
||||
switchHideChatAvatars();
|
||||
switchTokenCount();
|
||||
switchMessageActions();
|
||||
|
||||
@ -1433,6 +1454,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
const timer = localStorage.getItem(storage_keys.timer_enabled);
|
||||
const timestamps = localStorage.getItem(storage_keys.timestamps_enabled);
|
||||
const mesIDDisplay = localStorage.getItem(storage_keys.mesIDDisplay_enabled);
|
||||
const hideChatAvatars = localStorage.getItem(storage_keys.hideChatAvatars_enabled);
|
||||
const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions);
|
||||
const enableZenSliders = localStorage.getItem(storage_keys.enableZenSliders);
|
||||
const enableLabMode = localStorage.getItem(storage_keys.enableLabMode);
|
||||
@ -1456,6 +1478,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
power_user.timer_enabled = timer === null ? true : timer == 'true';
|
||||
power_user.timestamps_enabled = timestamps === null ? true : timestamps == 'true';
|
||||
power_user.mesIDDisplay_enabled = mesIDDisplay === null ? true : mesIDDisplay == 'true';
|
||||
power_user.hideChatAvatars_enabled = hideChatAvatars === null ? true : hideChatAvatars == 'true';
|
||||
power_user.expand_message_actions = expandMessageActions === null ? true : expandMessageActions == 'true';
|
||||
power_user.enableZenSliders = enableZenSliders === null ? false : enableZenSliders == 'true';
|
||||
power_user.enableLabMode = enableLabMode === null ? false : enableLabMode == 'true';
|
||||
@ -1541,6 +1564,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
$('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
|
||||
$('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
|
||||
$('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
|
||||
$('#hideChatAvatarsEndabled').prop('checked', power_user.hideChatAvatars_enabled);
|
||||
$('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);
|
||||
$('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);
|
||||
$('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');
|
||||
@ -2150,6 +2174,7 @@ async function saveTheme(name = undefined) {
|
||||
timestamp_model_icon: power_user.timestamp_model_icon,
|
||||
|
||||
mesIDDisplay_enabled: power_user.mesIDDisplay_enabled,
|
||||
hideChatAvatars_enabled: power_user.hideChatAvatars_enabled,
|
||||
message_token_count_enabled: power_user.message_token_count_enabled,
|
||||
expand_message_actions: power_user.expand_message_actions,
|
||||
enableZenSliders: power_user.enableZenSliders,
|
||||
@ -3376,6 +3401,13 @@ $(document).ready(() => {
|
||||
switchMesIDDisplay();
|
||||
});
|
||||
|
||||
$('#hideChatAvatarsEnabled').on('input', function () {
|
||||
const value = !!$(this).prop('checked');
|
||||
power_user.hideChatAvatars_enabled = value;
|
||||
localStorage.setItem(storage_keys.hideChatAvatars_enabled, Boolean(power_user.hideChatAvatars_enabled));
|
||||
switchHideChatAvatars();
|
||||
});
|
||||
|
||||
$('#hotswapEnabled').on('input', function () {
|
||||
const value = !!$(this).prop('checked');
|
||||
power_user.hotswap_enabled = value;
|
||||
|
@ -372,7 +372,7 @@ class YouTubeScraper {
|
||||
this.id = 'youtube';
|
||||
this.name = 'YouTube';
|
||||
this.description = 'Download a transcript from a YouTube video.';
|
||||
this.iconClass = 'fa-solid fa-closed-captioning';
|
||||
this.iconClass = 'fa-brands fa-youtube';
|
||||
this.iconAvailable = true;
|
||||
}
|
||||
|
||||
|
@ -62,10 +62,11 @@ async function clearSecret() {
|
||||
$('#main_api').trigger('change');
|
||||
}
|
||||
|
||||
function updateSecretDisplay() {
|
||||
export function updateSecretDisplay() {
|
||||
for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) {
|
||||
const validSecret = !!secret_state[secret_key];
|
||||
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
|
||||
|
||||
const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text');
|
||||
$(input_selector).attr('placeholder', placeholder);
|
||||
}
|
||||
}
|
||||
|
@ -203,13 +203,10 @@ class SlashCommandParser {
|
||||
|
||||
// Excluded commands format in their own function
|
||||
if (!excludedFromRegex.includes(command)) {
|
||||
console.debug(`parse: !excludedFromRegex.includes(${command}`);
|
||||
console.debug(` parse: unnamedArg before: ${unnamedArg}`);
|
||||
unnamedArg = getRegexedString(
|
||||
unnamedArg,
|
||||
regex_placement.SLASH_COMMAND,
|
||||
);
|
||||
console.debug(` parse: unnamedArg after: ${unnamedArg}`);
|
||||
}
|
||||
|
||||
// your weird complex command is now transformed into a juicy tiny text or something useful :)
|
||||
@ -338,13 +335,12 @@ function injectCallback(args, value) {
|
||||
chat_metadata.script_injects = {};
|
||||
}
|
||||
|
||||
chat_metadata.script_injects[id] = {
|
||||
value,
|
||||
position,
|
||||
depth,
|
||||
scan,
|
||||
role,
|
||||
};
|
||||
if (value) {
|
||||
const inject = { value, position, depth, scan, role };
|
||||
chat_metadata.script_injects[id] = inject;
|
||||
} else {
|
||||
delete chat_metadata.script_injects[id];
|
||||
}
|
||||
|
||||
setExtensionPrompt(prefixedId, value, position, depth, scan, role);
|
||||
saveMetadataDebounced();
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
|
||||
|
||||
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from './utils.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
|
||||
import { power_user } from './power-user.js';
|
||||
|
||||
export {
|
||||
@ -350,18 +350,20 @@ function createTagMapFromList(listElement, key) {
|
||||
* If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`.
|
||||
*
|
||||
* @param {string} key - The key for which to get tags via the tag map
|
||||
* @param {boolean} [sort=true] -
|
||||
* @returns {Tag[]} A list of tags
|
||||
*/
|
||||
function getTagsList(key) {
|
||||
function getTagsList(key, sort = true) {
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
return tag_map[key]
|
||||
const list = tag_map[key]
|
||||
.map(x => tags.find(y => y.id === x))
|
||||
.filter(x => x)
|
||||
.sort(compareTagsForSort);
|
||||
.filter(x => x);
|
||||
if (sort) list.sort(compareTagsForSort);
|
||||
return list;
|
||||
}
|
||||
|
||||
function getInlineListSelector() {
|
||||
@ -384,7 +386,7 @@ function getTagKey() {
|
||||
return selected_group;
|
||||
}
|
||||
|
||||
if (this_chid && menu_type === 'character_edit') {
|
||||
if (this_chid !== undefined && menu_type === 'character_edit') {
|
||||
return characters[this_chid].avatar;
|
||||
}
|
||||
|
||||
@ -644,6 +646,7 @@ function createNewTag(tagName) {
|
||||
* @property {Tag} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check.
|
||||
* @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key.
|
||||
* @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean.
|
||||
* @property {boolean} [sort=true] - Whether the tags should be sorted via the sort function, or kept as is.
|
||||
* @property {function(object): function} [tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions.
|
||||
* If set, the selector is executed on each tag as input argument. This allows a list of tags to be provided and each tag can have it's action based on the tag object itself.
|
||||
* @property {TagOptions} [tagOptions={}] - Options for tag behavior. (Same object will be passed into "appendTagToList")
|
||||
@ -655,10 +658,10 @@ function createNewTag(tagName) {
|
||||
* @param {JQuery<HTMLElement>|string} element - The container element where the tags are to be printed. (Optionally can also be a string selector for the element, which will then be resolved)
|
||||
* @param {PrintTagListOptions} [options] - Optional parameters for printing the tag list.
|
||||
*/
|
||||
function printTagList(element, { tags = undefined, addTag = undefined, forEntityOrKey = undefined, empty = true, tagActionSelector = undefined, tagOptions = {} } = {}) {
|
||||
function printTagList(element, { tags = undefined, addTag = undefined, forEntityOrKey = undefined, empty = true, sort = true, tagActionSelector = undefined, tagOptions = {} } = {}) {
|
||||
const $element = (typeof element === 'string') ? $(element) : element;
|
||||
const key = forEntityOrKey !== undefined ? getTagKeyForEntity(forEntityOrKey) : getTagKey();
|
||||
let printableTags = tags ? (typeof tags === 'function' ? tags() : tags) : getTagsList(key);
|
||||
let printableTags = tags ? (typeof tags === 'function' ? tags() : tags) : getTagsList(key, sort);
|
||||
|
||||
if (empty === 'always' || (empty && (printableTags?.length > 0 || key))) {
|
||||
$element.empty();
|
||||
@ -669,7 +672,7 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
|
||||
}
|
||||
|
||||
// one last sort, because we might have modified the tag list or manually retrieved it from a function
|
||||
printableTags = printableTags.sort(compareTagsForSort);
|
||||
if (sort) printableTags = printableTags.sort(compareTagsForSort);
|
||||
|
||||
const customAction = typeof tagActionSelector === 'function' ? tagActionSelector : null;
|
||||
|
||||
@ -872,10 +875,10 @@ function printTagFilters(type = tag_filter_types.character) {
|
||||
|
||||
// Print all action tags. (Exclude folder if that setting isn't chosen)
|
||||
const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id);
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, tags: actionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, sort: false, tags: actionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
|
||||
|
||||
const inListActionTags = Object.values(InListActionable);
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, sort: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
|
||||
|
||||
const characterTagIds = Object.values(tag_map).flat();
|
||||
const tagsToDisplay = tags.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort);
|
||||
@ -992,11 +995,11 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {})
|
||||
}
|
||||
|
||||
function onViewTagsListClick() {
|
||||
$('#dialogue_popup').addClass('large_dialogue_popup');
|
||||
const list = $(document.createElement('div'));
|
||||
list.attr('id', 'tag_view_list');
|
||||
const everything = Object.values(tag_map).flat();
|
||||
$(list).append(`
|
||||
const popup = $('#dialogue_popup');
|
||||
popup.addClass('large_dialogue_popup');
|
||||
const html = $(document.createElement('div'));
|
||||
html.attr('id', 'tag_view_list');
|
||||
html.append(`
|
||||
<div class="title_restorable alignItemsBaseline">
|
||||
<h3>Tag Management</h3>
|
||||
<div class="flex-container alignItemsBaseline">
|
||||
@ -1017,25 +1020,57 @@ function onViewTagsListClick() {
|
||||
</div>
|
||||
<div class="justifyLeft m-b-1">
|
||||
<small>
|
||||
Drag the handle to reorder.<br>
|
||||
Drag handle to reorder. Click name to rename. Click color to change display.<br>
|
||||
${(power_user.bogus_folders ? 'Click on the folder icon to use this tag as a folder.<br>' : '')}
|
||||
Click on the tag name to edit it.<br>
|
||||
Click on color box to assign new color.
|
||||
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-1" for="auto_sort_tags">
|
||||
<input type="checkbox" id="auto_sort_tags" name="auto_sort_tags" ${power_user.auto_sort_tags ? ' checked' : ''} />
|
||||
<span data-i18n="Use alphabetical sorting">
|
||||
Use alphabetical sorting
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]If enabled, tags will automatically be sorted alphabetically on creation or rename.\nIf disabled, new tags will be appended at the end.\n\nIf a tag is manually reordered by dragging, automatic sorting will be disabled."
|
||||
title="If enabled, tags will automatically be sorted alphabetically on creation or rename.\nIf disabled, new tags will be appended at the end.\n\nIf a tag is manually reordered by dragging, automatic sorting will be disabled.">
|
||||
</div>
|
||||
</span>
|
||||
</label>
|
||||
</small>
|
||||
</div>`);
|
||||
|
||||
const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
|
||||
list.append(tagContainer);
|
||||
html.append(tagContainer);
|
||||
|
||||
const sortedTags = sortTags(tags);
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
appendViewTagToList(tagContainer, tag, everything);
|
||||
}
|
||||
callPopup(html, 'text', null, { allowVerticalScrolling: true });
|
||||
|
||||
printViewTagList();
|
||||
makeTagListDraggable(tagContainer);
|
||||
|
||||
callPopup(list, 'text');
|
||||
$('#dialogue_popup .tag-color').on('change', (evt) => onTagColorize(evt));
|
||||
$('#dialogue_popup .tag-color2').on('change', (evt) => onTagColorize2(evt));
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the list of tags in the tag management view
|
||||
* @param {Event} event Event that triggered the color change
|
||||
* @param {boolean} toggle State of the toggle
|
||||
*/
|
||||
function toggleAutoSortTags(event, toggle) {
|
||||
if (toggle === power_user.auto_sort_tags) return;
|
||||
|
||||
// Ask user to confirm if enabling and it was manually sorted before
|
||||
if (toggle && isManuallySorted() && !confirm('Are you sure you want to automatically sort alphabetically?')) {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
event.target.checked = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
power_user.auto_sort_tags = toggle;
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
/** This function goes over all existing tags and checks whether they were reorderd in the past. @returns {boolean} */
|
||||
function isManuallySorted() {
|
||||
return tags.some((tag, index) => tag.sort_order !== index);
|
||||
}
|
||||
|
||||
function makeTagListDraggable(tagContainer) {
|
||||
@ -1067,6 +1102,13 @@ function makeTagListDraggable(tagContainer) {
|
||||
}
|
||||
});
|
||||
|
||||
// If tags were dragged manually, we have to disable auto sorting
|
||||
if (power_user.auto_sort_tags) {
|
||||
power_user.auto_sort_tags = false;
|
||||
$('#dialogue_popup input[name="auto_sort_tags"]').prop('checked', false);
|
||||
toastr.info('Automatic sorting of tags deactivated.');
|
||||
}
|
||||
|
||||
// If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags.
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
@ -1098,6 +1140,11 @@ function sortTags(tags) {
|
||||
* @returns {number} The compare result
|
||||
*/
|
||||
function compareTagsForSort(a, b) {
|
||||
const defaultSort = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
if (power_user.auto_sort_tags) {
|
||||
return defaultSort;
|
||||
}
|
||||
|
||||
if (a.sort_order !== undefined && b.sort_order !== undefined) {
|
||||
return a.sort_order - b.sort_order;
|
||||
} else if (a.sort_order !== undefined) {
|
||||
@ -1105,7 +1152,7 @@ function compareTagsForSort(a, b) {
|
||||
} else if (b.sort_order !== undefined) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
return defaultSort;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1208,7 +1255,10 @@ function onTagsBackupClick() {
|
||||
|
||||
function onTagCreateClick() {
|
||||
const tag = createNewTag('New Tag');
|
||||
appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []);
|
||||
printViewTagList();
|
||||
|
||||
const tagElement = ($('#dialogue_popup .tag_view_list_tags')).find(`.tag_view_item[id="${tag.id}"]`);
|
||||
flashHighlight(tagElement);
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
@ -1248,18 +1298,6 @@ function appendViewTagToList(list, tag, everything) {
|
||||
|
||||
list.append(template);
|
||||
|
||||
setTimeout(function () {
|
||||
document.querySelector(`.tag-color[id="${colorPickerId}"`).addEventListener('change', (evt) => {
|
||||
onTagColorize(evt);
|
||||
});
|
||||
}, 100);
|
||||
|
||||
setTimeout(function () {
|
||||
document.querySelector(`.tag-color2[id="${colorPicker2Id}"`).addEventListener('change', (evt) => {
|
||||
onTagColorize2(evt);
|
||||
});
|
||||
}, 100);
|
||||
|
||||
updateDrawTagFolder(template, tag);
|
||||
|
||||
// @ts-ignore
|
||||
@ -1394,6 +1432,17 @@ function copyTags(data) {
|
||||
tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));
|
||||
}
|
||||
|
||||
function printViewTagList(empty = true) {
|
||||
const tagContainer = $('#dialogue_popup .tag_view_list_tags');
|
||||
|
||||
if (empty) tagContainer.empty();
|
||||
const everything = Object.values(tag_map).flat();
|
||||
const sortedTags = sortTags(tags);
|
||||
for (const tag of sortedTags) {
|
||||
appendViewTagToList(tagContainer, tag, everything);
|
||||
}
|
||||
}
|
||||
|
||||
export function initTags() {
|
||||
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
|
||||
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
|
||||
@ -1412,4 +1461,31 @@ export function initTags() {
|
||||
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
|
||||
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
|
||||
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
|
||||
|
||||
$(document).on('input', '#dialogue_popup input[name="auto_sort_tags"]', (evt) => {
|
||||
const toggle = $(evt.target).is(':checked');
|
||||
toggleAutoSortTags(evt.originalEvent, toggle);
|
||||
printViewTagList();
|
||||
});
|
||||
$(document).on('focusout', `#dialogue_popup .tag_view_name`, (evt) => {
|
||||
// Remember the order, so we can flash highlight if it changed after reprinting
|
||||
const tagId = $(evt.target).parent('.tag_view_item').attr('id');
|
||||
const oldOrder = $(`#dialogue_popup .tag_view_item`).map((_, el) => el.id).get();
|
||||
|
||||
printViewTagList();
|
||||
|
||||
const newOrder = $(`#dialogue_popup .tag_view_item`).map((_, el) => el.id).get();
|
||||
const orderChanged = !oldOrder.every((id, index) => id === newOrder[index]);
|
||||
if (orderChanged) {
|
||||
flashHighlight($(`#dialogue_popup .tag_view_item[id="${tagId}"]`));
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize auto sort setting based on whether it was sorted before
|
||||
if (power_user.auto_sort_tags === undefined || power_user.auto_sort_tags === null) {
|
||||
power_user.auto_sort_tags = !isManuallySorted();
|
||||
if (power_user.auto_sort_tags) {
|
||||
printCharactersDebounced();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1419,3 +1419,13 @@ export function setValueByPath(obj, path, value) {
|
||||
|
||||
currentObject[keyParts[keyParts.length - 1]] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flashes the given HTML element via CSS flash animation for a defined period
|
||||
* @param {JQuery<HTMLElement>} element - The element to flash
|
||||
* @param {number} timespan - A numer in milliseconds how the flash should last
|
||||
*/
|
||||
export function flashHighlight(element, timespan = 2000) {
|
||||
element.addClass('flash animated');
|
||||
setTimeout(() => element.removeClass('flash animated'), timespan);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath } from './utils.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight } from './utils.js';
|
||||
import { extension_settings, getContext } from './extensions.js';
|
||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
@ -82,6 +82,11 @@ class WorldInfoBuffer {
|
||||
/** @typedef {{scanDepth?: number, caseSensitive?: boolean, matchWholeWords?: boolean}} WIScanEntry The entry that triggered the scan */
|
||||
// End typedef area
|
||||
|
||||
/**
|
||||
* @type {object[]} Array of entries that need to be activated no matter what
|
||||
*/
|
||||
static externalActivations = [];
|
||||
|
||||
/**
|
||||
* @type {string[]} Array of messages sorted by ascending depth
|
||||
*/
|
||||
@ -220,6 +225,23 @@ class WorldInfoBuffer {
|
||||
getDepth() {
|
||||
return world_info_depth + this.#skew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current entry is externally activated.
|
||||
* @param {object} entry WI entry to check
|
||||
* @returns {boolean} True if the entry is forcefully activated
|
||||
*/
|
||||
isExternallyActivated(entry) {
|
||||
// Entries could be copied with structuredClone, so we need to compare them by string representation
|
||||
return WorldInfoBuffer.externalActivations.some(x => JSON.stringify(x) === JSON.stringify(entry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the force activations buffer.
|
||||
*/
|
||||
cleanExternalActivations() {
|
||||
WorldInfoBuffer.externalActivations.splice(0, WorldInfoBuffer.externalActivations.length);
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorldInfoSettings() {
|
||||
@ -362,6 +384,10 @@ function setWorldInfoSettings(settings, data) {
|
||||
$('.chat_lorebook_button').toggleClass('world_set', hasWorldInfo);
|
||||
});
|
||||
|
||||
eventSource.on(event_types.WORLDINFO_FORCE_ACTIVATE, (entries) => {
|
||||
WorldInfoBuffer.externalActivations.push(...entries);
|
||||
});
|
||||
|
||||
// Add slash commands
|
||||
registerWorldInfoSlashCommands();
|
||||
}
|
||||
@ -496,7 +522,7 @@ function registerWorldInfoSlashCommands() {
|
||||
return '';
|
||||
}
|
||||
|
||||
const entry = createWorldInfoEntry(file, data, true);
|
||||
const entry = createWorldInfoEntry(file, data);
|
||||
|
||||
if (key) {
|
||||
entry.key.push(key);
|
||||
@ -564,6 +590,7 @@ function registerWorldInfoSlashCommands() {
|
||||
return '';
|
||||
}
|
||||
|
||||
registerSlashCommand('world', onWorldInfoChange, [], '<span class="monospace">[optional state=off|toggle] [optional silent=true] (optional name)</span> – sets active World, or unsets if no args provided, use <code>state=off</code> and <code>state=toggle</code> to deactivate or toggle a World, use <code>silent=true</code> to suppress toast messages', true, true);
|
||||
registerSlashCommand('getchatbook', getChatBookCallback, ['getchatlore', 'getchatwi'], '– get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe', true, true);
|
||||
registerSlashCommand('findentry', findBookEntryCallback, ['findlore', 'findwi'], '<span class="monospace">(file=bookName field=field [texts])</span> – find a UID of the record from the specified book using the fuzzy match of a field value (default: key) and pass it down the pipe, e.g. <tt>/findentry file=chatLore field=key Shadowfang</tt>', true, true);
|
||||
registerSlashCommand('getentryfield', getEntryFieldCallback, ['getlorefield', 'getwifield'], '<span class="monospace">(file=bookName field=field [UID])</span> – get a field value (default: content) of the record with the UID from the specified book and pass it down the pipe, e.g. <tt>/getentryfield file=chatLore field=content 123</tt>', true, true);
|
||||
@ -768,20 +795,20 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
callback: function (/** @type {object[]} */ page) {
|
||||
$('#world_popup_entries_list').empty();
|
||||
const keywordHeaders = `
|
||||
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 2.5em;">
|
||||
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 4.5em;">
|
||||
<small class="flex1">
|
||||
Title/Memo
|
||||
</small>
|
||||
<small style="width: calc(3.5em + 5px)">
|
||||
<small style="width: calc(3.5em + 15px)">
|
||||
Status
|
||||
</small>
|
||||
<small style="width: calc(3.5em + 20px)">
|
||||
<small style="width: calc(3.5em + 30px)">
|
||||
Position
|
||||
</small>
|
||||
<small style="width: calc(3.5em + 15px)">
|
||||
<small style="width: calc(3.5em + 20px)">
|
||||
Depth
|
||||
</small>
|
||||
<small style="width: calc(3.5em + 15px)">
|
||||
<small style="width: calc(3.5em + 20px)">
|
||||
Order
|
||||
</small>
|
||||
<small style="width: calc(3.5em + 15px)">
|
||||
@ -827,13 +854,13 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
const parentOffset = element.parent().offset();
|
||||
const scrollOffset = elementOffset.top - parentOffset.top;
|
||||
$('#WorldInfo').scrollTop(scrollOffset);
|
||||
element.addClass('flash animated');
|
||||
setTimeout(() => element.removeClass('flash animated'), 2000);
|
||||
flashHighlight(element);
|
||||
});
|
||||
}
|
||||
|
||||
$('#world_popup_new').off('click').on('click', () => {
|
||||
createWorldInfoEntry(name, data);
|
||||
const entry = createWorldInfoEntry(name, data);
|
||||
if (entry) updateEditor(entry.uid);
|
||||
});
|
||||
|
||||
$('#world_popup_name_button').off('click').on('click', async () => {
|
||||
@ -964,6 +991,8 @@ const originalDataKeyMap = {
|
||||
'caseSensitive': 'extensions.case_sensitive',
|
||||
'scanDepth': 'extensions.scan_depth',
|
||||
'automationId': 'extensions.automation_id',
|
||||
'vectorized': 'extensions.vectorized',
|
||||
'groupOverride': 'extensions.group_override',
|
||||
};
|
||||
|
||||
function setOriginalDataValue(data, uid, key, value) {
|
||||
@ -1071,6 +1100,16 @@ function getWorldEntry(name, data, entry) {
|
||||
);
|
||||
}
|
||||
|
||||
// Verify names to exist in the system
|
||||
if (data.entries[uid]?.characterFilter?.names?.length > 0) {
|
||||
for (const name of [...data.entries[uid].characterFilter.names]) {
|
||||
if (!getContext().characters.find(x => x.avatar.replace(/\.[^/.]+$/, '') === name)) {
|
||||
console.warn(`World Info: Character ${name} not found. Removing from the entry filter.`, entry);
|
||||
data.entries[uid].characterFilter.names = data.entries[uid].characterFilter.names.filter(x => x !== name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
@ -1300,6 +1339,18 @@ function getWorldEntry(name, data, entry) {
|
||||
groupInput.val(entry.group ?? '').trigger('input');
|
||||
setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data)), 1);
|
||||
|
||||
// inclusion priority
|
||||
const groupOverrideInput = template.find('input[name="groupOverride"]');
|
||||
groupOverrideInput.data('uid', entry.uid);
|
||||
groupOverrideInput.on('input', function () {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).prop('checked');
|
||||
data.entries[uid].groupOverride = value;
|
||||
setOriginalDataValue(data, uid, 'extensions.groupOverride', data.entries[uid].groupOverride);
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
groupOverrideInput.prop('checked', entry.groupOverride).trigger('input');
|
||||
|
||||
// probability
|
||||
if (entry.probability === undefined) {
|
||||
entry.probability = null;
|
||||
@ -1454,22 +1505,37 @@ function getWorldEntry(name, data, entry) {
|
||||
case 'constant':
|
||||
data.entries[uid].constant = true;
|
||||
data.entries[uid].disable = false;
|
||||
data.entries[uid].vectorized = false;
|
||||
setOriginalDataValue(data, uid, 'enabled', true);
|
||||
setOriginalDataValue(data, uid, 'constant', true);
|
||||
setOriginalDataValue(data, uid, 'extensions.vectorized', false);
|
||||
template.removeClass('disabledWIEntry');
|
||||
break;
|
||||
case 'normal':
|
||||
data.entries[uid].constant = false;
|
||||
data.entries[uid].disable = false;
|
||||
data.entries[uid].vectorized = false;
|
||||
setOriginalDataValue(data, uid, 'enabled', true);
|
||||
setOriginalDataValue(data, uid, 'constant', false);
|
||||
setOriginalDataValue(data, uid, 'extensions.vectorized', false);
|
||||
template.removeClass('disabledWIEntry');
|
||||
break;
|
||||
case 'vectorized':
|
||||
data.entries[uid].constant = false;
|
||||
data.entries[uid].disable = false;
|
||||
data.entries[uid].vectorized = true;
|
||||
setOriginalDataValue(data, uid, 'enabled', true);
|
||||
setOriginalDataValue(data, uid, 'constant', false);
|
||||
setOriginalDataValue(data, uid, 'extensions.vectorized', true);
|
||||
template.removeClass('disabledWIEntry');
|
||||
break;
|
||||
case 'disabled':
|
||||
data.entries[uid].constant = false;
|
||||
data.entries[uid].disable = true;
|
||||
data.entries[uid].vectorized = false;
|
||||
setOriginalDataValue(data, uid, 'enabled', false);
|
||||
setOriginalDataValue(data, uid, 'constant', false);
|
||||
setOriginalDataValue(data, uid, 'extensions.vectorized', false);
|
||||
template.addClass('disabledWIEntry');
|
||||
break;
|
||||
}
|
||||
@ -1480,6 +1546,8 @@ function getWorldEntry(name, data, entry) {
|
||||
const entryState = function () {
|
||||
if (entry.constant === true) {
|
||||
return 'constant';
|
||||
} else if (entry.vectorized === true) {
|
||||
return 'vectorized';
|
||||
} else if (entry.disable === true) {
|
||||
return 'disabled';
|
||||
} else {
|
||||
@ -1517,6 +1585,18 @@ function getWorldEntry(name, data, entry) {
|
||||
});
|
||||
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
|
||||
|
||||
// duplicate button
|
||||
const duplicateButton = template.find('.duplicate_entry_button');
|
||||
duplicateButton.data('uid', entry.uid);
|
||||
duplicateButton.on('click', function () {
|
||||
const uid = $(this).data('uid');
|
||||
const entry = duplicateWorldInfoEntry(data, uid);
|
||||
if (entry) {
|
||||
saveWorldInfo(name, data);
|
||||
updateEditor(entry.uid);
|
||||
}
|
||||
});
|
||||
|
||||
// delete button
|
||||
const deleteButton = template.find('.delete_entry_button');
|
||||
deleteButton.data('uid', entry.uid);
|
||||
@ -1701,7 +1781,33 @@ function createEntryInputAutocomplete(input, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteWorldInfoEntry(data, uid) {
|
||||
/**
|
||||
* Duplicated a WI entry by copying all of its properties and assigning a new uid
|
||||
* @param {*} data - The data of the book
|
||||
* @param {number} uid - The uid of the entry to copy in this book
|
||||
* @returns {*} The new WI duplicated entry
|
||||
*/
|
||||
function duplicateWorldInfoEntry(data, uid) {
|
||||
if (!data || !('entries' in data) || !data.entries[uid]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Exclude uid and gather the rest of the properties
|
||||
const { uid: _, ...originalData } = data.entries[uid];
|
||||
|
||||
// Create new entry and copy over data
|
||||
const entry = createWorldInfoEntry(data.name, data);
|
||||
Object.assign(entry, originalData);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a WI entry, with a user confirmation dialog
|
||||
* @param {*[]} data - The data of the book
|
||||
* @param {number} uid - The uid of the entry to copy in this book
|
||||
*/
|
||||
function deleteWorldInfoEntry(data, uid) {
|
||||
if (!data || !('entries' in data)) {
|
||||
return;
|
||||
}
|
||||
@ -1719,6 +1825,7 @@ const newEntryTemplate = {
|
||||
comment: '',
|
||||
content: '',
|
||||
constant: false,
|
||||
vectorized: false,
|
||||
selective: true,
|
||||
selectiveLogic: world_info_logic.AND_ANY,
|
||||
addMemo: false,
|
||||
@ -1730,6 +1837,7 @@ const newEntryTemplate = {
|
||||
useProbability: true,
|
||||
depth: DEFAULT_DEPTH,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
scanDepth: null,
|
||||
caseSensitive: null,
|
||||
matchWholeWords: null,
|
||||
@ -1737,7 +1845,7 @@ const newEntryTemplate = {
|
||||
role: 0,
|
||||
};
|
||||
|
||||
function createWorldInfoEntry(name, data, fromSlashCommand = false) {
|
||||
function createWorldInfoEntry(name, data) {
|
||||
const newUid = getFreeWorldEntryUid(data);
|
||||
|
||||
if (!Number.isInteger(newUid)) {
|
||||
@ -1748,10 +1856,6 @@ function createWorldInfoEntry(name, data, fromSlashCommand = false) {
|
||||
const newEntry = { uid: newUid, ...structuredClone(newEntryTemplate) };
|
||||
data.entries[newUid] = newEntry;
|
||||
|
||||
if (!fromSlashCommand) {
|
||||
updateEditor(newUid);
|
||||
}
|
||||
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
@ -1925,7 +2029,7 @@ async function getCharacterLore() {
|
||||
}
|
||||
|
||||
const data = await loadWorldInfoData(worldName);
|
||||
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
|
||||
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: worldName })) : [];
|
||||
entries = entries.concat(newEntries);
|
||||
}
|
||||
|
||||
@ -1941,7 +2045,7 @@ async function getGlobalLore() {
|
||||
let entries = [];
|
||||
for (const worldName of selected_world_info) {
|
||||
const data = await loadWorldInfoData(worldName);
|
||||
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
|
||||
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: worldName })) : [];
|
||||
entries = entries.concat(newEntries);
|
||||
}
|
||||
|
||||
@ -1963,14 +2067,14 @@ async function getChatLore() {
|
||||
}
|
||||
|
||||
const data = await loadWorldInfoData(chatWorld);
|
||||
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
|
||||
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: chatWorld })) : [];
|
||||
|
||||
console.debug(`Chat lore has ${entries.length} entries`);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function getSortedEntries() {
|
||||
export async function getSortedEntries() {
|
||||
try {
|
||||
const globalLore = await getGlobalLore();
|
||||
const characterLore = await getCharacterLore();
|
||||
@ -2098,7 +2202,7 @@ async function checkWorldInfo(chat, maxContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.constant) {
|
||||
if (entry.constant || buffer.isExternallyActivated(entry)) {
|
||||
entry.content = substituteParams(entry.content);
|
||||
activatedNow.add(entry);
|
||||
continue;
|
||||
@ -2186,7 +2290,7 @@ async function checkWorldInfo(chat, maxContext) {
|
||||
for (const entry of newEntries) {
|
||||
const rollValue = Math.random() * 100;
|
||||
|
||||
if (entry.useProbability && rollValue > entry.probability) {
|
||||
if (!entry.group && entry.useProbability && rollValue > entry.probability) {
|
||||
console.debug(`WI entry ${entry.uid} ${entry.key} failed probability check, skipping`);
|
||||
failedProbabilityChecks.add(entry);
|
||||
continue;
|
||||
@ -2295,6 +2399,8 @@ async function checkWorldInfo(chat, maxContext) {
|
||||
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);
|
||||
}
|
||||
|
||||
buffer.cleanExternalActivations();
|
||||
|
||||
return { worldInfoBefore, worldInfoAfter, WIDepthEntries, allActivatedEntries };
|
||||
}
|
||||
|
||||
@ -2318,15 +2424,25 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeEntry = (entry) => newEntries.splice(newEntries.indexOf(entry), 1);
|
||||
function removeAllBut(group, chosen, logging = true) {
|
||||
for (const entry of group) {
|
||||
if (entry === chosen) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (logging) console.debug(`Removing loser from inclusion group '${entry.group}' entry '${entry.uid}'`, entry);
|
||||
removeEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, group] of Object.entries(grouped)) {
|
||||
console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group);
|
||||
|
||||
if (Array.from(allActivatedEntries).some(x => x.group === key)) {
|
||||
console.debug(`Skipping inclusion group check, group already activated '${key}'`);
|
||||
// We need to forcefully deactivate all other entries in the group
|
||||
for (const entry of group) {
|
||||
newEntries.splice(newEntries.indexOf(entry), 1);
|
||||
}
|
||||
removeAllBut(group, null, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -2335,6 +2451,14 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for group prio
|
||||
const prios = group.filter(x => x.groupOverride).sort(sortFn);
|
||||
if (prios.length) {
|
||||
console.debug(`Activated inclusion group '${key}' with by prio winner entry '${prios[0].uid}'`, prios[0]);
|
||||
removeAllBut(group, prios[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do weighted random using probability of entry as weight
|
||||
const totalWeight = group.reduce((acc, item) => acc + item.probability, 0);
|
||||
const rollValue = Math.random() * totalWeight;
|
||||
@ -2345,7 +2469,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) {
|
||||
currentWeight += entry.probability;
|
||||
|
||||
if (rollValue <= currentWeight) {
|
||||
console.debug(`Activated inclusion group '${key}' with entry '${entry.uid}'`, entry);
|
||||
console.debug(`Activated inclusion group '${key}' with roll winner entry '${entry.uid}'`, entry);
|
||||
winner = entry;
|
||||
break;
|
||||
}
|
||||
@ -2357,14 +2481,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) {
|
||||
}
|
||||
|
||||
// Remove every group item from newEntries but the winner
|
||||
for (const entry of group) {
|
||||
if (entry === winner) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.debug(`Removing loser from inclusion group '${key}' entry '${entry.uid}'`, entry);
|
||||
newEntries.splice(newEntries.indexOf(entry), 1);
|
||||
}
|
||||
removeAllBut(group, winner);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2381,6 +2498,7 @@ function convertAgnaiMemoryBook(inputObj) {
|
||||
content: entry.entry,
|
||||
constant: false,
|
||||
selective: false,
|
||||
vectorized: false,
|
||||
selectiveLogic: world_info_logic.AND_ANY,
|
||||
order: entry.weight,
|
||||
position: 0,
|
||||
@ -2391,6 +2509,7 @@ function convertAgnaiMemoryBook(inputObj) {
|
||||
probability: null,
|
||||
useProbability: false,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
scanDepth: entry.extensions?.scan_depth ?? null,
|
||||
caseSensitive: entry.extensions?.case_sensitive ?? null,
|
||||
matchWholeWords: entry.extensions?.match_whole_words ?? null,
|
||||
@ -2415,6 +2534,7 @@ function convertRisuLorebook(inputObj) {
|
||||
content: entry.content,
|
||||
constant: entry.alwaysActive,
|
||||
selective: entry.selective,
|
||||
vectorized: false,
|
||||
selectiveLogic: world_info_logic.AND_ANY,
|
||||
order: entry.insertorder,
|
||||
position: world_info_position.before,
|
||||
@ -2425,6 +2545,7 @@ function convertRisuLorebook(inputObj) {
|
||||
probability: entry.activationPercent ?? null,
|
||||
useProbability: entry.activationPercent ?? false,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
scanDepth: entry.extensions?.scan_depth ?? null,
|
||||
caseSensitive: entry.extensions?.case_sensitive ?? null,
|
||||
matchWholeWords: entry.extensions?.match_whole_words ?? null,
|
||||
@ -2454,6 +2575,7 @@ function convertNovelLorebook(inputObj) {
|
||||
content: entry.text,
|
||||
constant: false,
|
||||
selective: false,
|
||||
vectorized: false,
|
||||
selectiveLogic: world_info_logic.AND_ANY,
|
||||
order: entry.contextConfig?.budgetPriority ?? 0,
|
||||
position: 0,
|
||||
@ -2464,6 +2586,7 @@ function convertNovelLorebook(inputObj) {
|
||||
probability: null,
|
||||
useProbability: false,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
scanDepth: entry.extensions?.scan_depth ?? null,
|
||||
caseSensitive: entry.extensions?.case_sensitive ?? null,
|
||||
matchWholeWords: entry.extensions?.match_whole_words ?? null,
|
||||
@ -2505,11 +2628,13 @@ function convertCharacterBook(characterBook) {
|
||||
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
|
||||
selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY,
|
||||
group: entry.extensions?.group ?? '',
|
||||
groupOverride: entry.extensions?.group_override ?? false,
|
||||
scanDepth: entry.extensions?.scan_depth ?? null,
|
||||
caseSensitive: entry.extensions?.case_sensitive ?? null,
|
||||
matchWholeWords: entry.extensions?.match_whole_words ?? null,
|
||||
automationId: entry.extensions?.automation_id ?? '',
|
||||
role: entry.extensions?.role ?? extension_prompt_roles.SYSTEM,
|
||||
vectorized: entry.extensions?.vectorized ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
@ -2785,11 +2910,6 @@ function assignLorebookToChat() {
|
||||
|
||||
jQuery(() => {
|
||||
|
||||
$(document).ready(function () {
|
||||
registerSlashCommand('world', onWorldInfoChange, [], '<span class="monospace">[optional state=off|toggle] [optional silent=true] (optional name)</span> – sets active World, or unsets if no args provided, use <code>state=off</code> and <code>state=toggle</code> to deactivate or toggle a World, use <code>silent=true</code> to suppress toast messages', true, true);
|
||||
});
|
||||
|
||||
|
||||
$('#world_info').on('mousedown change', async function (e) {
|
||||
// If there's no world names, don't do anything
|
||||
if (world_names.length === 0) {
|
||||
|
@ -1000,6 +1000,7 @@ body .panelControlBar {
|
||||
padding-left: 10px;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: clip;
|
||||
}
|
||||
|
||||
.mes_text {
|
||||
@ -2262,6 +2263,11 @@ grammarly-extension {
|
||||
}
|
||||
}
|
||||
|
||||
.dialogue_popup.dragover {
|
||||
filter: brightness(1.1) saturate(1.1);
|
||||
outline: 3px dashed var(--SmartThemeBorderColor);
|
||||
}
|
||||
|
||||
#bgtest {
|
||||
display: none;
|
||||
width: 100vw;
|
||||
@ -3159,6 +3165,15 @@ body.big-avatars .missing-avatar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.userSettingsInnerExpandable {
|
||||
border: 1px solid;
|
||||
border-color: var(--SmartThemeBorderColor);
|
||||
border-radius: 5px;
|
||||
padding: 2px 5px !important;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@keyframes ellipsis {
|
||||
0% {
|
||||
content: ""
|
||||
|
BIN
public/webfonts/fa-brands-400.ttf
Normal file
BIN
public/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
public/webfonts/fa-brands-400.woff2
Normal file
BIN
public/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
48
server.js
48
server.js
@ -45,7 +45,6 @@ const {
|
||||
forwardFetchResponse,
|
||||
} = require('./src/util');
|
||||
const { ensureThumbnailCache } = require('./src/endpoints/thumbnails');
|
||||
const { loadTokenizers } = require('./src/endpoints/tokenizers');
|
||||
|
||||
// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
|
||||
// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
|
||||
@ -263,11 +262,15 @@ app.get('/login', async (request, response) => {
|
||||
return response.redirect('/');
|
||||
}
|
||||
|
||||
try {
|
||||
const autoLogin = await userModule.tryAutoLogin(request);
|
||||
|
||||
if (autoLogin) {
|
||||
return response.redirect('/');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during auto-login:', error);
|
||||
}
|
||||
|
||||
return response.sendFile('login.html', { root: path.join(process.cwd(), 'public') });
|
||||
});
|
||||
@ -283,6 +286,7 @@ app.use(userModule.requireLoginMiddleware);
|
||||
|
||||
// File uploads
|
||||
app.use(multer({ dest: UPLOADS_PATH, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
|
||||
app.use(require('./src/middleware/multerMonkeyPatch'));
|
||||
|
||||
// User data mount
|
||||
app.use('/', userModule.router);
|
||||
@ -526,7 +530,10 @@ const autorunUrl = new URL(
|
||||
(':' + server_port),
|
||||
);
|
||||
|
||||
const setupTasks = async function () {
|
||||
/**
|
||||
* Tasks that need to be run before the server starts listening.
|
||||
*/
|
||||
const preSetupTasks = async function () {
|
||||
const version = await getVersion();
|
||||
|
||||
// Print formatted header
|
||||
@ -539,28 +546,21 @@ const setupTasks = async function () {
|
||||
}
|
||||
console.log();
|
||||
|
||||
// TODO: do endpoint init functions depend on certain directories existing or not existing? They should be callable
|
||||
// in any order for encapsulation reasons, but right now it's unknown if that would break anything.
|
||||
await userModule.initUserStorage(dataRoot);
|
||||
|
||||
if (listen && !basicAuthMode && enableAccounts) {
|
||||
await userModule.checkAccountsProtection();
|
||||
}
|
||||
|
||||
await settingsEndpoint.init();
|
||||
const directories = await userModule.ensurePublicDirectoriesExist();
|
||||
await userModule.migrateUserData();
|
||||
const directories = await userModule.getUserDirectoriesList();
|
||||
await contentManager.checkForNewContent(directories);
|
||||
await ensureThumbnailCache();
|
||||
cleanUploads();
|
||||
|
||||
await loadTokenizers();
|
||||
await settingsEndpoint.init();
|
||||
await statsEndpoint.init();
|
||||
|
||||
const cleanupPlugins = await loadPlugins();
|
||||
const consoleTitle = process.title;
|
||||
|
||||
let isExiting = false;
|
||||
const exitProcess = async () => {
|
||||
if (isExiting) return;
|
||||
isExiting = true;
|
||||
statsEndpoint.onExit();
|
||||
if (typeof cleanupPlugins === 'function') {
|
||||
await cleanupPlugins();
|
||||
@ -576,8 +576,12 @@ const setupTasks = async function () {
|
||||
console.error('Uncaught exception:', err);
|
||||
exitProcess();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tasks that need to be run after the server starts listening.
|
||||
*/
|
||||
const postSetupTasks = async function () {
|
||||
console.log('Launching...');
|
||||
|
||||
if (autorun) open(autorunUrl.toString());
|
||||
@ -597,6 +601,9 @@ const setupTasks = async function () {
|
||||
}
|
||||
}
|
||||
|
||||
if (listen && !basicAuthMode && enableAccounts) {
|
||||
await userModule.checkAccountsProtection();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -638,6 +645,12 @@ function setWindowTitle(title) {
|
||||
}
|
||||
}
|
||||
|
||||
// User storage module needs to be initialized before starting the server
|
||||
userModule.initUserStorage(dataRoot)
|
||||
.then(userModule.ensurePublicDirectoriesExist)
|
||||
.then(userModule.migrateUserData)
|
||||
.then(preSetupTasks)
|
||||
.finally(() => {
|
||||
if (cliArguments.ssl) {
|
||||
https.createServer(
|
||||
{
|
||||
@ -647,12 +660,13 @@ if (cliArguments.ssl) {
|
||||
.listen(
|
||||
Number(tavernUrl.port) || 443,
|
||||
tavernUrl.hostname,
|
||||
setupTasks,
|
||||
postSetupTasks,
|
||||
);
|
||||
} else {
|
||||
http.createServer(app).listen(
|
||||
Number(tavernUrl.port) || 80,
|
||||
tavernUrl.hostname,
|
||||
setupTasks,
|
||||
postSetupTasks,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -56,6 +56,8 @@ function validateAssetFileName(inputFilename) {
|
||||
* @returns {string[]} - The array of files
|
||||
*/
|
||||
function getFiles(dir, files = []) {
|
||||
if (!fs.existsSync(dir)) return files;
|
||||
|
||||
// Get an array of all files and directories in the passed directory using fs.readdirSync
|
||||
const fileList = fs.readdirSync(dir, { withFileTypes: true });
|
||||
// Create the full path of the file/directory by concatenating the passed directory and file/directory name
|
||||
|
@ -325,7 +325,7 @@ router.post('/generate', jsonParser, async function (request, response) {
|
||||
|
||||
// Map InfermaticAI response to OAI completions format
|
||||
if (apiType === TEXTGEN_TYPES.INFERMATICAI) {
|
||||
data['choices'] = (data?.choices || []).map(choice => ({ text: choice.message.content }));
|
||||
data['choices'] = (data?.choices || []).map(choice => ({ text: choice?.message?.content || choice.text }));
|
||||
}
|
||||
|
||||
return response.send(data);
|
||||
|
@ -433,12 +433,14 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
||||
depth: entry.depth ?? 4,
|
||||
selectiveLogic: entry.selectiveLogic ?? 0,
|
||||
group: entry.group ?? '',
|
||||
group_override: entry.groupOverride ?? false,
|
||||
prevent_recursion: entry.preventRecursion ?? false,
|
||||
scan_depth: entry.scanDepth ?? null,
|
||||
match_whole_words: entry.matchWholeWords ?? null,
|
||||
case_sensitive: entry.caseSensitive ?? null,
|
||||
automation_id: entry.automationId ?? '',
|
||||
role: entry.role ?? 0,
|
||||
vectorized: entry.vectorized ?? false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1097,7 +1099,7 @@ router.post('/export', jsonParser, async function (request, response) {
|
||||
const fileContent = await fsPromises.readFile(filename);
|
||||
const contentType = mime.lookup(filename) || 'image/png';
|
||||
response.setHeader('Content-Type', contentType);
|
||||
response.setHeader('Content-Disposition', `attachment; filename=${path.basename(filename)}`);
|
||||
response.setHeader('Content-Disposition', `attachment; filename="${encodeURI(path.basename(filename))}"`);
|
||||
return response.send(fileContent);
|
||||
}
|
||||
case 'json': {
|
||||
|
@ -5,7 +5,10 @@ const TASK = 'text-classification';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const cacheObject = {};
|
||||
/**
|
||||
* @type {Map<string, object>} Cache for classification results
|
||||
*/
|
||||
const cacheObject = new Map();
|
||||
|
||||
router.post('/labels', jsonParser, async (req, res) => {
|
||||
try {
|
||||
@ -23,15 +26,20 @@ router.post('/', jsonParser, async (req, res) => {
|
||||
try {
|
||||
const { text } = req.body;
|
||||
|
||||
/**
|
||||
* Get classification result for a given text
|
||||
* @param {string} text Text to classify
|
||||
* @returns {Promise<object>} Classification result
|
||||
*/
|
||||
async function getResult(text) {
|
||||
if (Object.hasOwn(cacheObject, text)) {
|
||||
return cacheObject[text];
|
||||
if (cacheObject.has(text)) {
|
||||
return cacheObject.get(text);
|
||||
} else {
|
||||
const module = await import('../transformers.mjs');
|
||||
const pipe = await module.default.getPipeline(TASK);
|
||||
const result = await pipe(text, { topk: 5 });
|
||||
result.sort((a, b) => b.score - a.score);
|
||||
cacheObject[text] = result;
|
||||
cacheObject.set(text, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ const fetch = require('node-fetch').default;
|
||||
const sanitize = require('sanitize-filename');
|
||||
const { getConfigValue } = require('../util');
|
||||
const { jsonParser } = require('../express-common');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const contentDirectory = path.join(process.cwd(), 'default/content');
|
||||
const contentIndexPath = path.join(contentDirectory, 'index.json');
|
||||
const characterCardParser = require('../character-card-parser.js');
|
||||
@ -133,7 +134,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
|
||||
console.log(`Content file ${contentItem.filename} copied to ${contentTarget}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(contentLogPath, contentLog.join('\n'));
|
||||
writeFileAtomicSync(contentLogPath, contentLog.join('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -386,6 +387,45 @@ async function downloadJannyCharacter(uuid) {
|
||||
throw new Error('Failed to download character');
|
||||
}
|
||||
|
||||
//Download Character Cards from AICharactersCards.com (AICC) API.
|
||||
async function downloadAICCCharacter(id) {
|
||||
const apiURL = `https://aicharactercards.com/wp-json/pngapi/v1/image/${id}`;
|
||||
try {
|
||||
const response = await fetch(apiURL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download character: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || 'image/png'; // Default to 'image/png' if header is missing
|
||||
const buffer = await response.buffer();
|
||||
const fileName = `${sanitize(id)}.png`; // Assuming PNG, but adjust based on actual content or headers
|
||||
|
||||
return {
|
||||
buffer: buffer,
|
||||
fileName: fileName,
|
||||
fileType: contentType,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error downloading character:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an aicharactercards URL to extract the path.
|
||||
* @param {string} url URL to parse
|
||||
* @returns {string | null} AICC path
|
||||
*/
|
||||
function parseAICC(url) {
|
||||
const pattern = /^https?:\/\/aicharactercards\.com\/character-cards\/([^/]+)\/([^/]+)\/?$|([^/]+)\/([^/]+)$/;
|
||||
const match = url.match(pattern);
|
||||
if (match) {
|
||||
// Match group 1 & 2 for full URL, 3 & 4 for relative path
|
||||
return match[1] && match[2] ? `${match[1]}/${match[2]}` : `${match[3]}/${match[4]}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} url
|
||||
* @returns {String | null } UUID of the character
|
||||
@ -414,6 +454,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
|
||||
|
||||
const isJannnyContent = url.includes('janitorai');
|
||||
const isPygmalionContent = url.includes('pygmalion.chat');
|
||||
const isAICharacterCardsContent = url.includes('aicharactercards.com');
|
||||
|
||||
if (isPygmalionContent) {
|
||||
const uuid = getUuidFromUrl(url);
|
||||
@ -431,6 +472,13 @@ router.post('/importURL', jsonParser, async (request, response) => {
|
||||
|
||||
type = 'character';
|
||||
result = await downloadJannyCharacter(uuid);
|
||||
} else if (isAICharacterCardsContent) {
|
||||
const AICCParsed = parseAICC(url);
|
||||
if (!AICCParsed) {
|
||||
return response.sendStatus(404);
|
||||
}
|
||||
type = 'character';
|
||||
result = await downloadAICCCharacter(AICCParsed);
|
||||
} else {
|
||||
const chubParsed = parseChubUrl(url);
|
||||
type = chubParsed?.type;
|
||||
@ -469,6 +517,7 @@ router.post('/importUUID', jsonParser, async (request, response) => {
|
||||
|
||||
const isJannny = uuid.includes('_character');
|
||||
const isPygmalion = (!isJannny && uuid.length == 36);
|
||||
const isAICC = uuid.startsWith('AICC/');
|
||||
const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';
|
||||
|
||||
if (isPygmalion) {
|
||||
@ -477,6 +526,10 @@ router.post('/importUUID', jsonParser, async (request, response) => {
|
||||
} else if (isJannny) {
|
||||
console.log('Downloading Janitor character:', uuid.split('_')[0]);
|
||||
result = await downloadJannyCharacter(uuid.split('_')[0]);
|
||||
} else if (isAICC) {
|
||||
const [, author, card] = uuid.split('/');
|
||||
console.log('Downloading AICC character:', `${author}/${card}`);
|
||||
result = await downloadAICCCharacter(`${author}/${card}`);
|
||||
} else {
|
||||
if (uuidType === 'character') {
|
||||
console.log('Downloading chub character:', uuid);
|
||||
|
@ -1,6 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const _ = require('lodash');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const { PUBLIC_DIRECTORIES, SETTINGS_FILE } = require('../constants');
|
||||
const { getConfigValue, generateTimestamp, removeOldBackups } = require('../util');
|
||||
@ -10,6 +11,32 @@ const { getAllUserHandles, getUserDirectories } = require('../users');
|
||||
const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);
|
||||
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
||||
|
||||
// 10 minutes
|
||||
const AUTOSAVE_INTERVAL = 10 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Map of functions to trigger settings autosave for a user.
|
||||
* @type {Map<string, function>}
|
||||
*/
|
||||
const AUTOSAVE_FUNCTIONS = new Map();
|
||||
|
||||
/**
|
||||
* Triggers autosave for a user every 10 minutes.
|
||||
* @param {string} handle User handle
|
||||
* @returns {void}
|
||||
*/
|
||||
function triggerAutoSave(handle) {
|
||||
if (!AUTOSAVE_FUNCTIONS.has(handle)) {
|
||||
const throttledAutoSave = _.throttle(() => backupUserSettings(handle), AUTOSAVE_INTERVAL);
|
||||
AUTOSAVE_FUNCTIONS.set(handle, throttledAutoSave);
|
||||
}
|
||||
|
||||
const functionToCall = AUTOSAVE_FUNCTIONS.get(handle);
|
||||
if (functionToCall) {
|
||||
functionToCall();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses files from a directory.
|
||||
* @param {string} directoryPath Path to the directory
|
||||
@ -121,6 +148,7 @@ router.post('/save', jsonParser, function (request, response) {
|
||||
try {
|
||||
const pathToSettings = path.join(request.user.directories.root, SETTINGS_FILE);
|
||||
writeFileAtomicSync(pathToSettings, JSON.stringify(request.body, null, 4), 'utf8');
|
||||
triggerAutoSave(request.user.profile.handle);
|
||||
response.send({ result: 'ok' });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
@ -10,6 +10,10 @@ const { getAllUserHandles, getUserDirectories } = require('../users');
|
||||
const { getConfigValue } = require('../util');
|
||||
const { jsonParser } = require('../express-common');
|
||||
|
||||
const thumbnailsDisabled = getConfigValue('disableThumbnails', false);
|
||||
const quality = getConfigValue('thumbnailsQuality', 95);
|
||||
const pngFormat = getConfigValue('avatarThumbnailsPng', false);
|
||||
|
||||
/**
|
||||
* Gets a path to thumbnail folder based on the type.
|
||||
* @param {import('../users').UserDirectoryList} directories User directories
|
||||
@ -115,9 +119,8 @@ async function generateThumbnail(directories, type, file) {
|
||||
let buffer;
|
||||
|
||||
try {
|
||||
const quality = getConfigValue('thumbnailsQuality', 95);
|
||||
const image = await jimp.read(pathToOriginalFile);
|
||||
const imgType = type == 'avatar' && getConfigValue('avatarThumbnailsPng', false) ? 'image/png' : 'image/jpeg';
|
||||
const imgType = type == 'avatar' && pngFormat ? 'image/png' : 'image/jpeg';
|
||||
buffer = await image.cover(mySize[0], mySize[1]).quality(quality).getBufferAsync(imgType);
|
||||
}
|
||||
catch (inner) {
|
||||
@ -188,7 +191,6 @@ router.get('/', jsonParser, async function (request, response) {
|
||||
return response.sendStatus(403);
|
||||
}
|
||||
|
||||
const thumbnailsDisabled = getConfigValue('disableThumbnails', false);
|
||||
if (thumbnailsDisabled) {
|
||||
const folder = getOriginalFolder(request.user.directories, type);
|
||||
|
||||
|
@ -10,6 +10,10 @@ const { TEXTGEN_TYPES } = require('../constants');
|
||||
const { jsonParser } = require('../express-common');
|
||||
const { setAdditionalHeaders } = require('../additional-headers');
|
||||
|
||||
/**
|
||||
* @typedef { (req: import('express').Request, res: import('express').Response) => Promise<any> } TokenizationHandler
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {{[key: string]: import("@dqbd/tiktoken").Tiktoken}} Tokenizers cache
|
||||
*/
|
||||
@ -48,16 +52,30 @@ const TEXT_COMPLETION_MODELS = [
|
||||
|
||||
const CHARS_PER_TOKEN = 3.35;
|
||||
|
||||
/**
|
||||
* Sentencepiece tokenizer for tokenizing text.
|
||||
*/
|
||||
class SentencePieceTokenizer {
|
||||
/**
|
||||
* @type {import('@agnai/sentencepiece-js').SentencePieceProcessor} Sentencepiece tokenizer instance
|
||||
*/
|
||||
#instance;
|
||||
/**
|
||||
* @type {string} Path to the tokenizer model
|
||||
*/
|
||||
#model;
|
||||
|
||||
/**
|
||||
* Creates a new Sentencepiece tokenizer.
|
||||
* @param {string} model Path to the tokenizer model
|
||||
*/
|
||||
constructor(model) {
|
||||
this.#model = model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Sentencepiece tokenizer instance.
|
||||
* @returns {Promise<import('@agnai/sentencepiece-js').SentencePieceProcessor|null>} Sentencepiece tokenizer instance
|
||||
*/
|
||||
async get() {
|
||||
if (this.#instance) {
|
||||
@ -76,18 +94,61 @@ class SentencePieceTokenizer {
|
||||
}
|
||||
}
|
||||
|
||||
const spp_llama = new SentencePieceTokenizer('src/sentencepiece/llama.model');
|
||||
const spp_nerd = new SentencePieceTokenizer('src/sentencepiece/nerdstash.model');
|
||||
const spp_nerd_v2 = new SentencePieceTokenizer('src/sentencepiece/nerdstash_v2.model');
|
||||
const spp_mistral = new SentencePieceTokenizer('src/sentencepiece/mistral.model');
|
||||
const spp_yi = new SentencePieceTokenizer('src/sentencepiece/yi.model');
|
||||
let claude_tokenizer;
|
||||
/**
|
||||
* Web tokenizer for tokenizing text.
|
||||
*/
|
||||
class WebTokenizer {
|
||||
/**
|
||||
* @type {Tokenizer} Web tokenizer instance
|
||||
*/
|
||||
#instance;
|
||||
/**
|
||||
* @type {string} Path to the tokenizer model
|
||||
*/
|
||||
#model;
|
||||
|
||||
/**
|
||||
* Creates a new Web tokenizer.
|
||||
* @param {string} model Path to the tokenizer model
|
||||
*/
|
||||
constructor(model) {
|
||||
this.#model = model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Web tokenizer instance.
|
||||
* @returns {Promise<Tokenizer|null>} Web tokenizer instance
|
||||
*/
|
||||
async get() {
|
||||
if (this.#instance) {
|
||||
return this.#instance;
|
||||
}
|
||||
|
||||
try {
|
||||
const arrayBuffer = fs.readFileSync(this.#model).buffer;
|
||||
this.#instance = await Tokenizer.fromJSON(arrayBuffer);
|
||||
console.log('Instantiated the tokenizer for', path.parse(this.#model).name);
|
||||
return this.#instance;
|
||||
} catch (error) {
|
||||
console.error('Web tokenizer failed to load: ' + this.#model, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const spp_llama = new SentencePieceTokenizer('src/tokenizers/llama.model');
|
||||
const spp_nerd = new SentencePieceTokenizer('src/tokenizers/nerdstash.model');
|
||||
const spp_nerd_v2 = new SentencePieceTokenizer('src/tokenizers/nerdstash_v2.model');
|
||||
const spp_mistral = new SentencePieceTokenizer('src/tokenizers/mistral.model');
|
||||
const spp_yi = new SentencePieceTokenizer('src/tokenizers/yi.model');
|
||||
const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');
|
||||
|
||||
const sentencepieceTokenizers = [
|
||||
'llama',
|
||||
'nerdstash',
|
||||
'nerdstash_v2',
|
||||
'mistral',
|
||||
'yi',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -112,6 +173,10 @@ function getSentencepiceTokenizer(model) {
|
||||
return spp_nerd_v2;
|
||||
}
|
||||
|
||||
if (model.includes('yi')) {
|
||||
return spp_yi;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -168,13 +233,23 @@ async function getTiktokenChunks(tokenizer, ids) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
async function getWebTokenizersChunks(tokenizer, ids) {
|
||||
/**
|
||||
* Gets the token chunks for the given token IDs using the Web tokenizer.
|
||||
* @param {Tokenizer} tokenizer Web tokenizer instance
|
||||
* @param {number[]} ids Token IDs
|
||||
* @returns {string[]} Token chunks
|
||||
*/
|
||||
function getWebTokenizersChunks(tokenizer, ids) {
|
||||
const chunks = [];
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
const chunkText = await tokenizer.decode(new Uint32Array([id]));
|
||||
for (let i = 0, lastProcessed = 0; i < ids.length; i++) {
|
||||
const chunkIds = ids.slice(lastProcessed, i + 1);
|
||||
const chunkText = tokenizer.decode(new Int32Array(chunkIds));
|
||||
if (chunkText === '<27>') {
|
||||
continue;
|
||||
}
|
||||
chunks.push(chunkText);
|
||||
lastProcessed = i + 1;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
@ -237,17 +312,12 @@ function getTiktokenTokenizer(model) {
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
async function loadClaudeTokenizer(modelPath) {
|
||||
try {
|
||||
const arrayBuffer = fs.readFileSync(modelPath).buffer;
|
||||
const instance = await Tokenizer.fromJSON(arrayBuffer);
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('Claude tokenizer failed to load: ' + modelPath, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the tokens for the given messages using the Claude tokenizer.
|
||||
* @param {Tokenizer} tokenizer Web tokenizer
|
||||
* @param {object[]} messages Array of messages
|
||||
* @returns {number} Number of tokens
|
||||
*/
|
||||
function countClaudeTokens(tokenizer, messages) {
|
||||
// Should be fine if we use the old conversion method instead of the messages API one i think?
|
||||
const convertedPrompt = convertClaudePrompt(messages, false, '', false, false, '', false);
|
||||
@ -264,9 +334,14 @@ function countClaudeTokens(tokenizer, messages) {
|
||||
/**
|
||||
* Creates an API handler for encoding Sentencepiece tokens.
|
||||
* @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer
|
||||
* @returns {any} Handler function
|
||||
* @returns {TokenizationHandler} Handler function
|
||||
*/
|
||||
function createSentencepieceEncodingHandler(tokenizer) {
|
||||
/**
|
||||
* Request handler for encoding Sentencepiece tokens.
|
||||
* @param {import('express').Request} request
|
||||
* @param {import('express').Response} response
|
||||
*/
|
||||
return async function (request, response) {
|
||||
try {
|
||||
if (!request.body) {
|
||||
@ -276,7 +351,7 @@ function createSentencepieceEncodingHandler(tokenizer) {
|
||||
const text = request.body.text || '';
|
||||
const instance = await tokenizer?.get();
|
||||
const { ids, count } = await countSentencepieceTokens(tokenizer, text);
|
||||
const chunks = await instance?.encodePieces(text);
|
||||
const chunks = instance?.encodePieces(text);
|
||||
return response.send({ ids, count, chunks });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@ -288,9 +363,14 @@ function createSentencepieceEncodingHandler(tokenizer) {
|
||||
/**
|
||||
* Creates an API handler for decoding Sentencepiece tokens.
|
||||
* @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer
|
||||
* @returns {any} Handler function
|
||||
* @returns {TokenizationHandler} Handler function
|
||||
*/
|
||||
function createSentencepieceDecodingHandler(tokenizer) {
|
||||
/**
|
||||
* Request handler for decoding Sentencepiece tokens.
|
||||
* @param {import('express').Request} request
|
||||
* @param {import('express').Response} response
|
||||
*/
|
||||
return async function (request, response) {
|
||||
try {
|
||||
if (!request.body) {
|
||||
@ -299,6 +379,7 @@ function createSentencepieceDecodingHandler(tokenizer) {
|
||||
|
||||
const ids = request.body.ids || [];
|
||||
const instance = await tokenizer?.get();
|
||||
if (!instance) throw new Error('Failed to load the Sentencepiece tokenizer');
|
||||
const ops = ids.map(id => instance.decodeIds([id]));
|
||||
const chunks = await Promise.all(ops);
|
||||
const text = chunks.join('');
|
||||
@ -313,9 +394,14 @@ function createSentencepieceDecodingHandler(tokenizer) {
|
||||
/**
|
||||
* Creates an API handler for encoding Tiktoken tokens.
|
||||
* @param {string} modelId Tiktoken model ID
|
||||
* @returns {any} Handler function
|
||||
* @returns {TokenizationHandler} Handler function
|
||||
*/
|
||||
function createTiktokenEncodingHandler(modelId) {
|
||||
/**
|
||||
* Request handler for encoding Tiktoken tokens.
|
||||
* @param {import('express').Request} request
|
||||
* @param {import('express').Response} response
|
||||
*/
|
||||
return async function (request, response) {
|
||||
try {
|
||||
if (!request.body) {
|
||||
@ -337,9 +423,14 @@ function createTiktokenEncodingHandler(modelId) {
|
||||
/**
|
||||
* Creates an API handler for decoding Tiktoken tokens.
|
||||
* @param {string} modelId Tiktoken model ID
|
||||
* @returns {any} Handler function
|
||||
* @returns {TokenizationHandler} Handler function
|
||||
*/
|
||||
function createTiktokenDecodingHandler(modelId) {
|
||||
/**
|
||||
* Request handler for decoding Tiktoken tokens.
|
||||
* @param {import('express').Request} request
|
||||
* @param {import('express').Response} response
|
||||
*/
|
||||
return async function (request, response) {
|
||||
try {
|
||||
if (!request.body) {
|
||||
@ -358,14 +449,6 @@ function createTiktokenDecodingHandler(modelId) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the model tokenizers.
|
||||
* @returns {Promise<void>} Promise that resolves when the tokenizers are loaded
|
||||
*/
|
||||
async function loadTokenizers() {
|
||||
claude_tokenizer = await loadClaudeTokenizer('src/claude.json');
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/ai21/count', jsonParser, async function (req, res) {
|
||||
@ -446,8 +529,10 @@ router.post('/openai/encode', jsonParser, async function (req, res) {
|
||||
|
||||
if (queryModel.includes('claude')) {
|
||||
const text = req.body.text || '';
|
||||
const tokens = Object.values(claude_tokenizer.encode(text));
|
||||
const chunks = await getWebTokenizersChunks(claude_tokenizer, tokens);
|
||||
const instance = await claude_tokenizer.get();
|
||||
if (!instance) throw new Error('Failed to load the Claude tokenizer');
|
||||
const tokens = Object.values(instance.encode(text));
|
||||
const chunks = getWebTokenizersChunks(instance, tokens);
|
||||
return res.send({ ids: tokens, count: tokens.length, chunks });
|
||||
}
|
||||
|
||||
@ -481,7 +566,9 @@ router.post('/openai/decode', jsonParser, async function (req, res) {
|
||||
|
||||
if (queryModel.includes('claude')) {
|
||||
const ids = req.body.ids || [];
|
||||
const chunkText = await claude_tokenizer.decode(new Uint32Array(ids));
|
||||
const instance = await claude_tokenizer.get();
|
||||
if (!instance) throw new Error('Failed to load the Claude tokenizer');
|
||||
const chunkText = instance.decode(new Int32Array(ids));
|
||||
return res.send({ text: chunkText });
|
||||
}
|
||||
|
||||
@ -503,7 +590,9 @@ router.post('/openai/count', jsonParser, async function (req, res) {
|
||||
const model = getTokenizerModel(queryModel);
|
||||
|
||||
if (model === 'claude') {
|
||||
num_tokens = countClaudeTokens(claude_tokenizer, req.body);
|
||||
const instance = await claude_tokenizer.get();
|
||||
if (!instance) throw new Error('Failed to load the Claude tokenizer');
|
||||
num_tokens = countClaudeTokens(instance, req.body);
|
||||
return res.send({ 'token_count': num_tokens });
|
||||
}
|
||||
|
||||
@ -665,7 +754,6 @@ module.exports = {
|
||||
getTokenizerModel,
|
||||
getTiktokenTokenizer,
|
||||
countClaudeTokens,
|
||||
loadTokenizers,
|
||||
getSentencepiceTokenizer,
|
||||
sentencepieceTokenizers,
|
||||
router,
|
||||
|
30
src/middleware/multerMonkeyPatch.js
Normal file
30
src/middleware/multerMonkeyPatch.js
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Decodes a file name from Latin1 to UTF-8.
|
||||
* @param {string} str Input string
|
||||
* @returns {string} Decoded file name
|
||||
*/
|
||||
function decodeFileName(str) {
|
||||
return Buffer.from(str, 'latin1').toString('utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to decode file names from Latin1 to UTF-8.
|
||||
* See: https://github.com/expressjs/multer/issues/1104
|
||||
* @param {import('express').Request} req Request
|
||||
* @param {import('express').Response} _res Response
|
||||
* @param {import('express').NextFunction} next Next middleware
|
||||
*/
|
||||
function multerMonkeyPatch(req, _res, next) {
|
||||
try {
|
||||
if (req.file) {
|
||||
req.file.originalname = decodeFileName(req.file.originalname);
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error in multerMonkeyPatch:', error);
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = multerMonkeyPatch;
|
23
src/users.js
23
src/users.js
@ -112,6 +112,16 @@ async function ensurePublicDirectoriesExist() {
|
||||
return directoriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all user directories.
|
||||
* @returns {Promise<import('./users').UserDirectoryList[]>} - The list of user directories
|
||||
*/
|
||||
async function getUserDirectoriesList() {
|
||||
const userHandles = await getAllUserHandles();
|
||||
const directoriesList = userHandles.map(handle => getUserDirectories(handle));
|
||||
return directoriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform migration from the old user data format to the new one.
|
||||
*/
|
||||
@ -289,7 +299,7 @@ async function migrateUserData() {
|
||||
fs.cpSync(
|
||||
migration.old,
|
||||
path.join(backupDirectory, path.basename(migration.old)),
|
||||
{ recursive: true, force: true }
|
||||
{ recursive: true, force: true },
|
||||
);
|
||||
fs.rmSync(migration.old, { recursive: true, force: true });
|
||||
} else {
|
||||
@ -299,7 +309,7 @@ async function migrateUserData() {
|
||||
fs.cpSync(
|
||||
migration.old,
|
||||
path.join(backupDirectory, path.basename(migration.old)),
|
||||
{ recursive: true, force: true }
|
||||
{ recursive: true, force: true },
|
||||
);
|
||||
fs.rmSync(migration.old, { recursive: true, force: true });
|
||||
}
|
||||
@ -511,7 +521,7 @@ async function tryAutoLogin(request) {
|
||||
const userHandles = await getAllUserHandles();
|
||||
if (userHandles.length === 1) {
|
||||
const user = await storage.getItem(toKey(userHandles[0]));
|
||||
if (!user.password) {
|
||||
if (user && !user.password) {
|
||||
request.session.handle = userHandles[0];
|
||||
return true;
|
||||
}
|
||||
@ -602,9 +612,13 @@ function createRouteHandler(directoryFn) {
|
||||
try {
|
||||
const directory = directoryFn(req);
|
||||
const filePath = decodeURIComponent(req.params[0]);
|
||||
const exists = fs.existsSync(path.join(directory, filePath));
|
||||
if (!exists) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
return res.sendFile(filePath, { root: directory });
|
||||
} catch (error) {
|
||||
return res.sendStatus(404);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -707,6 +721,7 @@ module.exports = {
|
||||
toAvatarKey,
|
||||
initUserStorage,
|
||||
ensurePublicDirectoriesExist,
|
||||
getUserDirectoriesList,
|
||||
getAllUserHandles,
|
||||
getUserDirectories,
|
||||
setUserDataMiddleware,
|
||||
|
17
src/util.js
17
src/util.js
@ -1,6 +1,7 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const commandExistsSync = require('command-exists').sync;
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const _ = require('lodash');
|
||||
const yauzl = require('yauzl');
|
||||
const mime = require('mime-types');
|
||||
@ -10,11 +11,20 @@ const { Readable } = require('stream');
|
||||
|
||||
const { PUBLIC_DIRECTORIES } = require('./constants');
|
||||
|
||||
/**
|
||||
* Parsed config object.
|
||||
*/
|
||||
let CACHED_CONFIG = null;
|
||||
|
||||
/**
|
||||
* Returns the config object from the config.yaml file.
|
||||
* @returns {object} Config object
|
||||
*/
|
||||
function getConfig() {
|
||||
if (CACHED_CONFIG) {
|
||||
return CACHED_CONFIG;
|
||||
}
|
||||
|
||||
if (!fs.existsSync('./config.yaml')) {
|
||||
console.error(color.red('No config file found. Please create a config.yaml file. The default config file can be found in the /default folder.'));
|
||||
console.error(color.red('The program will now exit.'));
|
||||
@ -23,6 +33,7 @@ function getConfig() {
|
||||
|
||||
try {
|
||||
const config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8'));
|
||||
CACHED_CONFIG = config;
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.warn('Failed to read config.yaml');
|
||||
@ -47,9 +58,11 @@ function getConfigValue(key, defaultValue = null) {
|
||||
* @param {any} value Value to set
|
||||
*/
|
||||
function setConfigValue(key, value) {
|
||||
// Reset cache so that the next getConfig call will read the updated config file
|
||||
CACHED_CONFIG = null;
|
||||
const config = getConfig();
|
||||
_.set(config, key, value);
|
||||
fs.writeFileSync('./config.yaml', yaml.stringify(config));
|
||||
writeFileAtomicSync('./config.yaml', yaml.stringify(config));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -350,7 +363,7 @@ function generateTimestamp() {
|
||||
* @param {string} prefix
|
||||
*/
|
||||
function removeOldBackups(prefix) {
|
||||
const MAX_BACKUPS = 25;
|
||||
const MAX_BACKUPS = 50;
|
||||
|
||||
let files = fs.readdirSync(PUBLIC_DIRECTORIES.backups).filter(f => f.startsWith(prefix));
|
||||
if (files.length > MAX_BACKUPS) {
|
||||
|
5
start.sh
5
start.sh
@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Make sure pwd is the directory of the script
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if ! command -v npm &> /dev/null
|
||||
then
|
||||
read -p "npm is not installed. Do you want to install nodejs and npm? (y/n)" choice
|
||||
@ -26,4 +29,4 @@ export NODE_ENV=production
|
||||
npm i --no-audit --no-fund --quiet --omit=dev
|
||||
|
||||
echo "Entering SillyTavern..."
|
||||
node "$(dirname "$0")/server.js" "$@"
|
||||
node "server.js" "$@"
|
||||
|
Loading…
x
Reference in New Issue
Block a user