mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into feat/ext-manager-toolbar
This commit is contained in:
@@ -36,10 +36,10 @@ jobs:
|
|||||||
for ISSUE in $(echo $issues | jq -r '.[]'); do
|
for ISSUE in $(echo $issues | jq -r '.[]'); do
|
||||||
if [ "${{ github.ref }}" == "refs/heads/staging" ]; then
|
if [ "${{ github.ref }}" == "refs/heads/staging" ]; then
|
||||||
LABEL="✅ Done (staging)"
|
LABEL="✅ Done (staging)"
|
||||||
gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL"
|
gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL" --remove-label "🧑💻 In Progress"
|
||||||
elif [ "${{ github.ref }}" == "refs/heads/release" ]; then
|
elif [ "${{ github.ref }}" == "refs/heads/release" ]; then
|
||||||
LABEL="✅ Done"
|
LABEL="✅ Done"
|
||||||
gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL"
|
gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL" --remove-label "🧑💻 In Progress"
|
||||||
fi
|
fi
|
||||||
echo "Added label '$LABEL' to issue #$ISSUE"
|
echo "Added label '$LABEL' (and removed '🧑💻 In Progress' if present) in issue #$ISSUE"
|
||||||
done
|
done
|
||||||
|
4
.github/workflows/pr-auto-manager.yml
vendored
4
.github/workflows/pr-auto-manager.yml
vendored
@@ -262,6 +262,6 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
for ISSUE in $(echo $final_issues | jq -r '.[]'); do
|
for ISSUE in $(echo $final_issues | jq -r '.[]'); do
|
||||||
gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)"
|
gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)" --remove-label "🧑💻 In Progress"
|
||||||
echo "Added label '✅ Done (staging)' to issue #$ISSUE"
|
echo "Added label '✅ Done (staging)' (and removed '🧑💻 In Progress' if present) in issue #$ISSUE"
|
||||||
done
|
done
|
||||||
|
@@ -5998,6 +5998,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<i class="menu_button move_entry_button fa-solid fa-right-left" title="Move Entry to Another Lorebook" data-i18n="[title]Move Entry to Another Lorebook"></i>
|
||||||
<i class="menu_button duplicate_entry_button fa-solid fa-paste" title="Duplicate world info entry" data-i18n="[title]Duplicate world info entry" type="submit" value=""></i>
|
<i class="menu_button duplicate_entry_button fa-solid fa-paste" title="Duplicate world info entry" data-i18n="[title]Duplicate world info entry" type="submit" value=""></i>
|
||||||
<i class="menu_button delete_entry_button fa-solid fa-trash-can" title="Delete world info entry" data-i18n="[title]Delete world info entry" type="submit" value=""></i>
|
<i class="menu_button delete_entry_button fa-solid fa-trash-can" title="Delete world info entry" data-i18n="[title]Delete world info entry" type="submit" value=""></i>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { DOMPurify, Popper } from '../lib.js';
|
import { DOMPurify } from '../lib.js';
|
||||||
|
|
||||||
import { event_types, eventSource, is_send_press, main_api, substituteParams } from '../script.js';
|
import { event_types, eventSource, is_send_press, main_api, substituteParams } from '../script.js';
|
||||||
import { is_group_generating } from './group-chats.js';
|
import { is_group_generating } from './group-chats.js';
|
||||||
@@ -1440,36 +1440,8 @@ class PromptManager {
|
|||||||
footerDiv.querySelector('select').selectedIndex = selectedPromptIndex;
|
footerDiv.querySelector('select').selectedIndex = selectedPromptIndex;
|
||||||
|
|
||||||
// Add prompt export dialogue and options
|
// Add prompt export dialogue and options
|
||||||
|
|
||||||
const exportForCharacter = await renderTemplateAsync('promptManagerExportForCharacter');
|
|
||||||
const exportPopup = await renderTemplateAsync('promptManagerExportPopup', { isGlobalStrategy: 'global' === this.configuration.promptOrder.strategy, exportForCharacter });
|
|
||||||
rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup);
|
|
||||||
|
|
||||||
// Destroy previous popper instance if it exists
|
|
||||||
if (this.exportPopper) {
|
|
||||||
this.exportPopper.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.exportPopper = Popper.createPopper(
|
|
||||||
document.getElementById('prompt-manager-export'),
|
|
||||||
document.getElementById('prompt-manager-export-format-popup'),
|
|
||||||
{ placement: 'bottom' },
|
|
||||||
);
|
|
||||||
|
|
||||||
const showExportSelection = () => {
|
|
||||||
const popup = document.getElementById('prompt-manager-export-format-popup');
|
|
||||||
const show = popup.hasAttribute('data-show');
|
|
||||||
|
|
||||||
if (show) popup.removeAttribute('data-show');
|
|
||||||
else popup.setAttribute('data-show', '');
|
|
||||||
|
|
||||||
this.exportPopper.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport);
|
footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport);
|
||||||
footerDiv.querySelector('#prompt-manager-export').addEventListener('click', showExportSelection);
|
footerDiv.querySelector('#prompt-manager-export').addEventListener('click', this.handleFullExport);
|
||||||
rangeBlockDiv.querySelector('.export-promptmanager-prompts-full').addEventListener('click', this.handleFullExport);
|
|
||||||
rangeBlockDiv.querySelector('.export-promptmanager-prompts-character')?.addEventListener('click', this.handleCharacterExport);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ import { getPresetManager } from './preset-manager.js';
|
|||||||
import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
|
import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
|
||||||
import { getTextGenServer } from './textgen-settings.js';
|
import { getTextGenServer } from './textgen-settings.js';
|
||||||
import { extractReasoningFromData } from './reasoning.js';
|
import { extractReasoningFromData } from './reasoning.js';
|
||||||
import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';
|
import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js';
|
||||||
import { getStreamingReply, tryParseStreamingError } from './openai.js';
|
import { getStreamingReply, tryParseStreamingError } from './openai.js';
|
||||||
import EventSourceStream from './sse-stream.js';
|
import EventSourceStream from './sse-stream.js';
|
||||||
|
|
||||||
@@ -190,6 +190,7 @@ export class TextCompletionService {
|
|||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
* @param {string?} [options.presetName] - Name of the preset to use for generation settings
|
* @param {string?} [options.presetName] - Name of the preset to use for generation settings
|
||||||
* @param {string?} [options.instructName] - Name of instruct preset for message formatting
|
* @param {string?} [options.instructName] - Name of instruct preset for message formatting
|
||||||
|
* @param {Partial<InstructSettings>?} [options.instructSettings] - Override instruct settings
|
||||||
* @param {boolean} extractData - Whether to extract structured data from response
|
* @param {boolean} extractData - Whether to extract structured data from response
|
||||||
* @param {AbortSignal?} [signal]
|
* @param {AbortSignal?} [signal]
|
||||||
* @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
|
* @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
|
||||||
@@ -222,15 +223,20 @@ export class TextCompletionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @type {InstructSettings | undefined} */
|
||||||
|
let instructPreset;
|
||||||
// Handle instruct formatting if requested
|
// Handle instruct formatting if requested
|
||||||
if (Array.isArray(prompt) && instructName) {
|
if (Array.isArray(prompt) && instructName) {
|
||||||
const instructPresetManager = getPresetManager('instruct');
|
const instructPresetManager = getPresetManager('instruct');
|
||||||
let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
|
instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
|
||||||
if (instructPreset) {
|
if (instructPreset) {
|
||||||
// Clone the preset to avoid modifying the original
|
// Clone the preset to avoid modifying the original
|
||||||
instructPreset = structuredClone(instructPreset);
|
instructPreset = structuredClone(instructPreset);
|
||||||
instructPreset.macro = false;
|
|
||||||
instructPreset.names_behavior = names_behavior_types.NONE;
|
instructPreset.names_behavior = names_behavior_types.NONE;
|
||||||
|
if (options.instructSettings) {
|
||||||
|
Object.assign(instructPreset, options.instructSettings);
|
||||||
|
}
|
||||||
|
|
||||||
// Format messages using instruct formatting
|
// Format messages using instruct formatting
|
||||||
const formattedMessages = [];
|
const formattedMessages = [];
|
||||||
@@ -266,10 +272,9 @@ export class TextCompletionService {
|
|||||||
formattedMessages.push(messageContent);
|
formattedMessages.push(messageContent);
|
||||||
}
|
}
|
||||||
requestData.prompt = formattedMessages.join('');
|
requestData.prompt = formattedMessages.join('');
|
||||||
if (instructPreset.output_suffix) {
|
const stoppingStrings = getInstructStoppingSequences({ customInstruct: instructPreset, useStopStrings: false });
|
||||||
requestData.stop = [instructPreset.output_suffix];
|
requestData.stop = stoppingStrings;
|
||||||
requestData.stopping_strings = [instructPreset.output_suffix];
|
requestData.stopping_strings = stoppingStrings;
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
|
console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
|
||||||
requestData.prompt = prompt.map(x => x.content).join('\n\n');
|
requestData.prompt = prompt.map(x => x.content).join('\n\n');
|
||||||
@@ -283,7 +288,61 @@ export class TextCompletionService {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const data = this.createRequestData(requestData);
|
const data = this.createRequestData(requestData);
|
||||||
|
|
||||||
return await this.sendRequest(data, extractData, signal);
|
const response = await this.sendRequest(data, extractData, signal);
|
||||||
|
// Remove stopping strings from the end
|
||||||
|
if (!data.stream && extractData) {
|
||||||
|
/** @type {ExtractedData} */
|
||||||
|
// @ts-ignore
|
||||||
|
const extractedData = response;
|
||||||
|
|
||||||
|
let message = extractedData.content;
|
||||||
|
|
||||||
|
message = message.replace(/[^\S\r\n]+$/gm, '');
|
||||||
|
|
||||||
|
if (requestData.stopping_strings) {
|
||||||
|
for (const stoppingString of requestData.stopping_strings) {
|
||||||
|
if (stoppingString.length) {
|
||||||
|
for (let j = stoppingString.length; j > 0; j--) {
|
||||||
|
if (message.slice(-j) === stoppingString.slice(0, j)) {
|
||||||
|
message = message.slice(0, -j);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instructPreset) {
|
||||||
|
[
|
||||||
|
instructPreset.stop_sequence,
|
||||||
|
instructPreset.input_sequence,
|
||||||
|
].forEach(sequence => {
|
||||||
|
if (sequence?.trim()) {
|
||||||
|
const index = message.indexOf(sequence);
|
||||||
|
if (index !== -1) {
|
||||||
|
message = message.substring(0, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
instructPreset.output_sequence,
|
||||||
|
instructPreset.last_output_sequence,
|
||||||
|
].forEach(sequences => {
|
||||||
|
if (sequences) {
|
||||||
|
sequences.split('\n')
|
||||||
|
.filter(line => line.trim() !== '')
|
||||||
|
.forEach(line => {
|
||||||
|
message = message.replaceAll(line, '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extractedData.content = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -285,6 +285,7 @@ export class ConnectionManagerRequestService {
|
|||||||
extractData: true,
|
extractData: true,
|
||||||
includePreset: true,
|
includePreset: true,
|
||||||
includeInstruct: true,
|
includeInstruct: true,
|
||||||
|
instructSettings: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
static getAllowedTypes() {
|
static getAllowedTypes() {
|
||||||
@@ -298,11 +299,17 @@ export class ConnectionManagerRequestService {
|
|||||||
* @param {string} profileId
|
* @param {string} profileId
|
||||||
* @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
|
* @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
|
||||||
* @param {number} maxTokens
|
* @param {number} maxTokens
|
||||||
* @param {{stream?: boolean, signal?: AbortSignal, extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true
|
* @param {Object} custom
|
||||||
|
* @param {boolean?} [custom.stream=false]
|
||||||
|
* @param {AbortSignal?} [custom.signal]
|
||||||
|
* @param {boolean?} [custom.extractData=true]
|
||||||
|
* @param {boolean?} [custom.includePreset=true]
|
||||||
|
* @param {boolean?} [custom.includeInstruct=true]
|
||||||
|
* @param {Partial<InstructSettings>?} [custom.instructSettings] Override instruct settings
|
||||||
* @returns {Promise<import('../custom-request.js').ExtractedData | (() => AsyncGenerator<import('../custom-request.js').StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
|
* @returns {Promise<import('../custom-request.js').ExtractedData | (() => AsyncGenerator<import('../custom-request.js').StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
|
||||||
*/
|
*/
|
||||||
static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
|
static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
|
||||||
const { stream, signal, extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };
|
const { stream, signal, extractData, includePreset, includeInstruct, instructSettings } = { ...this.defaultSendRequestParams, ...custom };
|
||||||
|
|
||||||
const context = SillyTavern.getContext();
|
const context = SillyTavern.getContext();
|
||||||
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
|
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
|
||||||
@@ -346,6 +353,7 @@ export class ConnectionManagerRequestService {
|
|||||||
}, {
|
}, {
|
||||||
instructName: includeInstruct ? profile.instruct : undefined,
|
instructName: includeInstruct ? profile.instruct : undefined,
|
||||||
presetName: includePreset ? profile.preset : undefined,
|
presetName: includePreset ? profile.preset : undefined,
|
||||||
|
instructSettings: includeInstruct ? instructSettings : undefined,
|
||||||
}, extractData, signal);
|
}, extractData, signal);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
@@ -243,9 +243,14 @@ export function autoSelectInstructPreset(modelId) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts instruct mode sequences to an array of stopping strings.
|
* Converts instruct mode sequences to an array of stopping strings.
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {InstructSettings?} [options.customInstruct=null] - Custom instruct settings.
|
||||||
|
* @param {boolean?} [options.useStopStrings] - Decides whether to use "Chat Start" and "Example Separator"
|
||||||
* @returns {string[]} Array of instruct mode stopping strings.
|
* @returns {string[]} Array of instruct mode stopping strings.
|
||||||
*/
|
*/
|
||||||
export function getInstructStoppingSequences() {
|
export function getInstructStoppingSequences({ customInstruct = null, useStopStrings = null } = {}) {
|
||||||
|
const instruct = structuredClone(customInstruct ?? power_user.instruct);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds instruct mode sequence to the result array.
|
* Adds instruct mode sequence to the result array.
|
||||||
* @param {string} sequence Sequence string.
|
* @param {string} sequence Sequence string.
|
||||||
@@ -254,7 +259,7 @@ export function getInstructStoppingSequences() {
|
|||||||
function addInstructSequence(sequence) {
|
function addInstructSequence(sequence) {
|
||||||
// Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string
|
// Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string
|
||||||
// But it's a problem for Metharme which doesn't use newlines to separate them.
|
// But it's a problem for Metharme which doesn't use newlines to separate them.
|
||||||
const wrap = (s) => power_user.instruct.wrap ? '\n' + s : s;
|
const wrap = (s) => instruct.wrap ? '\n' + s : s;
|
||||||
// Sequence must be a non-empty string
|
// Sequence must be a non-empty string
|
||||||
if (typeof sequence === 'string' && sequence.length > 0) {
|
if (typeof sequence === 'string' && sequence.length > 0) {
|
||||||
// If sequence is just a whitespace or newline - we don't want to make it a stopping string
|
// If sequence is just a whitespace or newline - we don't want to make it a stopping string
|
||||||
@@ -262,7 +267,7 @@ export function getInstructStoppingSequences() {
|
|||||||
if (sequence.trim().length > 0) {
|
if (sequence.trim().length > 0) {
|
||||||
const wrappedSequence = wrap(sequence);
|
const wrappedSequence = wrap(sequence);
|
||||||
// Need to respect "insert macro" setting
|
// Need to respect "insert macro" setting
|
||||||
const stopString = power_user.instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence;
|
const stopString = instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence;
|
||||||
result.push(stopString);
|
result.push(stopString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,14 +275,15 @@ export function getInstructStoppingSequences() {
|
|||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
if (power_user.instruct.enabled) {
|
// Since preset's don't have "enabled", we assume it's always enabled
|
||||||
const stop_sequence = power_user.instruct.stop_sequence || '';
|
if (customInstruct ?? instruct.enabled) {
|
||||||
const input_sequence = power_user.instruct.input_sequence?.replace(/{{name}}/gi, name1) || '';
|
const stop_sequence = instruct.stop_sequence || '';
|
||||||
const output_sequence = power_user.instruct.output_sequence?.replace(/{{name}}/gi, name2) || '';
|
const input_sequence = instruct.input_sequence?.replace(/{{name}}/gi, name1) || '';
|
||||||
const first_output_sequence = power_user.instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || '';
|
const output_sequence = instruct.output_sequence?.replace(/{{name}}/gi, name2) || '';
|
||||||
const last_output_sequence = power_user.instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || '';
|
const first_output_sequence = instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || '';
|
||||||
const system_sequence = power_user.instruct.system_sequence?.replace(/{{name}}/gi, 'System') || '';
|
const last_output_sequence = instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || '';
|
||||||
const last_system_sequence = power_user.instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || '';
|
const system_sequence = instruct.system_sequence?.replace(/{{name}}/gi, 'System') || '';
|
||||||
|
const last_system_sequence = instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || '';
|
||||||
|
|
||||||
const combined_sequence = [
|
const combined_sequence = [
|
||||||
stop_sequence,
|
stop_sequence,
|
||||||
@@ -292,7 +298,7 @@ export function getInstructStoppingSequences() {
|
|||||||
combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence);
|
combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (power_user.context.use_stop_strings) {
|
if (useStopStrings ?? power_user.context.use_stop_strings) {
|
||||||
if (power_user.context.chat_start) {
|
if (power_user.context.chat_start) {
|
||||||
result.push(`\n${substituteParams(power_user.context.chat_start)}`);
|
result.push(`\n${substituteParams(power_user.context.chat_start)}`);
|
||||||
}
|
}
|
||||||
|
@@ -111,6 +111,7 @@ export function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigate
|
|||||||
reloadUserAvatar();
|
reloadUserAvatar();
|
||||||
updatePersonaUIStates({ navigateToCurrent: navigateToCurrent });
|
updatePersonaUIStates({ navigateToCurrent: navigateToCurrent });
|
||||||
selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange });
|
selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange });
|
||||||
|
retriggerFirstMessageOnEmptyChat();
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
$('.zoomed_avatar[forchar]').remove();
|
$('.zoomed_avatar[forchar]').remove();
|
||||||
}
|
}
|
||||||
@@ -465,7 +466,7 @@ export function initPersona(avatarId, personaName, personaDescription) {
|
|||||||
* @returns {Promise<boolean>} A promise that resolves to true if the character was converted, false otherwise.
|
* @returns {Promise<boolean>} A promise that resolves to true if the character was converted, false otherwise.
|
||||||
*/
|
*/
|
||||||
export async function convertCharacterToPersona(characterId = null) {
|
export async function convertCharacterToPersona(characterId = null) {
|
||||||
if (null === characterId) characterId = this_chid;
|
if (null === characterId) characterId = Number(this_chid);
|
||||||
|
|
||||||
const avatarUrl = characters[characterId]?.avatar;
|
const avatarUrl = characters[characterId]?.avatar;
|
||||||
if (!avatarUrl) {
|
if (!avatarUrl) {
|
||||||
@@ -1243,7 +1244,7 @@ function getPersonaStates(avatarId) {
|
|||||||
/** @type {PersonaConnection[]} */
|
/** @type {PersonaConnection[]} */
|
||||||
const connections = power_user.persona_descriptions[avatarId]?.connections;
|
const connections = power_user.persona_descriptions[avatarId]?.connections;
|
||||||
const hasCharLock = !!connections?.some(c =>
|
const hasCharLock = !!connections?.some(c =>
|
||||||
(!selected_group && c.type === 'character' && c.id === characters[this_chid]?.avatar)
|
(!selected_group && c.type === 'character' && c.id === characters[Number(this_chid)]?.avatar)
|
||||||
|| (selected_group && c.type === 'group' && c.id === selected_group));
|
|| (selected_group && c.type === 'group' && c.id === selected_group));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1481,7 +1482,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) {
|
|||||||
* @returns {string[]} - An array of persona keys that are connected to the given character key
|
* @returns {string[]} - An array of persona keys that are connected to the given character key
|
||||||
*/
|
*/
|
||||||
export function getConnectedPersonas(characterKey = undefined) {
|
export function getConnectedPersonas(characterKey = undefined) {
|
||||||
characterKey ??= selected_group || characters[this_chid]?.avatar;
|
characterKey ??= selected_group || characters[Number(this_chid)]?.avatar;
|
||||||
const connectedPersonas = Object.entries(power_user.persona_descriptions)
|
const connectedPersonas = Object.entries(power_user.persona_descriptions)
|
||||||
.filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey))
|
.filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey))
|
||||||
.map(([key, _]) => key);
|
.map(([key, _]) => key);
|
||||||
@@ -1513,7 +1514,7 @@ export async function showCharConnections() {
|
|||||||
console.log(`Unlocking persona ${personaId} from current character ${name2}`);
|
console.log(`Unlocking persona ${personaId} from current character ${name2}`);
|
||||||
power_user.persona_descriptions[personaId].connections = connections.filter(c => {
|
power_user.persona_descriptions[personaId].connections = connections.filter(c => {
|
||||||
if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false;
|
if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false;
|
||||||
else if (c.type == 'character' && c.id == characters[this_chid]?.avatar) return false;
|
else if (c.type == 'character' && c.id == characters[Number(this_chid)]?.avatar) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
@@ -1545,8 +1546,8 @@ export async function showCharConnections() {
|
|||||||
export function getCurrentConnectionObj() {
|
export function getCurrentConnectionObj() {
|
||||||
if (selected_group)
|
if (selected_group)
|
||||||
return { type: 'group', id: selected_group };
|
return { type: 'group', id: selected_group };
|
||||||
if (characters[this_chid]?.avatar)
|
if (characters[Number(this_chid)]?.avatar)
|
||||||
return { type: 'character', id: characters[this_chid]?.avatar };
|
return { type: 'character', id: characters[Number(this_chid)]?.avatar };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1664,7 +1665,7 @@ async function syncUserNameToPersona() {
|
|||||||
* Only works if only the first message is present, and not in group mode.
|
* Only works if only the first message is present, and not in group mode.
|
||||||
*/
|
*/
|
||||||
export function retriggerFirstMessageOnEmptyChat() {
|
export function retriggerFirstMessageOnEmptyChat() {
|
||||||
if (this_chid >= 0 && !selected_group && chat.length === 1) {
|
if (Number(this_chid) >= 0 && !selected_group && chat.length === 1) {
|
||||||
$('#firstmessage_textarea').trigger('input');
|
$('#firstmessage_textarea').trigger('input');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1782,7 +1783,6 @@ function setNameCallback({ mode = 'all' }, name) {
|
|||||||
if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1];
|
if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1];
|
||||||
if (persona) {
|
if (persona) {
|
||||||
autoSelectPersona(persona);
|
autoSelectPersona(persona);
|
||||||
retriggerFirstMessageOnEmptyChat();
|
|
||||||
return '';
|
return '';
|
||||||
} else if (mode === 'lookup') {
|
} else if (mode === 'lookup') {
|
||||||
toastr.warning(`Persona ${name} not found`);
|
toastr.warning(`Persona ${name} not found`);
|
||||||
@@ -1793,7 +1793,6 @@ function setNameCallback({ mode = 'all' }, name) {
|
|||||||
if (['temp', 'all'].includes(mode)) {
|
if (['temp', 'all'].includes(mode)) {
|
||||||
// Otherwise, set just the name
|
// Otherwise, set just the name
|
||||||
setUserName(name); //this prevented quickReply usage
|
setUserName(name); //this prevented quickReply usage
|
||||||
retriggerFirstMessageOnEmptyChat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
@@ -1944,9 +1943,6 @@ export async function initPersonas() {
|
|||||||
$(document).on('click', '#user_avatar_block .avatar-container', function () {
|
$(document).on('click', '#user_avatar_block .avatar-container', function () {
|
||||||
const imgfile = $(this).attr('data-avatar-id');
|
const imgfile = $(this).attr('data-avatar-id');
|
||||||
setUserAvatar(imgfile);
|
setUserAvatar(imgfile);
|
||||||
|
|
||||||
// force firstMes {{user}} update on persona switch
|
|
||||||
retriggerFirstMessageOnEmptyChat();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#persona_rename_button').on('click', () => renamePersona(user_avatar));
|
$('#persona_rename_button').on('click', () => renamePersona(user_avatar));
|
||||||
@@ -1979,4 +1975,3 @@ export async function initPersonas() {
|
|||||||
eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat);
|
eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat);
|
||||||
switchPersonaGridView();
|
switchPersonaGridView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
<div class="row">
|
|
||||||
<a class="export-promptmanager-prompts-character list-group-item" data-i18n="Export for character">Export for character</a>
|
|
||||||
<span class="tooltip fa-solid fa-info-circle" data-i18n="[title]Export prompts for this character, including their order." title="Export prompts for this character, including their order."></span>
|
|
||||||
</div>
|
|
@@ -1,12 +0,0 @@
|
|||||||
<div id="prompt-manager-export-format-popup" class="list-group">
|
|
||||||
<div class="prompt-manager-export-format-popup-flex">
|
|
||||||
<div class="row">
|
|
||||||
<a class="export-promptmanager-prompts-full list-group-item" data-i18n="Export all">Export all</a>
|
|
||||||
<span class="tooltip fa-solid fa-info-circle" data-i18n="[title]Export all your prompts to a file" title="Export all your prompts to a file"></span>
|
|
||||||
</div>
|
|
||||||
{{#if isGlobalStrategy}}
|
|
||||||
{{else}}
|
|
||||||
{{{exportForCharacter}}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -1,4 +1,4 @@
|
|||||||
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 4.5em;">
|
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 7.0em;">
|
||||||
<small class="flex1" data-i18n="Title/Memo">Title/Memo</small>
|
<small class="flex1" data-i18n="Title/Memo">Title/Memo</small>
|
||||||
<small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small>
|
<small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small>
|
||||||
<small style="width: calc(3.5em + 20px)" data-i18n="Position">Position</small>
|
<small style="width: calc(3.5em + 20px)" data-i18n="Position">Position</small>
|
||||||
|
@@ -2208,7 +2208,7 @@ function verifyWorldInfoSearchSortRule() {
|
|||||||
* Use `originalWIDataKeyMap` to find the correct value to be set.
|
* Use `originalWIDataKeyMap` to find the correct value to be set.
|
||||||
*
|
*
|
||||||
* @param {object} data - The data object containing the original data entries.
|
* @param {object} data - The data object containing the original data entries.
|
||||||
* @param {string} uid - The unique identifier of the data entry.
|
* @param {number} uid - The unique identifier of the data entry.
|
||||||
* @param {string} key - The key of the value to be set.
|
* @param {string} key - The key of the value to be set.
|
||||||
* @param {any} value - The value to be set.
|
* @param {any} value - The value to be set.
|
||||||
*/
|
*/
|
||||||
@@ -2232,7 +2232,9 @@ export function setWIOriginalDataValue(data, uid, key, value) {
|
|||||||
*/
|
*/
|
||||||
export function deleteWIOriginalDataValue(data, uid) {
|
export function deleteWIOriginalDataValue(data, uid) {
|
||||||
if (data.originalData && Array.isArray(data.originalData.entries)) {
|
if (data.originalData && Array.isArray(data.originalData.entries)) {
|
||||||
const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid);
|
// Non-strict equality is used here to allow for both string and number comparisons
|
||||||
|
// @eslint-disable-next-line eqeqeq
|
||||||
|
const originalIndex = data.originalData.entries.findIndex(x => x.uid == uid);
|
||||||
|
|
||||||
if (originalIndex >= 0) {
|
if (originalIndex >= 0) {
|
||||||
data.originalData.entries.splice(originalIndex, 1);
|
data.originalData.entries.splice(originalIndex, 1);
|
||||||
@@ -3143,6 +3145,84 @@ export async function getWorldEntry(name, data, entry) {
|
|||||||
updateEditor(navigation_option.previous);
|
updateEditor(navigation_option.previous);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// move button
|
||||||
|
const moveButton = template.find('.move_entry_button');
|
||||||
|
moveButton.attr('data-uid', entry.uid);
|
||||||
|
moveButton.attr('data-current-world', name);
|
||||||
|
moveButton.on('click', async function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const sourceUid = $(this).attr('data-uid');
|
||||||
|
const sourceWorld = $(this).attr('data-current-world');
|
||||||
|
const sourceWorldInfo = await loadWorldInfo(sourceWorld);
|
||||||
|
if (!sourceWorldInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sourceName = sourceWorldInfo.entries[sourceUid]?.comment;
|
||||||
|
if (sourceName === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.id = 'move_entry_target_select';
|
||||||
|
select.classList.add('text_pole', 'wide100p', 'marginTop10');
|
||||||
|
|
||||||
|
const defaultOption = document.createElement('option');
|
||||||
|
defaultOption.value = '';
|
||||||
|
defaultOption.textContent = `-- ${t`Select Target Lorebook`} --`;
|
||||||
|
select.appendChild(defaultOption);
|
||||||
|
|
||||||
|
let selectableWorldCount = 0;
|
||||||
|
world_names.forEach(worldName => {
|
||||||
|
if (worldName !== sourceWorld) { // Exclude current world
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = world_names.indexOf(worldName).toString();
|
||||||
|
option.textContent = worldName;
|
||||||
|
select.appendChild(option);
|
||||||
|
selectableWorldCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectableWorldCount === 0) {
|
||||||
|
toastr.warning(t`There are no other lorebooks to move to.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create wrapper div
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.textContent = t`Move "${sourceName}" to:`;
|
||||||
|
|
||||||
|
// Create container and append elements
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
container.appendChild(select);
|
||||||
|
|
||||||
|
let selectedWorldIndex = -1;
|
||||||
|
select.addEventListener('change', function() {
|
||||||
|
selectedWorldIndex = this.value === '' ? -1 : Number(this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const popupConfirm = await callGenericPopup(container, POPUP_TYPE.CONFIRM, '', {
|
||||||
|
okButton: t`Move`,
|
||||||
|
cancelButton: t`Cancel`,
|
||||||
|
});
|
||||||
|
if (!popupConfirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedWorldIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedValue = world_names[selectedWorldIndex];
|
||||||
|
|
||||||
|
if (!selectedValue) {
|
||||||
|
toastr.warning(t`Please select a target lorebook.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await moveWorldInfoEntry(sourceWorld, selectedValue, sourceUid);
|
||||||
|
});
|
||||||
|
|
||||||
// scan depth
|
// scan depth
|
||||||
const scanDepthInput = template.find('input[name="scanDepth"]');
|
const scanDepthInput = template.find('input[name="scanDepth"]');
|
||||||
scanDepthInput.data('uid', entry.uid);
|
scanDepthInput.data('uid', entry.uid);
|
||||||
@@ -5271,3 +5351,99 @@ jQuery(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a World Info entry from a source lorebook to a target lorebook.
|
||||||
|
*
|
||||||
|
* @param {string} sourceName - The name of the source lorebook file.
|
||||||
|
* @param {string} targetName - The name of the target lorebook file.
|
||||||
|
* @param {string|number} uid - The UID of the entry to move from the source lorebook.
|
||||||
|
* @returns {Promise<boolean>} True if the move was successful, false otherwise.
|
||||||
|
*/
|
||||||
|
export async function moveWorldInfoEntry(sourceName, targetName, uid) {
|
||||||
|
if (sourceName === targetName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!world_names.includes(sourceName)) {
|
||||||
|
toastr.error(t`Source lorebook '${sourceName}' not found.`);
|
||||||
|
console.error(`[WI Move] Source lorebook '${sourceName}' does not exist.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!world_names.includes(targetName)) {
|
||||||
|
toastr.error(t`Target lorebook '${targetName}' not found.`);
|
||||||
|
console.error(`[WI Move] Target lorebook '${targetName}' does not exist.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryUidString = String(uid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceData = await loadWorldInfo(sourceName);
|
||||||
|
const targetData = await loadWorldInfo(targetName);
|
||||||
|
|
||||||
|
if (!sourceData || !sourceData.entries) {
|
||||||
|
toastr.error(t`Failed to load data for source lorebook '${sourceName}'.`);
|
||||||
|
console.error(`[WI Move] Could not load source data for '${sourceName}'.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!targetData || !targetData.entries) {
|
||||||
|
toastr.error(t`Failed to load data for target lorebook '${targetName}'.`);
|
||||||
|
console.error(`[WI Move] Could not load target data for '${targetName}'.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceData.entries[entryUidString]) {
|
||||||
|
toastr.error(t`Entry not found in source lorebook '${sourceName}'.`);
|
||||||
|
console.error(`[WI Move] Entry UID ${entryUidString} not found in '${sourceName}'.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryToMove = structuredClone(sourceData.entries[entryUidString]);
|
||||||
|
|
||||||
|
|
||||||
|
const newUid = getFreeWorldEntryUid(targetData);
|
||||||
|
if (newUid === null) {
|
||||||
|
console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entryToMove.uid = newUid;
|
||||||
|
// Place the entry at the end of the target lorebook
|
||||||
|
const maxDisplayIndex = Object.values(targetData.entries).reduce((max, entry) => Math.max(max, entry.displayIndex ?? -1), -1);
|
||||||
|
entryToMove.displayIndex = maxDisplayIndex + 1;
|
||||||
|
|
||||||
|
targetData.entries[newUid] = entryToMove;
|
||||||
|
|
||||||
|
delete sourceData.entries[entryUidString];
|
||||||
|
// Remove from originalData if it exists
|
||||||
|
deleteWIOriginalDataValue(sourceData, entryUidString);
|
||||||
|
// TODO: setWIOriginalDataValue
|
||||||
|
console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
|
||||||
|
|
||||||
|
|
||||||
|
await saveWorldInfo(targetName, targetData, true);
|
||||||
|
console.debug(`[WI Move] Saved target lorebook '${targetName}'.`);
|
||||||
|
await saveWorldInfo(sourceName, sourceData, true);
|
||||||
|
console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`);
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`[WI Move] ${entryToMove.comment} moved successfully to '${targetName}'.`);
|
||||||
|
|
||||||
|
// Check if the currently viewed book in the editor is the source or target and reload it
|
||||||
|
const currentEditorBookIndex = Number($('#world_editor_select').val());
|
||||||
|
if (!isNaN(currentEditorBookIndex)) {
|
||||||
|
const currentEditorBookName = world_names[currentEditorBookIndex];
|
||||||
|
if (currentEditorBookName === sourceName || currentEditorBookName === targetName) {
|
||||||
|
reloadEditor(currentEditorBookName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`);
|
||||||
|
console.error('[WI Move] Unexpected error:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user