Merge branch 'staging' into integrity

5ab284f1f5e262cee16a4edc6a905a00750adc4e

Cohee <18619528+Cohee1207@users.noreply.github.com>

19 files changed, +748 -132Showing whitespace changes
.github/workflows/issues-auto-manager.yml+13 -9
@@ -7,6 +7,10 @@ on:
7 issue_comment:7 issue_comment:
8 types: [created]8 types: [created]
99
10permissions:
11 contents: read
12 issues: write
13
10jobs:14jobs:
11 label-on-content:15 label-on-content:
12 name: 🏷️ Label Issues by Content16 name: 🏷️ Label Issues by Content
@@ -16,7 +20,7 @@ jobs:
16 - name: Checkout Repository20 - name: Checkout Repository
17 # Checkout21 # Checkout
18 # https://github.com/marketplace/actions/checkout22 # https://github.com/marketplace/actions/checkout
19 uses: actions/checkout@v423 uses: actions/checkout@v4.2.2
2024
21 - name: Auto-Label Issues (Based on Issue Content)25 - name: Auto-Label Issues (Based on Issue Content)
22 # only auto label based on issue content once, on open (to prevent re-labeling removed labels)26 # only auto label based on issue content once, on open (to prevent re-labeling removed labels)
@@ -24,7 +28,7 @@ jobs:
2428
25 # Issue Labeler29 # Issue Labeler
26 # https://github.com/marketplace/actions/regex-issue-labeler30 # https://github.com/marketplace/actions/regex-issue-labeler
27 uses: github/issue-labeler@v331 uses: github/issue-labeler@v3.4
28 with:32 with:
29 configuration-path: .github/issues-auto-labels.yml33 configuration-path: .github/issues-auto-labels.yml
30 enable-versioned-regex: 034 enable-versioned-regex: 0
@@ -39,7 +43,7 @@ jobs:
39 if: contains(fromJSON('["πŸ‘©β€πŸ’» Good First Issue", "πŸ™ Help Wanted", "πŸͺ² Confirmed", "⚠️ High Priority", "❕ Medium Priority", "πŸ’€ Low Priority"]'), github.event.label.name)43 if: contains(fromJSON('["πŸ‘©β€πŸ’» Good First Issue", "πŸ™ Help Wanted", "πŸͺ² Confirmed", "⚠️ High Priority", "❕ Medium Priority", "πŸ’€ Low Priority"]'), github.event.label.name)
40 # πŸ€– Issues Helper44 # πŸ€– Issues Helper
41 # https://github.com/marketplace/actions/issues-helper45 # https://github.com/marketplace/actions/issues-helper
42 uses: actions-cool/issues-helper@v346 uses: actions-cool/issues-helper@v3.6.0
43 with:47 with:
44 actions: 'add-labels'48 actions: 'add-labels'
45 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}49 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -49,7 +53,7 @@ jobs:
49 if: contains(fromJSON('["βœ… Done", "βœ… Done (staging)", "⚰️ Stale", "❌ wontfix"]'), github.event.label.name)53 if: contains(fromJSON('["βœ… Done", "βœ… Done (staging)", "⚰️ Stale", "❌ wontfix"]'), github.event.label.name)
50 # πŸ€– Issues Helper54 # πŸ€– Issues Helper
51 # https://github.com/marketplace/actions/issues-helper55 # https://github.com/marketplace/actions/issues-helper
52 uses: actions-cool/issues-helper@v356 uses: actions-cool/issues-helper@v3.6.0
53 with:57 with:
54 actions: 'remove-labels'58 actions: 'remove-labels'
55 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}59 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -59,7 +63,7 @@ jobs:
59 if: contains(fromJSON('["❌ wontfix","πŸ‘ Approved","πŸ‘©β€πŸ’» Good First Issue"]'), github.event.label.name)63 if: contains(fromJSON('["❌ wontfix","πŸ‘ Approved","πŸ‘©β€πŸ’» Good First Issue"]'), github.event.label.name)
60 # πŸ€– Issues Helper64 # πŸ€– Issues Helper
61 # https://github.com/marketplace/actions/issues-helper65 # https://github.com/marketplace/actions/issues-helper
62 uses: actions-cool/issues-helper@v366 uses: actions-cool/issues-helper@v3.6.0
63 with:67 with:
64 actions: 'remove-labels'68 actions: 'remove-labels'
65 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}69 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -69,7 +73,7 @@ jobs:
69 if: github.event.label.name == 'πŸͺ² Confirmed'73 if: github.event.label.name == 'πŸͺ² Confirmed'
70 # πŸ€– Issues Helper74 # πŸ€– Issues Helper
71 # https://github.com/marketplace/actions/issues-helper75 # https://github.com/marketplace/actions/issues-helper
72 uses: actions-cool/issues-helper@v376 uses: actions-cool/issues-helper@v3.6.0
73 with:77 with:
74 actions: 'remove-labels'78 actions: 'remove-labels'
75 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}79 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -85,7 +89,7 @@ jobs:
85 - name: Remove Stale Label89 - name: Remove Stale Label
86 # πŸ€– Issues Helper90 # πŸ€– Issues Helper
87 # https://github.com/marketplace/actions/issues-helper91 # https://github.com/marketplace/actions/issues-helper
88 uses: actions-cool/issues-helper@v392 uses: actions-cool/issues-helper@v3.6.0
89 with:93 with:
90 actions: 'remove-labels'94 actions: 'remove-labels'
91 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}95 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -101,12 +105,12 @@ jobs:
101 - name: Checkout Repository105 - name: Checkout Repository
102 # Checkout106 # Checkout
103 # https://github.com/marketplace/actions/checkout107 # https://github.com/marketplace/actions/checkout
104 uses: actions/checkout@v4108 uses: actions/checkout@v4.2.2
105109
106 - name: Post Issue Comments Based on Labels110 - name: Post Issue Comments Based on Labels
107 # Label Commenter111 # Label Commenter
108 # https://github.com/marketplace/actions/label-commenter112 # https://github.com/marketplace/actions/label-commenter
109 uses: peaceiris/actions-label-commenter@v1113 uses: peaceiris/actions-label-commenter@v1.10.0
110 with:114 with:
111 config_file: .github/issues-auto-comments.yml115 config_file: .github/issues-auto-comments.yml
112 github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}116 github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
.github/workflows/issues-updates-on-merge.yml+6 -2
@@ -6,6 +6,10 @@ on:
6 - staging6 - staging
7 - release7 - release
88
9permissions:
10 contents: read
11 issues: write
12
9jobs:13jobs:
10 # This runs commits to staging/release, reading the commit messages. Check `pr-auto-manager.yml`:`update-linked-issues` for PR-linked updates.14 # This runs commits to staging/release, reading the commit messages. Check `pr-auto-manager.yml`:`update-linked-issues` for PR-linked updates.
11 update-linked-issues:15 update-linked-issues:
@@ -16,12 +20,12 @@ jobs:
16 - name: Checkout Repository20 - name: Checkout Repository
17 # Checkout21 # Checkout
18 # https://github.com/marketplace/actions/checkout22 # https://github.com/marketplace/actions/checkout
19 uses: actions/checkout@v423 uses: actions/checkout@v4.2.2
2024
21 - name: Extract Linked Issues from Commit Message25 - name: Extract Linked Issues from Commit Message
22 id: extract_issues26 id: extract_issues
23 run: |27 run: |
24 ISSUES=$(git log -1 --pretty=%B | grep -oiE '(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) #([0-9]+)' | awk '{print $2}' | tr -d '#' | jq -R -s -c 'split("\n")[:-1]')28 ISSUES=$(git log ${{ github.event.before }}..${{ github.event.after }} --pretty=%B | grep -oiE '(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) #([0-9]+)' | awk '{print $2}' | tr -d '#' | jq -R -s -c 'split("\n")[:-1]')
25 echo "issues=$ISSUES" >> $GITHUB_ENV29 echo "issues=$ISSUES" >> $GITHUB_ENV
2630
27 - name: Label Linked Issues31 - name: Label Linked Issues
.github/workflows/job-close-stale.yml+8 -3
@@ -6,6 +6,11 @@ on:
6 schedule:6 schedule:
7 - cron: '0 0 * * *' # Runs every day at midnight UTC7 - cron: '0 0 * * *' # Runs every day at midnight UTC
88
9permissions:
10 contents: read
11 issues: write
12 pull-requests: write
13
9jobs:14jobs:
10 mark-inactivity:15 mark-inactivity:
11 name: ⏳ Mark Issues/PRs without Activity16 name: ⏳ Mark Issues/PRs without Activity
@@ -15,7 +20,7 @@ jobs:
15 - name: Mark Issues/PRs without Activity20 - name: Mark Issues/PRs without Activity
16 # Close Stale Issues and PRs21 # Close Stale Issues and PRs
17 # https://github.com/marketplace/actions/close-stale-issues22 # https://github.com/marketplace/actions/close-stale-issues
18 uses: actions/stale@v923 uses: actions/stale@v9.1.0
19 with:24 with:
20 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}25 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
21 days-before-stale: 18326 days-before-stale: 183
@@ -49,7 +54,7 @@ jobs:
49 - name: Mark Issues/PRs Awaiting User Response54 - name: Mark Issues/PRs Awaiting User Response
50 # Close Stale Issues and PRs55 # Close Stale Issues and PRs
51 # https://github.com/marketplace/actions/close-stale-issues56 # https://github.com/marketplace/actions/close-stale-issues
52 uses: actions/stale@v957 uses: actions/stale@v9.1.0
53 with:58 with:
54 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}59 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
55 days-before-stale: 760 days-before-stale: 7
@@ -76,7 +81,7 @@ jobs:
76 - name: Mark Issues with Alternative Exists81 - name: Mark Issues with Alternative Exists
77 # Close Stale Issues and PRs82 # Close Stale Issues and PRs
78 # https://github.com/marketplace/actions/close-stale-issues83 # https://github.com/marketplace/actions/close-stale-issues
79 uses: actions/stale@v984 uses: actions/stale@v9.1.0
80 with:85 with:
81 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}86 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
82 days-before-stale: 787 days-before-stale: 7
.github/workflows/on-close-handler.yml+6 -1
@@ -6,6 +6,11 @@ on:
6 pull_request_target:6 pull_request_target:
7 types: [closed]7 types: [closed]
88
9permissions:
10 contents: read
11 issues: write
12 pull-requests: write
13
9jobs:14jobs:
10 remove-labels:15 remove-labels:
11 name: πŸ—‘οΈ Remove Pending Labels on Close16 name: πŸ—‘οΈ Remove Pending Labels on Close
@@ -15,7 +20,7 @@ jobs:
15 - name: Remove Pending Labels on Close20 - name: Remove Pending Labels on Close
16 # πŸ€– Issues Helper21 # πŸ€– Issues Helper
17 # https://github.com/marketplace/actions/issues-helper22 # https://github.com/marketplace/actions/issues-helper
18 uses: actions-cool/issues-helper@v323 uses: actions-cool/issues-helper@v3.6.0
19 with:24 with:
20 actions: remove-labels25 actions: remove-labels
21 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}26 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
.github/workflows/on-open-handler.yml+6 -1
@@ -6,6 +6,11 @@ on:
6 pull_request_target:6 pull_request_target:
7 types: [opened]7 types: [opened]
88
9permissions:
10 contents: read
11 issues: write
12 pull-requests: write
13
9jobs:14jobs:
10 label-maintainer:15 label-maintainer:
11 name: 🏷️ Label if Author is a Repo Maintainer16 name: 🏷️ Label if Author is a Repo Maintainer
@@ -16,7 +21,7 @@ jobs:
16 - name: Label if Author is a Repo Maintainer21 - name: Label if Author is a Repo Maintainer
17 # πŸ€– Issues Helper22 # πŸ€– Issues Helper
18 # https://github.com/marketplace/actions/issues-helper23 # https://github.com/marketplace/actions/issues-helper
19 uses: actions-cool/issues-helper@v324 uses: actions-cool/issues-helper@v3.6.0
20 with:25 with:
21 actions: 'add-labels'26 actions: 'add-labels'
22 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}27 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
.github/workflows/pr-auto-manager.yml+21 -11
@@ -6,6 +6,10 @@ on:
6 pull_request_review_comment:6 pull_request_review_comment:
7 types: [created]7 types: [created]
88
9permissions:
10 contents: read
11 pull-requests: write
12
9jobs:13jobs:
10 label-by-size:14 label-by-size:
11 name: 🏷️ Label PR by Size15 name: 🏷️ Label PR by Size
@@ -15,7 +19,7 @@ jobs:
15 - name: Label PR Size19 - name: Label PR Size
16 # Pull Request Size Labeler20 # Pull Request Size Labeler
17 # https://github.com/marketplace/actions/pull-request-size-labeler21 # https://github.com/marketplace/actions/pull-request-size-labeler
18 uses: codelytv/pr-size-labeler@v122 uses: codelytv/pr-size-labeler@v1.10.2
19 with:23 with:
20 GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}24 GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
21 xs_label: '🟩 ⬀○○○○'25 xs_label: '🟩 ⬀○○○○'
@@ -43,12 +47,12 @@ jobs:
43 - name: Checkout Repository47 - name: Checkout Repository
44 # Checkout48 # Checkout
45 # https://github.com/marketplace/actions/checkout49 # https://github.com/marketplace/actions/checkout
46 uses: actions/checkout@v450 uses: actions/checkout@v4.2.2
4751
48 - name: Apply Labels Based on Branch Name and Target Branch52 - name: Apply Labels Based on Branch Name and Target Branch
49 # Pull Request Labeler53 # Pull Request Labeler
50 # https://github.com/marketplace/actions/labeler54 # https://github.com/marketplace/actions/labeler
51 uses: actions/labeler@v555 uses: actions/labeler@v5.0.0
52 with:56 with:
53 configuration-path: .github/pr-auto-labels-by-branch.yml57 configuration-path: .github/pr-auto-labels-by-branch.yml
54 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}58 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -61,12 +65,12 @@ jobs:
61 - name: Checkout Repository65 - name: Checkout Repository
62 # Checkout66 # Checkout
63 # https://github.com/marketplace/actions/checkout67 # https://github.com/marketplace/actions/checkout
64 uses: actions/checkout@v468 uses: actions/checkout@v4.2.2
6569
66 - name: Apply Labels Based on Changed Files70 - name: Apply Labels Based on Changed Files
67 # Pull Request Labeler71 # Pull Request Labeler
68 # https://github.com/marketplace/actions/labeler72 # https://github.com/marketplace/actions/labeler
69 uses: actions/labeler@v573 uses: actions/labeler@v5.0.0
70 with:74 with:
71 configuration-path: .github/pr-auto-labels-by-files.yml75 configuration-path: .github/pr-auto-labels-by-files.yml
72 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}76 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -75,13 +79,13 @@ jobs:
75 name: πŸ—‘οΈ Remove Stale Label on Comment79 name: πŸ—‘οΈ Remove Stale Label on Comment
76 runs-on: ubuntu-latest80 runs-on: ubuntu-latest
77 # Only runs when this is not done by the github actions bot81 # Only runs when this is not done by the github actions bot
78 if: github.actor != 'github-actions[bot]'82 if: github.event_name == 'pull_request_review_comment' && github.actor != 'github-actions[bot]'
7983
80 steps:84 steps:
81 - name: Remove Stale Label85 - name: Remove Stale Label
82 # πŸ€– Issues Helper86 # πŸ€– Issues Helper
83 # https://github.com/marketplace/actions/issues-helper87 # https://github.com/marketplace/actions/issues-helper
84 uses: actions-cool/issues-helper@v388 uses: actions-cool/issues-helper@v3.6.0
85 with:89 with:
86 actions: 'remove-labels'90 actions: 'remove-labels'
87 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}91 token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -95,12 +99,18 @@ jobs:
95 # Run, even if the previous jobs were skipped/failed99 # Run, even if the previous jobs were skipped/failed
96 if: always()100 if: always()
97101
102 # Override permissions, as this needs to write a check
103 permissions:
104 checks: write
105 contents: read
106 pull-requests: read
107
98 steps:108 steps:
99 - name: Check Merge Blocking109 - name: Check Merge Blocking
100 # GitHub Script110 # GitHub Script
101 # https://github.com/marketplace/actions/github-scriptLabels111 # https://github.com/marketplace/actions/github-script
102 id: label-check112 id: label-check
103 uses: actions/github-script@v7113 uses: actions/github-script@v7.0.1
104 with:114 with:
105 script: |115 script: |
106 const prLabels = context.payload.pull_request.labels.map(label => label.name);116 const prLabels = context.payload.pull_request.labels.map(label => label.name);
@@ -143,12 +153,12 @@ jobs:
143 - name: Checkout Repository153 - name: Checkout Repository
144 # Checkout154 # Checkout
145 # https://github.com/marketplace/actions/checkout155 # https://github.com/marketplace/actions/checkout
146 uses: actions/checkout@v4156 uses: actions/checkout@v4.2.2
147157
148 - name: Post PR Comments Based on Labels158 - name: Post PR Comments Based on Labels
149 # Label Commenter for PRs159 # Label Commenter for PRs
150 # https://github.com/marketplace/actions/label-commenter160 # https://github.com/marketplace/actions/label-commenter
151 uses: peaceiris/actions-label-commenter@v1161 uses: peaceiris/actions-label-commenter@v1.10.0
152 with:162 with:
153 config_file: .github/pr-auto-comments.yml163 config_file: .github/pr-auto-comments.yml
154 github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}164 github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
.github/workflows/pr-check-merge-conflicts.yaml+5 -1
@@ -7,6 +7,10 @@ on:
7 pull_request_target:7 pull_request_target:
8 types: [synchronize]8 types: [synchronize]
99
10permissions:
11 contents: read
12 pull-requests: write
13
10jobs:14jobs:
11 check-merge-conflicts:15 check-merge-conflicts:
12 name: βš”οΈ Check Merge Conflicts16 name: βš”οΈ Check Merge Conflicts
@@ -16,7 +20,7 @@ jobs:
16 - name: Check Merge Conflicts20 - name: Check Merge Conflicts
17 # Label Conflicting Pull Requests21 # Label Conflicting Pull Requests
18 # https://github.com/marketplace/actions/label-conflicting-pull-requests22 # https://github.com/marketplace/actions/label-conflicting-pull-requests
19 uses: eps1lon/actions-label-merge-conflict@v323 uses: eps1lon/actions-label-merge-conflict@v3.0.3
20 with:24 with:
21 dirtyLabel: '🚫 Merge Conflicts'25 dirtyLabel: '🚫 Merge Conflicts'
22 repoToken: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}26 repoToken: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
public/global.d.ts+4 -0
@@ -1,7 +1,11 @@
1import libs from './lib';1import libs from './lib';
2import getContext from './scripts/st-context';2import getContext from './scripts/st-context';
3import { power_user } from './scripts/power-user';
34
4declare global {5declare global {
6 // Custom types
7 declare type InstructSettings = typeof power_user.instruct;
8
5 // Global namespace modules9 // Global namespace modules
6 interface Window {10 interface Window {
7 ai: any;11 ai: any;
public/script.js+60 -25
@@ -515,6 +515,9 @@ export const event_types = {
515 ONLINE_STATUS_CHANGED: 'online_status_changed',515 ONLINE_STATUS_CHANGED: 'online_status_changed',
516 IMAGE_SWIPED: 'image_swiped',516 IMAGE_SWIPED: 'image_swiped',
517 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',517 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
518 CONNECTION_PROFILE_CREATED: 'connection_profile_created',
519 CONNECTION_PROFILE_DELETED: 'connection_profile_deleted',
520 CONNECTION_PROFILE_UPDATED: 'connection_profile_updated',
518 TOOL_CALLS_PERFORMED: 'tool_calls_performed',521 TOOL_CALLS_PERFORMED: 'tool_calls_performed',
519 TOOL_CALLS_RENDERED: 'tool_calls_rendered',522 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
520};523};
@@ -1371,8 +1374,11 @@ export function resultCheckStatus() {
1371 * If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing.1374 * If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing.
1372 * If the character is different from the currently selected one, it will clear the chat and reset any selected character or group.1375 * If the character is different from the currently selected one, it will clear the chat and reset any selected character or group.
1373 * @param {number} id The ID of the character to switch to.1376 * @param {number} id The ID of the character to switch to.
1377 * @param {object} [options] Options for the switch.
1378 * @param {boolean} [options.switchMenu=true] Whether to switch the right menu to the character edit menu if the character is already selected.
1379 * @returns {Promise<void>} A promise that resolves when the character is switched.
1374 */1380 */
1375export async function selectCharacterById(id) {1381export async function selectCharacterById(id, { switchMenu = true } = {}) {
1376 if (characters[id] === undefined) {1382 if (characters[id] === undefined) {
1377 return;1383 return;
1378 }1384 }
@@ -1401,9 +1407,9 @@ export async function selectCharacterById(id) {
1401 }1407 }
1402 } else {1408 } else {
1403 //if clicked on character that was already selected1409 //if clicked on character that was already selected
1404 selected_button = 'character_edit';1410 switchMenu && (selected_button = 'character_edit');
1405 await unshallowCharacter(this_chid);1411 await unshallowCharacter(this_chid);
1406 select_selected_character(this_chid);1412 select_selected_character(this_chid, { switchMenu });
1407 }1413 }
1408}1414}
14091415
@@ -1788,6 +1794,7 @@ export async function getCharacters() {
1788 body: JSON.stringify({}),1794 body: JSON.stringify({}),
1789 });1795 });
1790 if (response.ok === true) {1796 if (response.ok === true) {
1797 const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null;
1791 characters.splice(0, characters.length);1798 characters.splice(0, characters.length);
1792 const getData = await response.json();1799 const getData = await response.json();
1793 for (let i = 0; i < getData.length; i++) {1800 for (let i = 0; i < getData.length; i++) {
@@ -1801,8 +1808,16 @@ export async function getCharacters() {
18011808
1802 characters[i]['chat'] = String(characters[i]['chat']);1809 characters[i]['chat'] = String(characters[i]['chat']);
1803 }1810 }
1804 if (this_chid !== undefined) {1811
1805 $('#avatar_url_pole').val(characters[this_chid].avatar);1812 if (previousAvatar) {
1813 const newCharacterId = characters.findIndex(x => x.avatar === previousAvatar);
1814 if (newCharacterId >= 0) {
1815 setCharacterId(newCharacterId);
1816 await selectCharacterById(newCharacterId, { switchMenu: false });
1817 } else {
1818 await Popup.show.text(t`ERROR: The active character is no longer available.`, t`The page will be refreshed to prevent data loss. Press "OK" to continue.`);
1819 return location.reload();
1820 }
1806 }1821 }
18071822
1808 await getGroups();1823 await getGroups();
@@ -6528,6 +6543,8 @@ export async function renameCharacter(name = null, { silent = false, renameChats
65286543
6529 await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);6544 await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
65306545
6546 // Unload current character
6547 setCharacterId(undefined);
6531 // Reload characters list6548 // Reload characters list
6532 await getCharacters();6549 await getCharacters();
65336550
@@ -6536,7 +6553,6 @@ export async function renameCharacter(name = null, { silent = false, renameChats
65366553
6537 if (newChId !== -1) {6554 if (newChId !== -1) {
6538 // Select the character after the renaming6555 // Select the character after the renaming
6539 setCharacterId(undefined);
6540 await selectCharacterById(newChId);6556 await selectCharacterById(newChId);
65416557
6542 // Async delay to update UI6558 // Async delay to update UI
@@ -6871,14 +6887,14 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
6871 */6887 */
6872export async function unshallowCharacter(characterId) {6888export async function unshallowCharacter(characterId) {
6873 if (characterId === undefined) {6889 if (characterId === undefined) {
6874 console.warn('Undefined character cannot be unshallowed');6890 console.debug('Undefined character cannot be unshallowed');
6875 return;6891 return;
6876 }6892 }
68776893
6878 /** @type {import('./scripts/char-data.js').v1CharData} */6894 /** @type {import('./scripts/char-data.js').v1CharData} */
6879 const character = characters[characterId];6895 const character = characters[characterId];
6880 if (!character) {6896 if (!character) {
6881 console.warn('Character not found:', characterId);6897 console.debug('Character not found:', characterId);
6882 return;6898 return;
6883 }6899 }
68846900
@@ -6889,7 +6905,7 @@ export async function unshallowCharacter(characterId) {
68896905
6890 const avatar = character.avatar;6906 const avatar = character.avatar;
6891 if (!avatar) {6907 if (!avatar) {
6892 console.warn('Character has no avatar field:', characterId);6908 console.debug('Character has no avatar field:', characterId);
6893 return;6909 return;
6894 }6910 }
68956911
@@ -7895,14 +7911,19 @@ export function select_rm_info(type, charId, previousCharId = null) {
7895 }7911 }
7896}7912}
78977913
7898export function select_selected_character(chid) {7914/**
7915 * Selects the right menu for displaying the character editor.
7916 * @param {number|string} chid Character array index
7917 * @param {object} [param1] Options for the switch
7918 * @param {boolean} [param1.switchMenu=true] Whether to switch the menu
7919 */
7920export function select_selected_character(chid, { switchMenu = true } = {}) {
7899 //character select7921 //character select
7900 //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')');7922 //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')');
7901 select_rm_create();7923 select_rm_create({ switchMenu });
7902 setMenuType('character_edit');7924 switchMenu && setMenuType('character_edit');
7903 $('#delete_button').css('display', 'flex');7925 $('#delete_button').css('display', 'flex');
7904 $('#export_button').css('display', 'flex');7926 $('#export_button').css('display', 'flex');
7905 var display_name = characters[chid].name;
79067927
7907 //create text poles7928 //create text poles
7908 $('#rm_button_back').css('display', 'none');7929 $('#rm_button_back').css('display', 'none');
@@ -7917,7 +7938,7 @@ export function select_selected_character(chid) {
79177938
7918 // Don't update the navbar name if we're peeking the group member defs7939 // Don't update the navbar name if we're peeking the group member defs
7919 if (!selected_group) {7940 if (!selected_group) {
7920 $('#rm_button_selected_ch').children('h2').text(display_name);7941 $('#rm_button_selected_ch').children('h2').text(characters[chid].name);
7921 }7942 }
79227943
7923 $('#add_avatar_button').val('');7944 $('#add_avatar_button').val('');
@@ -7948,22 +7969,20 @@ export function select_selected_character(chid) {
7948 $('#chat_import_avatar_url').val(characters[chid].avatar);7969 $('#chat_import_avatar_url').val(characters[chid].avatar);
7949 $('#chat_import_character_name').val(characters[chid].name);7970 $('#chat_import_character_name').val(characters[chid].name);
7950 $('#character_json_data').val(characters[chid].json_data);7971 $('#character_json_data').val(characters[chid].json_data);
7951 let this_avatar = default_avatar;
7952 if (characters[chid].avatar != 'none') {
7953 this_avatar = getThumbnailUrl('avatar', characters[chid].avatar);
7954 }
79557972
7956 updateFavButtonState(characters[chid].fav || characters[chid].fav == 'true');7973 updateFavButtonState(characters[chid].fav || characters[chid].fav == 'true');
79577974
7958 $('#avatar_load_preview').attr('src', this_avatar);7975 const avatarUrl = characters[chid].avatar != 'none' ? getThumbnailUrl('avatar', characters[chid].avatar) : default_avatar;
7959 $('#name_div').removeClass('displayBlock');7976 $('#avatar_load_preview').attr('src', avatarUrl);
7960 $('#name_div').addClass('displayNone');
7961 $('#renameCharButton').css('display', '');
7962 $('.open_alternate_greetings').data('chid', chid);7977 $('.open_alternate_greetings').data('chid', chid);
7963 $('#set_character_world').data('chid', chid);7978 $('#set_character_world').data('chid', chid);
7964 setWorldInfoButtonClass(chid);7979 setWorldInfoButtonClass(chid);
7965 checkEmbeddedWorld(chid);7980 checkEmbeddedWorld(chid);
79667981
7982 $('#name_div').removeClass('displayBlock');
7983 $('#name_div').addClass('displayNone');
7984 $('#renameCharButton').css('display', '');
7985
7967 $('#form_create').attr('actiontype', 'editcharacter');7986 $('#form_create').attr('actiontype', 'editcharacter');
7968 $('.form_create_bottom_buttons_block .chat_lorebook_button').show();7987 $('.form_create_bottom_buttons_block .chat_lorebook_button').show();
79697988
@@ -7975,8 +7994,13 @@ export function select_selected_character(chid) {
7975 saveSettingsDebounced();7994 saveSettingsDebounced();
7976}7995}
79777996
7978function select_rm_create() {7997/**
7979 setMenuType('create');7998 * Selects the right menu for creating a new character.
7999 * @param {object} [options] Options for the switch
8000 * @param {boolean} [options.switchMenu=true] Whether to switch the menu
8001 */
8002function select_rm_create({ switchMenu = true } = {}) {
8003 switchMenu && setMenuType('create');
79808004
7981 //console.log('select_rm_Create() -- selected button: '+selected_button);8005 //console.log('select_rm_Create() -- selected button: '+selected_button);
7982 if (selected_button == 'create') {8006 if (selected_button == 'create') {
@@ -7986,7 +8010,7 @@ function select_rm_create() {
7986 }8010 }
7987 }8011 }
79888012
7989 selectRightMenuWithAnimation('rm_ch_create_block');8013 switchMenu && selectRightMenuWithAnimation('rm_ch_create_block');
79908014
7991 $('#set_chat_scenario').hide();8015 $('#set_chat_scenario').hide();
7992 $('#delete_button_div').css('display', 'none');8016 $('#delete_button_div').css('display', 'none');
@@ -9209,6 +9233,17 @@ function swipe_right(_event, { source, repeated } = {}) {
9209 }9233 }
9210}9234}
92119235
9236/**
9237 * @typedef {object} ConnectAPIMap
9238 * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
9239 * @property {string?} [button] - CSS selector for the API button
9240 * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
9241 * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
9242 */
9243
9244/**
9245 * @type {Record<string, ConnectAPIMap>}
9246 */
9212export const CONNECT_API_MAP = {9247export const CONNECT_API_MAP = {
9213 // Default APIs not contined inside text gen / chat gen9248 // Default APIs not contined inside text gen / chat gen
9214 'kobold': {9249 'kobold': {
public/scripts/custom-request.js+231 -33
@@ -1,20 +1,20 @@
1import { getPresetManager } from './preset-manager.js';1import { getPresetManager } from './preset-manager.js';
2import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';2import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
3import { getTextGenServer } from './textgen-settings.js';3import { getTextGenServer } from './textgen-settings.js';
4import { extractReasoningFromData } from './reasoning.js';
5import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';
46
5// #region Type Definitions7// #region Type Definitions
6/**8/**
7 * @typedef {Object} TextCompletionRequestBase9 * @typedef {Object} TextCompletionRequestBase
8 * @property {string} prompt - The text prompt for completion
9 * @property {number} max_tokens - Maximum number of tokens to generate10 * @property {number} max_tokens - Maximum number of tokens to generate
10 * @property {string} [model] - Optional model name11 * @property {string} [model] - Optional model name
11 * @property {string} api_type - Type of API to use12 * @property {string} api_type - Type of API to use
12 * @property {string} [api_server] - Optional API server URL13 * @property {string} [api_server] - Optional API server URL
13 * @property {number} [temperature] - Optional temperature parameter14 * @property {number} [temperature] - Optional temperature parameter
15 * @property {number} [min_p] - Optional min_p parameter
14 */16 */
1517
16/** @typedef {Record<string, any> & TextCompletionRequestBase} TextCompletionRequest */
17
18/**18/**
19 * @typedef {Object} TextCompletionPayloadBase19 * @typedef {Object} TextCompletionPayloadBase
20 * @property {string} prompt - The text prompt for completion20 * @property {string} prompt - The text prompt for completion
@@ -44,6 +44,13 @@ import { getTextGenServer } from './textgen-settings.js';
44 */44 */
4545
46/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */46/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
47
48/**
49 * @typedef {Object} ExtractedData
50 * @property {string} content - Extracted content.
51 * @property {string} reasoning - Extracted reasoning.
52 */
53
47// #endregion54// #endregion
4855
49/**56/**
@@ -53,11 +60,11 @@ export class TextCompletionService {
53 static TYPE = 'textgenerationwebui';60 static TYPE = 'textgenerationwebui';
5461
55 /**62 /**
56 * @param {TextCompletionRequest} custom63 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom
57 * @returns {TextCompletionPayload}64 * @returns {TextCompletionPayload}
58 */65 */
59 static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, ...props }) {66 static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
60 return {67 const payload = {
61 ...props,68 ...props,
62 prompt,69 prompt,
63 max_tokens,70 max_tokens,
@@ -66,15 +73,25 @@ export class TextCompletionService {
66 api_type,73 api_type,
67 api_server: api_server ?? getTextGenServer(api_type),74 api_server: api_server ?? getTextGenServer(api_type),
68 temperature,75 temperature,
76 min_p,
69 stream: false,77 stream: false,
70 };78 };
79
80 // Remove undefined values to avoid API errors
81 Object.keys(payload).forEach(key => {
82 if (payload[key] === undefined) {
83 delete payload[key];
84 }
85 });
86
87 return payload;
71 }88 }
7289
73 /**90 /**
74 * Sends a text completion request to the specified server91 * Sends a text completion request to the specified server
75 * @param {TextCompletionPayload} data Request data92 * @param {TextCompletionPayload} data Request data
76 * @param {boolean?} extractData Extract message from the response. Default true93 * @param {boolean?} extractData Extract message from the response. Default true
77 * @returns {Promise<string | any>} Extracted data or the raw response94 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
78 * @throws {Error}95 * @throws {Error}
79 */96 */
80 static async sendRequest(data, extractData = true) {97 static async sendRequest(data, extractData = true) {
@@ -91,31 +108,150 @@ export class TextCompletionService {
91 throw json;108 throw json;
92 }109 }
93110
94 return extractData ? extractMessageFromData(json, this.TYPE) : json;111 if (!extractData) {
112 return json;
113 }
114
115 return {
116 content: extractMessageFromData(json, this.TYPE),
117 reasoning: extractReasoningFromData(json, {
118 mainApi: this.TYPE,
119 textGenType: data.api_type,
120 ignoreShowThoughts: true,
121 }),
122 };
95 }123 }
96124
97 /**125 /**
98 * @param {string} presetName126 * Process and send a text completion request with optional preset & instruct
99 * @param {TextCompletionRequest} custom127 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom
100 * @param {boolean?} extractData Extract message from the response. Default true128 * @param {Object} options - Configuration options
101 * @returns {Promise<string | any>} Extracted data or the raw response129 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
130 * @param {string?} [options.instructName] - Name of instruct preset for message formatting
131 * @param {boolean} extractData - Whether to extract structured data from response
132 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
102 * @throws {Error}133 * @throws {Error}
103 */134 */
104 static async sendRequestWithPreset(presetName, custom, extractData = true) {135 static async processRequest(
136 custom,
137 options = {},
138 extractData = true,
139 ) {
140 const { presetName, instructName } = options;
141 let requestData = { ...custom };
142 const prompt = custom.prompt;
143
144 // Apply generation preset if specified
145 if (presetName) {
105 const presetManager = getPresetManager(this.TYPE);146 const presetManager = getPresetManager(this.TYPE);
106 if (!presetManager) {147 if (presetManager) {
107 throw new Error('Preset manager not found');148 const preset = presetManager.getCompletionPresetByName(presetName);
149 if (preset) {
150 // Convert preset to payload and merge with custom parameters
151 const presetPayload = this.presetToGeneratePayload(preset, {});
152 requestData = { ...presetPayload, ...requestData };
153 } else {
154 console.warn(`Preset "${presetName}" not found, continuing with default settings`);
155 }
156 } else {
157 console.warn('Preset manager not found, continuing with default settings');
158 }
108 }159 }
109160
110 const preset = presetManager.getCompletionPresetByName(presetName);161 // Handle instruct formatting if requested
111 if (!preset) {162 if (Array.isArray(prompt) && instructName) {
112 throw new Error('Preset not found');163 const instructPresetManager = getPresetManager('instruct');
164 let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
165 if (instructPreset) {
166 // Clone the preset to avoid modifying the original
167 instructPreset = structuredClone(instructPreset);
168 instructPreset.macro = false;
169 instructPreset.names_behavior = names_behavior_types.NONE;
170
171 // Format messages using instruct formatting
172 const formattedMessages = [];
173 for (const message of prompt) {
174 let messageContent = message.content;
175 if (!message.ignoreInstruct) {
176 messageContent = formatInstructModeChat(
177 message.role,
178 message.content,
179 message.role === 'user',
180 false,
181 undefined,
182 undefined,
183 undefined,
184 undefined,
185 instructPreset,
186 );
187
188 // Add prompt formatting for the last message
189 if (message === prompt[prompt.length - 1]) {
190 messageContent += formatInstructModePrompt(
191 undefined,
192 false,
193 undefined,
194 undefined,
195 undefined,
196 false,
197 false,
198 instructPreset,
199 );
200 }
201 }
202 formattedMessages.push(messageContent);
203 }
204 requestData.prompt = formattedMessages.join('');
205 if (instructPreset.output_suffix) {
206 requestData.stop = [instructPreset.output_suffix];
207 requestData.stopping_strings = [instructPreset.output_suffix];
208 }
209 } else {
210 console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
211 requestData.prompt = prompt.map(x => x.content).join('\n\n');
212 }
213 } else if (typeof prompt === 'string') {
214 requestData.prompt = prompt;
215 } else {
216 requestData.prompt = prompt.map(x => x.content).join('\n\n');
113 }217 }
114218
115 const data = this.createRequestData({ ...preset, ...custom });219 // @ts-ignore
220 const data = this.createRequestData(requestData);
116221
117 return await this.sendRequest(data, extractData);222 return await this.sendRequest(data, extractData);
118 }223 }
224
225 /**
226 * Converts a preset to a valid text completion payload.
227 * Only supports temperature.
228 * @param {Object} preset - The preset configuration
229 * @param {Object} customPreset - Additional parameters to override preset values
230 * @returns {Object} - Formatted payload for text completion API
231 */
232 static presetToGeneratePayload(preset, customPreset = {}) {
233 if (!preset || typeof preset !== 'object') {
234 throw new Error('Invalid preset: must be an object');
235 }
236
237 // Merge preset with custom parameters
238 const settings = { ...preset, ...customPreset };
239
240 // Initialize base payload with common parameters
241 let payload = {
242 'temperature': settings.temp ? Number(settings.temp) : undefined,
243 'min_p': settings.min_p ? Number(settings.min_p) : undefined,
244 };
245
246 // Remove undefined values to avoid API errors
247 Object.keys(payload).forEach(key => {
248 if (payload[key] === undefined) {
249 delete payload[key];
250 }
251 });
252
253 return payload;
254 }
119}255}
120256
121/**257/**
@@ -129,7 +265,7 @@ export class ChatCompletionService {
129 * @returns {ChatCompletionPayload}265 * @returns {ChatCompletionPayload}
130 */266 */
131 static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) {267 static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) {
132 return {268 const payload = {
133 ...props,269 ...props,
134 messages,270 messages,
135 model,271 model,
@@ -138,13 +274,22 @@ export class ChatCompletionService {
138 temperature,274 temperature,
139 stream: false,275 stream: false,
140 };276 };
277
278 // Remove undefined values to avoid API errors
279 Object.keys(payload).forEach(key => {
280 if (payload[key] === undefined) {
281 delete payload[key];
282 }
283 });
284
285 return payload;
141 }286 }
142287
143 /**288 /**
144 * Sends a chat completion request289 * Sends a chat completion request
145 * @param {ChatCompletionPayload} data Request data290 * @param {ChatCompletionPayload} data Request data
146 * @param {boolean?} extractData Extract message from the response. Default true291 * @param {boolean?} extractData Extract message from the response. Default true
147 * @returns {Promise<string | any>} Extracted data or the raw response292 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
148 * @throws {Error}293 * @throws {Error}
149 */294 */
150 static async sendRequest(data, extractData = true) {295 static async sendRequest(data, extractData = true) {
@@ -161,29 +306,82 @@ export class ChatCompletionService {
161 throw json;306 throw json;
162 }307 }
163308
164 return extractData ? extractMessageFromData(json, this.TYPE) : json;309 if (!extractData) {
310 return json;
311 }
312
313 return {
314 content: extractMessageFromData(json, this.TYPE),
315 reasoning: extractReasoningFromData(json, {
316 mainApi: this.TYPE,
317 textGenType: data.chat_completion_source,
318 ignoreShowThoughts: true,
319 }),
320 };
165 }321 }
166322
167 /**323 /**
168 * @param {string} presetName324 * Process and send a chat completion request with optional preset
169 * @param {ChatCompletionPayload} custom325 * @param {ChatCompletionPayload} custom
170 * @param {boolean} extractData Extract message from the response. Default true326 * @param {Object} options - Configuration options
171 * @returns {Promise<string | any>} Extracted data or the raw response327 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
328 * @param {boolean} extractData - Whether to extract structured data from response
329 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
172 * @throws {Error}330 * @throws {Error}
173 */331 */
174 static async sendRequestWithPreset(presetName, custom, extractData = true) {332 static async processRequest(custom, options, extractData = true) {
175 const presetManager = getPresetManager(this.TYPE);333 const { presetName } = options;
176 if (!presetManager) {334 let requestData = { ...custom };
177 throw new Error('Preset manager not found');
178 }
179335
336 // Apply generation preset if specified
337 if (presetName) {
338 const presetManager = getPresetManager(this.TYPE);
339 if (presetManager) {
180 const preset = presetManager.getCompletionPresetByName(presetName);340 const preset = presetManager.getCompletionPresetByName(presetName);
181 if (!preset) {341 if (preset) {
182 throw new Error('Preset not found');342 // Convert preset to payload and merge with custom parameters
343 const presetPayload = this.presetToGeneratePayload(preset, {});
344 requestData = { ...presetPayload, ...requestData };
345 } else {
346 console.warn(`Preset "${presetName}" not found, continuing with default settings`);
347 }
348 } else {
349 console.warn('Preset manager not found, continuing with default settings');
350 }
183 }351 }
184352
185 const data = this.createRequestData({ ...preset, ...custom });353 const data = this.createRequestData(requestData);
186354
187 return await this.sendRequest(data, extractData);355 return await this.sendRequest(data, extractData);
188 }356 }
357
358 /**
359 * Converts a preset to a valid chat completion payload
360 * Only supports temperature.
361 * @param {Object} preset - The preset configuration
362 * @param {Object} customParams - Additional parameters to override preset values
363 * @returns {Object} - Formatted payload for chat completion API
364 */
365 static presetToGeneratePayload(preset, customParams = {}) {
366 if (!preset || typeof preset !== 'object') {
367 throw new Error('Invalid preset: must be an object');
368 }
369
370 // Merge preset with custom parameters
371 const settings = { ...preset, ...customParams };
372
373 // Initialize base payload with common parameters
374 const payload = {
375 temperature: settings.temperature ? Number(settings.temperature) : undefined,
376 };
377
378 // Remove undefined values to avoid API errors
379 Object.keys(payload).forEach(key => {
380 if (payload[key] === undefined) {
381 delete payload[key];
382 }
383 });
384
385 return payload;
386 }
189}387}
public/scripts/extensions/connection-manager/index.js+28 -6
@@ -1,4 +1,4 @@
1import { Fuse } from '../../../lib.js';1import { DOMPurify, Fuse } from '../../../lib.js';
22
3import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';3import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
4import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';4import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
@@ -267,9 +267,14 @@ async function createConnectionProfile(forceName = null) {
267 });267 });
268 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);268 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
269 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);269 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
270 const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });270 let name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
271271 // If it's cancelled, it will be false
272 if (!name) {
273 return null;
274 }
275 name = DOMPurify.sanitize(String(name));
272 if (!name) {276 if (!name) {
277 toastr.error('Name cannot be empty.');
273 return null;278 return null;
274 }279 }
275280
@@ -303,7 +308,8 @@ async function deleteConnectionProfile() {
303 return;308 return;
304 }309 }
305310
306 const name = extension_settings.connectionManager.profiles[index].name;311 const profile = extension_settings.connectionManager.profiles[index];
312 const name = profile.name;
307 const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);313 const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);
308314
309 if (!confirm) {315 if (!confirm) {
@@ -313,6 +319,8 @@ async function deleteConnectionProfile() {
313 extension_settings.connectionManager.profiles.splice(index, 1);319 extension_settings.connectionManager.profiles.splice(index, 1);
314 extension_settings.connectionManager.selectedProfile = null;320 extension_settings.connectionManager.selectedProfile = null;
315 saveSettingsDebounced();321 saveSettingsDebounced();
322
323 await eventSource.emit(event_types.CONNECTION_PROFILE_DELETED, profile);
316}324}
317325
318/**326/**
@@ -512,6 +520,7 @@ async function renderDetailsContent(detailsContent) {
512 saveSettingsDebounced();520 saveSettingsDebounced();
513 renderConnectionProfiles(profiles);521 renderConnectionProfiles(profiles);
514 await renderDetailsContent(detailsContent);522 await renderDetailsContent(detailsContent);
523 await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
515 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);524 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
516 });525 });
517526
@@ -523,9 +532,11 @@ async function renderDetailsContent(detailsContent) {
523 console.log('No profile selected');532 console.log('No profile selected');
524 return;533 return;
525 }534 }
535 const oldProfile = structuredClone(profile);
526 await updateConnectionProfile(profile);536 await updateConnectionProfile(profile);
527 await renderDetailsContent(detailsContent);537 await renderDetailsContent(detailsContent);
528 saveSettingsDebounced();538 saveSettingsDebounced();
539 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
529 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);540 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
530 toastr.success('Connection profile updated', '', { timeOut: 1500 });541 toastr.success('Connection profile updated', '', { timeOut: 1500 });
531 });542 });
@@ -559,7 +570,7 @@ async function renderDetailsContent(detailsContent) {
559 return acc;570 return acc;
560 }, {});571 }, {});
561 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));572 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
562 const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {573 let newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
563 rows: 2,574 rows: 2,
564 customButtons: [{575 customButtons: [{
565 text: t`Save and Update`,576 text: t`Save and Update`,
@@ -571,7 +582,13 @@ async function renderDetailsContent(detailsContent) {
571 }],582 }],
572 });583 });
573584
585 // If it's cancelled, it will be false
586 if (!newName) {
587 return;
588 }
589 newName = DOMPurify.sanitize(String(newName));
574 if (!newName) {590 if (!newName) {
591 toastr.error('Name cannot be empty.');
575 return;592 return;
576 }593 }
577594
@@ -584,6 +601,7 @@ async function renderDetailsContent(detailsContent) {
584 return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];601 return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
585 }).get();602 }).get();
586603
604 const oldProfile = structuredClone(profile);
587 if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {605 if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
588 profile.exclude = newExcludeList;606 profile.exclude = newExcludeList;
589 for (const command of newExcludeList) {607 for (const command of newExcludeList) {
@@ -598,10 +616,11 @@ async function renderDetailsContent(detailsContent) {
598616
599 if (profile.name !== newName) {617 if (profile.name !== newName) {
600 toastr.success('Connection profile renamed.');618 toastr.success('Connection profile renamed.');
601 profile.name = String(newName);619 profile.name = newName;
602 }620 }
603621
604 saveSettingsDebounced();622 saveSettingsDebounced();
623 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
605 renderConnectionProfiles(profiles);624 renderConnectionProfiles(profiles);
606 await renderDetailsContent(detailsContent);625 await renderDetailsContent(detailsContent);
607 });626 });
@@ -704,6 +723,7 @@ async function renderDetailsContent(detailsContent) {
704 saveSettingsDebounced();723 saveSettingsDebounced();
705 renderConnectionProfiles(profiles);724 renderConnectionProfiles(profiles);
706 await renderDetailsContent(detailsContent);725 await renderDetailsContent(detailsContent);
726 await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
707 return profile.name;727 return profile.name;
708 },728 },
709 }));729 }));
@@ -718,9 +738,11 @@ async function renderDetailsContent(detailsContent) {
718 toastr.warning('No profile selected.');738 toastr.warning('No profile selected.');
719 return '';739 return '';
720 }740 }
741 const oldProfile = structuredClone(profile);
721 await updateConnectionProfile(profile);742 await updateConnectionProfile(profile);
722 await renderDetailsContent(detailsContent);743 await renderDetailsContent(detailsContent);
723 saveSettingsDebounced();744 saveSettingsDebounced();
745 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
724 return profile.name;746 return profile.name;
725 },747 },
726 }));748 }));
public/scripts/extensions/shared.js+308 -1
@@ -1,5 +1,6 @@
1import { getRequestHeaders } from '../../script.js';1import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
3import { t } from '../i18n.js';
3import { oai_settings } from '../openai.js';4import { oai_settings } from '../openai.js';
4import { SECRET_KEYS, secret_state } from '../secrets.js';5import { SECRET_KEYS, secret_state } from '../secrets.js';
5import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';6import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@@ -273,3 +274,309 @@ export async function getWebLlmContextSize() {
273 const model = await engine.getCurrentModelInfo();274 const model = await engine.getCurrentModelInfo();
274 return model?.context_size;275 return model?.context_size;
275}276}
277
278/**
279 * It uses the profiles to send a generate request to the API. Doesn't support streaming.
280 */
281export class ConnectionManagerRequestService {
282 static defaultSendRequestParams = {
283 extractData: true,
284 includePreset: true,
285 includeInstruct: true,
286 };
287
288 static getAllowedTypes() {
289 return {
290 openai: t`Chat Completion`,
291 textgenerationwebui: t`Text Completion`,
292 };
293 }
294
295 /**
296 * @param {string} profileId
297 * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
298 * @param {number} maxTokens
299 * @param {{extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true
300 * @returns {Promise<import('../custom-request.js').ExtractedData | any>} Extracted data or the raw response
301 */
302 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
303 const { extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };
304
305 const context = SillyTavern.getContext();
306 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
307 throw new Error('Connection Manager is not available');
308 }
309
310 const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
311 const selectedApiMap = this.validateProfile(profile);
312
313 try {
314 switch (selectedApiMap.selected) {
315 case 'openai': {
316 if (!selectedApiMap.source) {
317 throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);
318 }
319
320 const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
321 return await context.ChatCompletionService.processRequest({
322 messages,
323 max_tokens: maxTokens,
324 model: profile.model,
325 chat_completion_source: selectedApiMap.source,
326 }, {
327 presetName: includePreset ? profile.preset : undefined,
328 }, extractData);
329 }
330 case 'textgenerationwebui': {
331 if (!selectedApiMap.type) {
332 throw new Error(`API type ${selectedApiMap.selected} does not support text completions`);
333 }
334
335 return await context.TextCompletionService.processRequest({
336 prompt,
337 max_tokens: maxTokens,
338 model: profile.model,
339 api_type: selectedApiMap.type,
340 api_server: profile['api-url'],
341 }, {
342 instructName: includeInstruct ? profile.instruct : undefined,
343 presetName: includePreset ? profile.preset : undefined,
344 }, extractData);
345 }
346 default: {
347 throw new Error(`Unknown API type ${selectedApiMap.selected}`);
348 }
349 }
350 } catch (error) {
351 throw new Error('API request failed', { cause: error });
352 }
353 }
354
355 /**
356 * Respects allowed types.
357 * @returns {import('./connection-manager/index.js').ConnectionProfile[]}
358 */
359 static getSupportedProfiles() {
360 const context = SillyTavern.getContext();
361 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
362 throw new Error('Connection Manager is not available');
363 }
364
365 const profiles = context.extensionSettings.connectionManager.profiles;
366 return profiles.filter((p) => this.isProfileSupported(p));
367 }
368
369 /**
370 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
371 * @returns {boolean}
372 */
373 static isProfileSupported(profile) {
374 if (!profile) {
375 return false;
376 }
377
378 const apiMap = CONNECT_API_MAP[profile.api];
379 if (!Object.hasOwn(this.getAllowedTypes(), apiMap.selected)) {
380 return false;
381 }
382
383 // Some providers not need model, like koboldcpp. But I don't want to check by provider.
384 switch (apiMap.selected) {
385 case 'openai':
386 return !!apiMap.source;
387 case 'textgenerationwebui':
388 return !!apiMap.type;
389 }
390
391 return false;
392 }
393
394 /**
395 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
396 * @return {import('../../script.js').ConnectAPIMap}
397 * @throws {Error}
398 */
399 static validateProfile(profile) {
400 if (!profile) {
401 throw new Error('Could not find profile.');
402 }
403 if (!profile.api) {
404 throw new Error('Select a connection profile that has an API');
405 }
406
407 const context = SillyTavern.getContext();
408 const selectedApiMap = context.CONNECT_API_MAP[profile.api];
409 if (!selectedApiMap) {
410 throw new Error(`Unknown API type ${profile.api}`);
411 }
412 if (!Object.hasOwn(this.getAllowedTypes(), selectedApiMap.selected)) {
413 throw new Error(`API type ${selectedApiMap.selected} is not supported. Supported types: ${Object.values(this.getAllowedTypes()).join(', ')}`);
414 }
415
416 return selectedApiMap;
417 }
418
419 /**
420 * Create profiles dropdown and updates select element accordingly. Use onChange, onCreate, unUpdate, onDelete callbacks for custom behaviour. e.g updating extension settings.
421 * @param {string} selector
422 * @param {string} initialSelectedProfileId
423 * @param {(profile?: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onChange - 3 cases. 1- When user selects new profile. 2- When user deletes selected profile. 3- When user updates selected profile.
424 * @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onCreate
425 * @param {(oldProfile: import('./connection-manager/index.js').ConnectionProfile, newProfile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} unUpdate
426 * @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onDelete
427 */
428 static handleDropdown(
429 selector,
430 initialSelectedProfileId,
431 onChange = () => { },
432 onCreate = () => { },
433 unUpdate = () => { },
434 onDelete = () => { },
435 ) {
436 const context = SillyTavern.getContext();
437 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
438 throw new Error('Connection Manager is not available');
439 }
440
441 /**
442 * @type {JQuery<HTMLSelectElement>}
443 */
444 const dropdown = $(selector);
445
446 if (!dropdown || !dropdown.length) {
447 throw new Error(`Could not find dropdown with selector ${selector}`);
448 }
449
450 dropdown.empty();
451
452 // Create default option using document.createElement
453 const defaultOption = document.createElement('option');
454 defaultOption.value = '';
455 defaultOption.textContent = 'Select a Connection Profile';
456 defaultOption.dataset.i18n = 'Select a Connection Profile';
457 dropdown.append(defaultOption);
458
459 const profiles = context.extensionSettings.connectionManager.profiles;
460
461 // Create optgroups using document.createElement
462 const groups = {};
463 for (const [apiType, groupLabel] of Object.entries(this.getAllowedTypes())) {
464 const optgroup = document.createElement('optgroup');
465 optgroup.label = groupLabel;
466 groups[apiType] = optgroup;
467 }
468
469 const sortedProfilesByGroup = {};
470 for (const apiType of Object.keys(this.getAllowedTypes())) {
471 sortedProfilesByGroup[apiType] = [];
472 }
473
474 for (const profile of profiles) {
475 if (this.isProfileSupported(profile)) {
476 const apiMap = CONNECT_API_MAP[profile.api];
477 if (sortedProfilesByGroup[apiMap.selected]) {
478 sortedProfilesByGroup[apiMap.selected].push(profile);
479 }
480 }
481 }
482
483 // Sort each group alphabetically and add to dropdown
484 for (const [apiType, groupProfiles] of Object.entries(sortedProfilesByGroup)) {
485 if (groupProfiles.length === 0) continue;
486
487 groupProfiles.sort((a, b) => a.name.localeCompare(b.name));
488
489 const group = groups[apiType];
490 for (const profile of groupProfiles) {
491 const option = document.createElement('option');
492 option.value = profile.id;
493 option.textContent = profile.name;
494 group.appendChild(option);
495 }
496 }
497
498 for (const group of Object.values(groups)) {
499 if (group.children.length > 0) {
500 dropdown.append(group);
501 }
502 }
503
504 const selectedProfile = profiles.find((p) => p.id === initialSelectedProfileId);
505 if (selectedProfile) {
506 dropdown.val(selectedProfile.id);
507 }
508
509 context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_CREATED, async (profile) => {
510 const isSupported = this.isProfileSupported(profile);
511 if (!isSupported) {
512 return;
513 }
514
515 const group = groups[CONNECT_API_MAP[profile.api].selected];
516 const option = document.createElement('option');
517 option.value = profile.id;
518 option.textContent = profile.name;
519 group.appendChild(option);
520
521 await onCreate(profile);
522 });
523
524 context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_UPDATED, async (oldProfile, newProfile) => {
525 const currentSelected = dropdown.val();
526 const isSelectedProfile = currentSelected === oldProfile.id;
527 await unUpdate(oldProfile, newProfile);
528
529 if (!this.isProfileSupported(newProfile)) {
530 if (isSelectedProfile) {
531 dropdown.val('');
532 dropdown.trigger('change');
533 }
534 return;
535 }
536
537 const group = groups[CONNECT_API_MAP[newProfile.api].selected];
538 const oldOption = group.querySelector(`option[value="${oldProfile.id}"]`);
539 if (oldOption) {
540 oldOption.remove();
541 }
542
543 const option = document.createElement('option');
544 option.value = newProfile.id;
545 option.textContent = newProfile.name;
546 group.appendChild(option);
547
548 if (isSelectedProfile) {
549 // Ackchyually, we don't need to reselect but what if id changes? It is not possible for now I couldn't stop myself.
550 dropdown.val(newProfile.id);
551 dropdown.trigger('change');
552 }
553 });
554
555 context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_DELETED, async (profile) => {
556 const currentSelected = dropdown.val();
557 const isSelectedProfile = currentSelected === profile.id;
558 if (!this.isProfileSupported(profile)) {
559 return;
560 }
561
562 const group = groups[CONNECT_API_MAP[profile.api].selected];
563 const optionToRemove = group.querySelector(`option[value="${profile.id}"]`);
564 if (optionToRemove) {
565 optionToRemove.remove();
566 }
567
568 if (isSelectedProfile) {
569 dropdown.val('');
570 dropdown.trigger('change');
571 }
572
573 await onDelete(profile);
574 });
575
576 dropdown.on('change', async () => {
577 const profileId = dropdown.val();
578 const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
579 await onChange(profile);
580 });
581 }
582}
public/scripts/instruct-mode.js+35 -31
@@ -320,59 +320,61 @@ export const force_output_sequence = {
320 * @param {string} name1 User name.320 * @param {string} name1 User name.
321 * @param {string} name2 Character name.321 * @param {string} name2 Character name.
322 * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).322 * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
323 * @param {InstructSettings} customInstruct Custom instruct mode settings.
323 * @returns {string} Formatted instruct mode chat message.324 * @returns {string} Formatted instruct mode chat message.
324 */325 */
325export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence) {326export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence, customInstruct = null) {
326 let includeNames = isNarrator ? false : power_user.instruct.names_behavior === names_behavior_types.ALWAYS;327 const instruct = structuredClone(customInstruct ?? power_user.instruct);
328 let includeNames = isNarrator ? false : instruct.names_behavior === names_behavior_types.ALWAYS;
327329
328 if (!isNarrator && power_user.instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {330 if (!isNarrator && instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {
329 includeNames = true;331 includeNames = true;
330 }332 }
331333
332 function getPrefix() {334 function getPrefix() {
333 if (isNarrator) {335 if (isNarrator) {
334 return power_user.instruct.system_same_as_user ? power_user.instruct.input_sequence : power_user.instruct.system_sequence;336 return instruct.system_same_as_user ? instruct.input_sequence : instruct.system_sequence;
335 }337 }
336338
337 if (isUser) {339 if (isUser) {
338 if (forceOutputSequence === force_output_sequence.FIRST) {340 if (forceOutputSequence === force_output_sequence.FIRST) {
339 return power_user.instruct.first_input_sequence || power_user.instruct.input_sequence;341 return instruct.first_input_sequence || instruct.input_sequence;
340 }342 }
341343
342 if (forceOutputSequence === force_output_sequence.LAST) {344 if (forceOutputSequence === force_output_sequence.LAST) {
343 return power_user.instruct.last_input_sequence || power_user.instruct.input_sequence;345 return instruct.last_input_sequence || instruct.input_sequence;
344 }346 }
345347
346 return power_user.instruct.input_sequence;348 return instruct.input_sequence;
347 }349 }
348350
349 if (forceOutputSequence === force_output_sequence.FIRST) {351 if (forceOutputSequence === force_output_sequence.FIRST) {
350 return power_user.instruct.first_output_sequence || power_user.instruct.output_sequence;352 return instruct.first_output_sequence || instruct.output_sequence;
351 }353 }
352354
353 if (forceOutputSequence === force_output_sequence.LAST) {355 if (forceOutputSequence === force_output_sequence.LAST) {
354 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;356 return instruct.last_output_sequence || instruct.output_sequence;
355 }357 }
356358
357 return power_user.instruct.output_sequence;359 return instruct.output_sequence;
358 }360 }
359361
360 function getSuffix() {362 function getSuffix() {
361 if (isNarrator) {363 if (isNarrator) {
362 return power_user.instruct.system_same_as_user ? power_user.instruct.input_suffix : power_user.instruct.system_suffix;364 return instruct.system_same_as_user ? instruct.input_suffix : instruct.system_suffix;
363 }365 }
364366
365 if (isUser) {367 if (isUser) {
366 return power_user.instruct.input_suffix;368 return instruct.input_suffix;
367 }369 }
368370
369 return power_user.instruct.output_suffix;371 return instruct.output_suffix;
370 }372 }
371373
372 let prefix = getPrefix() || '';374 let prefix = getPrefix() || '';
373 let suffix = getSuffix() || '';375 let suffix = getSuffix() || '';
374376
375 if (power_user.instruct.macro) {377 if (instruct.macro) {
376 prefix = substituteParams(prefix, name1, name2);378 prefix = substituteParams(prefix, name1, name2);
377 prefix = prefix.replace(/{{name}}/gi, name || 'System');379 prefix = prefix.replace(/{{name}}/gi, name || 'System');
378380
@@ -380,11 +382,11 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
380 suffix = suffix.replace(/{{name}}/gi, name || 'System');382 suffix = suffix.replace(/{{name}}/gi, name || 'System');
381 }383 }
382384
383 if (!suffix && power_user.instruct.wrap) {385 if (!suffix && instruct.wrap) {
384 suffix = '\n';386 suffix = '\n';
385 }387 }
386388
387 const separator = power_user.instruct.wrap ? '\n' : '';389 const separator = instruct.wrap ? '\n' : '';
388390
389 // Don't include the name if it's empty391 // Don't include the name if it's empty
390 const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];392 const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
@@ -504,30 +506,32 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
504 * @param {string} name2 Character name.506 * @param {string} name2 Character name.
505 * @param {boolean} isQuiet Is quiet mode generation.507 * @param {boolean} isQuiet Is quiet mode generation.
506 * @param {boolean} isQuietToLoud Is quiet to loud generation.508 * @param {boolean} isQuietToLoud Is quiet to loud generation.
509 * @param {InstructSettings} customInstruct Custom instruct settings.
507 * @returns {string} Formatted instruct mode last prompt line.510 * @returns {string} Formatted instruct mode last prompt line.
508 */511 */
509export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud) {512export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud, customInstruct = null) {
510 const includeNames = name && (power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud);513 const instruct = structuredClone(customInstruct ?? power_user.instruct);
514 const includeNames = name && (instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud);
511515
512 function getSequence() {516 function getSequence() {
513 // User impersonation prompt517 // User impersonation prompt
514 if (isImpersonate) {518 if (isImpersonate) {
515 return power_user.instruct.input_sequence;519 return instruct.input_sequence;
516 }520 }
517521
518 // Neutral / system / quiet prompt522 // Neutral / system / quiet prompt
519 // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise523 // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise
520 if (isQuiet && !isQuietToLoud) {524 if (isQuiet && !isQuietToLoud) {
521 return power_user.instruct.last_system_sequence || power_user.instruct.output_sequence;525 return instruct.last_system_sequence || instruct.output_sequence;
522 }526 }
523527
524 // Quiet in-character prompt528 // Quiet in-character prompt
525 if (isQuiet && isQuietToLoud) {529 if (isQuiet && isQuietToLoud) {
526 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;530 return instruct.last_output_sequence || instruct.output_sequence;
527 }531 }
528532
529 // Default AI response533 // Default AI response
530 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;534 return instruct.last_output_sequence || instruct.output_sequence;
531 }535 }
532536
533 let sequence = getSequence() || '';537 let sequence = getSequence() || '';
@@ -536,21 +540,21 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
536 // A hack for Mistral's formatting that has a normal output sequence ending with a space540 // A hack for Mistral's formatting that has a normal output sequence ending with a space
537 if (541 if (
538 includeNames &&542 includeNames &&
539 power_user.instruct.last_output_sequence &&543 instruct.last_output_sequence &&
540 power_user.instruct.output_sequence &&544 instruct.output_sequence &&
541 sequence === power_user.instruct.last_output_sequence &&545 sequence === instruct.last_output_sequence &&
542 /\s$/.test(power_user.instruct.output_sequence) &&546 /\s$/.test(instruct.output_sequence) &&
543 !/\s$/.test(power_user.instruct.last_output_sequence)547 !/\s$/.test(instruct.last_output_sequence)
544 ) {548 ) {
545 nameFiller = power_user.instruct.output_sequence.slice(-1);549 nameFiller = instruct.output_sequence.slice(-1);
546 }550 }
547551
548 if (power_user.instruct.macro) {552 if (instruct.macro) {
549 sequence = substituteParams(sequence, name1, name2);553 sequence = substituteParams(sequence, name1, name2);
550 sequence = sequence.replace(/{{name}}/gi, name || 'System');554 sequence = sequence.replace(/{{name}}/gi, name || 'System');
551 }555 }
552556
553 const separator = power_user.instruct.wrap ? '\n' : '';557 const separator = instruct.wrap ? '\n' : '';
554 let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence);558 let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence);
555559
556 // Quiet prompt already has a newline at the end560 // Quiet prompt already has a newline at the end
@@ -562,7 +566,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
562 text += (includeNames ? promptBias : (separator + promptBias.trimStart()));566 text += (includeNames ? promptBias : (separator + promptBias.trimStart()));
563 }567 }
564568
565 return (power_user.instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);569 return (instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);
566}570}
567571
568/**572/**
public/scripts/openai.js+1 -1
@@ -2048,7 +2048,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2048 delete generate_data.stop;2048 delete generate_data.stop;
2049 delete generate_data.logprobs;2049 delete generate_data.logprobs;
2050 }2050 }
2051 if (isOAI && oai_settings.openai_model.includes('gpt-4.5-preview') || isOpenRouter && oai_settings.openrouter_model.includes('gpt-4.5-preview')) {2051 if (isOAI && oai_settings.openai_model.includes('gpt-4.5') || isOpenRouter && oai_settings.openrouter_model.includes('gpt-4.5')) {
2052 delete generate_data.logprobs;2052 delete generate_data.logprobs;
2053 }2053 }
20542054
public/scripts/power-user.js+2 -0
@@ -218,7 +218,9 @@ let power_user = {
218 system_sequence: '',218 system_sequence: '',
219 system_suffix: '',219 system_suffix: '',
220 last_system_sequence: '',220 last_system_sequence: '',
221 first_input_sequence: '',
221 first_output_sequence: '',222 first_output_sequence: '',
223 last_input_sequence: '',
222 last_output_sequence: '',224 last_output_sequence: '',
223 system_sequence_prefix: '',225 system_sequence_prefix: '',
224 system_sequence_suffix: '',226 system_sequence_suffix: '',
public/scripts/reasoning.js+10 -5
@@ -57,19 +57,24 @@ function toggleReasoningAutoExpand() {
57 * @param {object} data Response data57 * @param {object} data Response data
58 * @returns {string} Extracted reasoning58 * @returns {string} Extracted reasoning
59 */59 */
60export function extractReasoningFromData(data) {60export function extractReasoningFromData(data, {
61 switch (main_api) {61 mainApi = null,
62 ignoreShowThoughts = false,
63 textGenType = null,
64 chatCompletionSource = null
65} = {}) {
66 switch (mainApi ?? main_api) {
62 case 'textgenerationwebui':67 case 'textgenerationwebui':
63 switch (textgenerationwebui_settings.type) {68 switch (textGenType ?? textgenerationwebui_settings.type) {
64 case textgen_types.OPENROUTER:69 case textgen_types.OPENROUTER:
65 return data?.choices?.[0]?.reasoning ?? '';70 return data?.choices?.[0]?.reasoning ?? '';
66 }71 }
67 break;72 break;
6873
69 case 'openai':74 case 'openai':
70 if (!oai_settings.show_thoughts) break;75 if (!ignoreShowThoughts && !oai_settings.show_thoughts) break;
7176
72 switch (oai_settings.chat_completion_source) {77 switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
73 case chat_completion_sources.DEEPSEEK:78 case chat_completion_sources.DEEPSEEK:
74 return data?.choices?.[0]?.message?.reasoning_content ?? '';79 return data?.choices?.[0]?.message?.reasoning_content ?? '';
75 case chat_completion_sources.OPENROUTER:80 case chat_completion_sources.OPENROUTER:
public/scripts/st-context.js+2 -0
@@ -80,6 +80,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
80import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';80import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
81import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';81import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
82import { ChatCompletionService, TextCompletionService } from './custom-request.js';82import { ChatCompletionService, TextCompletionService } from './custom-request.js';
83import { ConnectionManagerRequestService } from './extensions/shared.js';
83import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';84import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';
8485
85export function getContext() {86export function getContext() {
@@ -215,6 +216,7 @@ export function getContext() {
215 clearChat,216 clearChat,
216 ChatCompletionService,217 ChatCompletionService,
217 TextCompletionService,218 TextCompletionService,
219 ConnectionManagerRequestService,
218 updateReasoningUI,220 updateReasoningUI,
219 parseReasoningFromString,221 parseReasoningFromString,
220 unshallowCharacter,222 unshallowCharacter,
public/scripts/textgen-settings.js+1 -1
@@ -86,7 +86,7 @@ const OOBA_DEFAULT_ORDER = [
86 'encoder_repetition_penalty',86 'encoder_repetition_penalty',
87 'no_repeat_ngram',87 'no_repeat_ngram',
88];88];
89const APHRODITE_DEFAULT_ORDER = [89export const APHRODITE_DEFAULT_ORDER = [
90 'dry',90 'dry',
91 'penalties',91 'penalties',
92 'no_repeat_ngram',92 'no_repeat_ngram',
src/endpoints/tokenizers.js+1 -1
@@ -411,7 +411,7 @@ export function getTokenizerModel(requestModel) {
411 return 'gpt-4o';411 return 'gpt-4o';
412 }412 }
413413
414 if (requestModel.includes('gpt-4.5-preview')) {414 if (requestModel.includes('gpt-4.5')) {
415 return 'gpt-4o';415 return 'gpt-4o';
416 }416 }
417417