Merge branch 'staging' into feat/ext-manager-toolbar
| @@ -36,10 +36,10 @@ jobs: | |||
| 36 | for ISSUE in $(echo $issues | jq -r '.[]'); do | 36 | for ISSUE in $(echo $issues | jq -r '.[]'); do |
| 37 | if [ "${{ github.ref }}" == "refs/heads/staging" ]; then | 37 | if [ "${{ github.ref }}" == "refs/heads/staging" ]; then |
| 38 | LABEL="✅ Done (staging)" | 38 | LABEL="✅ Done (staging)" |
| 39 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL" | 39 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL" --remove-label "🧑💻 In Progress" |
| 40 | elif [ "${{ github.ref }}" == "refs/heads/release" ]; then | 40 | elif [ "${{ github.ref }}" == "refs/heads/release" ]; then |
| 41 | LABEL="✅ Done" | 41 | LABEL="✅ Done" |
| 42 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL" | 42 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL" --remove-label "🧑💻 In Progress" |
| 43 | fi | 43 | fi |
| 44 | echo "Added label '$LABEL' to issue #$ISSUE" | 44 | echo "Added label '$LABEL' (and removed '🧑💻 In Progress' if present) in issue #$ISSUE" |
| 45 | done | 45 | done |
| @@ -262,6 +262,6 @@ jobs: | |||
| 262 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | 262 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 263 | run: | | 263 | run: | |
| 264 | for ISSUE in $(echo $final_issues | jq -r '.[]'); do | 264 | for ISSUE in $(echo $final_issues | jq -r '.[]'); do |
| 265 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)" | 265 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)" --remove-label "🧑💻 In Progress" |
| 266 | echo "Added label '✅ Done (staging)' to issue #$ISSUE" | 266 | echo "Added label '✅ Done (staging)' (and removed '🧑💻 In Progress' if present) in issue #$ISSUE" |
| 267 | done | 267 | done |
| @@ -5998,6 +5998,7 @@ | |||
| 5998 | </div> | 5998 | </div> |
| 5999 | </div> | 5999 | </div> |
| 6000 | </div> | 6000 | </div> |
| 6001 | <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> | ||
| 6001 | <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> | 6002 | <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> |
| 6002 | <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> | 6003 | <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> |
| 6003 | </div> | 6004 | </div> |
| @@ -1,6 +1,6 @@ | |||
| 1 | 'use strict'; | 1 | 'use strict'; |
| 2 | 2 | ||
| 3 | import { DOMPurify, Popper } from '../lib.js'; | 3 | import { DOMPurify } from '../lib.js'; |
| 4 | 4 | ||
| 5 | import { event_types, eventSource, is_send_press, main_api, substituteParams } from '../script.js'; | 5 | import { event_types, eventSource, is_send_press, main_api, substituteParams } from '../script.js'; |
| 6 | import { is_group_generating } from './group-chats.js'; | 6 | import { is_group_generating } from './group-chats.js'; |
| @@ -1440,36 +1440,8 @@ class PromptManager { | |||
| 1440 | footerDiv.querySelector('select').selectedIndex = selectedPromptIndex; | 1440 | footerDiv.querySelector('select').selectedIndex = selectedPromptIndex; |
| 1441 | 1441 | ||
| 1442 | // Add prompt export dialogue and options | 1442 | // Add prompt export dialogue and options |
| 1443 | |||
| 1444 | const exportForCharacter = await renderTemplateAsync('promptManagerExportForCharacter'); | ||
| 1445 | const exportPopup = await renderTemplateAsync('promptManagerExportPopup', { isGlobalStrategy: 'global' === this.configuration.promptOrder.strategy, exportForCharacter }); | ||
| 1446 | rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup); | ||
| 1447 | |||
| 1448 | // Destroy previous popper instance if it exists | ||
| 1449 | if (this.exportPopper) { | ||
| 1450 | this.exportPopper.destroy(); | ||
| 1451 | } | ||
| 1452 | |||
| 1453 | this.exportPopper = Popper.createPopper( | ||
| 1454 | document.getElementById('prompt-manager-export'), | ||
| 1455 | document.getElementById('prompt-manager-export-format-popup'), | ||
| 1456 | { placement: 'bottom' }, | ||
| 1457 | ); | ||
| 1458 | |||
| 1459 | const showExportSelection = () => { | ||
| 1460 | const popup = document.getElementById('prompt-manager-export-format-popup'); | ||
| 1461 | const show = popup.hasAttribute('data-show'); | ||
| 1462 | |||
| 1463 | if (show) popup.removeAttribute('data-show'); | ||
| 1464 | else popup.setAttribute('data-show', ''); | ||
| 1465 | |||
| 1466 | this.exportPopper.update(); | ||
| 1467 | }; | ||
| 1468 | |||
| 1469 | footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport); | 1443 | footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport); |
| 1470 | footerDiv.querySelector('#prompt-manager-export').addEventListener('click', showExportSelection); | 1444 | footerDiv.querySelector('#prompt-manager-export').addEventListener('click', this.handleFullExport); |
| 1471 | rangeBlockDiv.querySelector('.export-promptmanager-prompts-full').addEventListener('click', this.handleFullExport); | ||
| 1472 | rangeBlockDiv.querySelector('.export-promptmanager-prompts-character')?.addEventListener('click', this.handleCharacterExport); | ||
| 1473 | } | 1445 | } |
| 1474 | } | 1446 | } |
| 1475 | 1447 | ||
| @@ -2,7 +2,7 @@ import { getPresetManager } from './preset-manager.js'; | |||
| 2 | import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js'; | 2 | import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js'; |
| 3 | import { getTextGenServer } from './textgen-settings.js'; | 3 | import { getTextGenServer } from './textgen-settings.js'; |
| 4 | import { extractReasoningFromData } from './reasoning.js'; | 4 | import { extractReasoningFromData } from './reasoning.js'; |
| 5 | import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js'; | 5 | import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js'; |
| 6 | import { getStreamingReply, tryParseStreamingError } from './openai.js'; | 6 | import { getStreamingReply, tryParseStreamingError } from './openai.js'; |
| 7 | import EventSourceStream from './sse-stream.js'; | 7 | import EventSourceStream from './sse-stream.js'; |
| 8 | 8 | ||
| @@ -190,6 +190,7 @@ export class TextCompletionService { | |||
| 190 | * @param {Object} options - Configuration options | 190 | * @param {Object} options - Configuration options |
| 191 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings | 191 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings |
| 192 | * @param {string?} [options.instructName] - Name of instruct preset for message formatting | 192 | * @param {string?} [options.instructName] - Name of instruct preset for message formatting |
| 193 | * @param {Partial<InstructSettings>?} [options.instructSettings] - Override instruct settings | ||
| 193 | * @param {boolean} extractData - Whether to extract structured data from response | 194 | * @param {boolean} extractData - Whether to extract structured data from response |
| 194 | * @param {AbortSignal?} [signal] | 195 | * @param {AbortSignal?} [signal] |
| 195 | * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator | 196 | * @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 { | |||
| 222 | } | 223 | } |
| 223 | } | 224 | } |
| 224 | 225 | ||
| 226 | |||
| 227 | /** @type {InstructSettings | undefined} */ | ||
| 228 | let instructPreset; | ||
| 225 | // Handle instruct formatting if requested | 229 | // Handle instruct formatting if requested |
| 226 | if (Array.isArray(prompt) && instructName) { | 230 | if (Array.isArray(prompt) && instructName) { |
| 227 | const instructPresetManager = getPresetManager('instruct'); | 231 | const instructPresetManager = getPresetManager('instruct'); |
| 228 | let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName); | 232 | instructPreset = instructPresetManager?.getCompletionPresetByName(instructName); |
| 229 | if (instructPreset) { | 233 | if (instructPreset) { |
| 230 | // Clone the preset to avoid modifying the original | 234 | // Clone the preset to avoid modifying the original |
| 231 | instructPreset = structuredClone(instructPreset); | 235 | instructPreset = structuredClone(instructPreset); |
| 232 | instructPreset.macro = false; | ||
| 233 | instructPreset.names_behavior = names_behavior_types.NONE; | 236 | instructPreset.names_behavior = names_behavior_types.NONE; |
| 237 | if (options.instructSettings) { | ||
| 238 | Object.assign(instructPreset, options.instructSettings); | ||
| 239 | } | ||
| 234 | 240 | ||
| 235 | // Format messages using instruct formatting | 241 | // Format messages using instruct formatting |
| 236 | const formattedMessages = []; | 242 | const formattedMessages = []; |
| @@ -266,10 +272,9 @@ export class TextCompletionService { | |||
| 266 | formattedMessages.push(messageContent); | 272 | formattedMessages.push(messageContent); |
| 267 | } | 273 | } |
| 268 | requestData.prompt = formattedMessages.join(''); | 274 | requestData.prompt = formattedMessages.join(''); |
| 269 | if (instructPreset.output_suffix) { | 275 | const stoppingStrings = getInstructStoppingSequences({ customInstruct: instructPreset, useStopStrings: false }); |
| 270 | requestData.stop = [instructPreset.output_suffix]; | 276 | requestData.stop = stoppingStrings; |
| 271 | requestData.stopping_strings = [instructPreset.output_suffix]; | 277 | requestData.stopping_strings = stoppingStrings; |
| 272 | } | ||
| 273 | } else { | 278 | } else { |
| 274 | console.warn(`Instruct preset "${instructName}" not found, using basic formatting`); | 279 | console.warn(`Instruct preset "${instructName}" not found, using basic formatting`); |
| 275 | requestData.prompt = prompt.map(x => x.content).join('\n\n'); | 280 | requestData.prompt = prompt.map(x => x.content).join('\n\n'); |
| @@ -283,7 +288,61 @@ export class TextCompletionService { | |||
| 283 | // @ts-ignore | 288 | // @ts-ignore |
| 284 | const data = this.createRequestData(requestData); | 289 | const data = this.createRequestData(requestData); |
| 285 | 290 | ||
| 286 | return await this.sendRequest(data, extractData, signal); | 291 | const response = await this.sendRequest(data, extractData, signal); |
| 292 | // Remove stopping strings from the end | ||
| 293 | if (!data.stream && extractData) { | ||
| 294 | /** @type {ExtractedData} */ | ||
| 295 | // @ts-ignore | ||
| 296 | const extractedData = response; | ||
| 297 | |||
| 298 | let message = extractedData.content; | ||
| 299 | |||
| 300 | message = message.replace(/[^\S\r\n]+$/gm, ''); | ||
| 301 | |||
| 302 | if (requestData.stopping_strings) { | ||
| 303 | for (const stoppingString of requestData.stopping_strings) { | ||
| 304 | if (stoppingString.length) { | ||
| 305 | for (let j = stoppingString.length; j > 0; j--) { | ||
| 306 | if (message.slice(-j) === stoppingString.slice(0, j)) { | ||
| 307 | message = message.slice(0, -j); | ||
| 308 | break; | ||
| 309 | } | ||
| 310 | } | ||
| 311 | } | ||
| 312 | } | ||
| 313 | } | ||
| 314 | |||
| 315 | if (instructPreset) { | ||
| 316 | [ | ||
| 317 | instructPreset.stop_sequence, | ||
| 318 | instructPreset.input_sequence, | ||
| 319 | ].forEach(sequence => { | ||
| 320 | if (sequence?.trim()) { | ||
| 321 | const index = message.indexOf(sequence); | ||
| 322 | if (index !== -1) { | ||
| 323 | message = message.substring(0, index); | ||
| 324 | } | ||
| 325 | } | ||
| 326 | }); | ||
| 327 | |||
| 328 | [ | ||
| 329 | instructPreset.output_sequence, | ||
| 330 | instructPreset.last_output_sequence, | ||
| 331 | ].forEach(sequences => { | ||
| 332 | if (sequences) { | ||
| 333 | sequences.split('\n') | ||
| 334 | .filter(line => line.trim() !== '') | ||
| 335 | .forEach(line => { | ||
| 336 | message = message.replaceAll(line, ''); | ||
| 337 | }); | ||
| 338 | } | ||
| 339 | }); | ||
| 340 | } | ||
| 341 | |||
| 342 | extractedData.content = message; | ||
| 343 | } | ||
| 344 | |||
| 345 | return response; | ||
| 287 | } | 346 | } |
| 288 | 347 | ||
| 289 | /** | 348 | /** |
| @@ -285,6 +285,7 @@ export class ConnectionManagerRequestService { | |||
| 285 | extractData: true, | 285 | extractData: true, |
| 286 | includePreset: true, | 286 | includePreset: true, |
| 287 | includeInstruct: true, | 287 | includeInstruct: true, |
| 288 | instructSettings: {}, | ||
| 288 | }; | 289 | }; |
| 289 | 290 | ||
| 290 | static getAllowedTypes() { | 291 | static getAllowedTypes() { |
| @@ -298,11 +299,17 @@ export class ConnectionManagerRequestService { | |||
| 298 | * @param {string} profileId | 299 | * @param {string} profileId |
| 299 | * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt | 300 | * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt |
| 300 | * @param {number} maxTokens | 301 | * @param {number} maxTokens |
| 301 | * @param {{stream?: boolean, signal?: AbortSignal, extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true | 302 | * @param {Object} custom |
| 303 | * @param {boolean?} [custom.stream=false] | ||
| 304 | * @param {AbortSignal?} [custom.signal] | ||
| 305 | * @param {boolean?} [custom.extractData=true] | ||
| 306 | * @param {boolean?} [custom.includePreset=true] | ||
| 307 | * @param {boolean?} [custom.includeInstruct=true] | ||
| 308 | * @param {Partial<InstructSettings>?} [custom.instructSettings] Override instruct settings | ||
| 302 | * @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 | 309 | * @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 |
| 303 | */ | 310 | */ |
| 304 | static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) { | 311 | static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) { |
| 305 | const { stream, signal, extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom }; | 312 | const { stream, signal, extractData, includePreset, includeInstruct, instructSettings } = { ...this.defaultSendRequestParams, ...custom }; |
| 306 | 313 | ||
| 307 | const context = SillyTavern.getContext(); | 314 | const context = SillyTavern.getContext(); |
| 308 | if (context.extensionSettings.disabledExtensions.includes('connection-manager')) { | 315 | if (context.extensionSettings.disabledExtensions.includes('connection-manager')) { |
| @@ -346,6 +353,7 @@ export class ConnectionManagerRequestService { | |||
| 346 | }, { | 353 | }, { |
| 347 | instructName: includeInstruct ? profile.instruct : undefined, | 354 | instructName: includeInstruct ? profile.instruct : undefined, |
| 348 | presetName: includePreset ? profile.preset : undefined, | 355 | presetName: includePreset ? profile.preset : undefined, |
| 356 | instructSettings: includeInstruct ? instructSettings : undefined, | ||
| 349 | }, extractData, signal); | 357 | }, extractData, signal); |
| 350 | } | 358 | } |
| 351 | default: { | 359 | default: { |
| @@ -243,9 +243,14 @@ export function autoSelectInstructPreset(modelId) { | |||
| 243 | 243 | ||
| 244 | /** | 244 | /** |
| 245 | * Converts instruct mode sequences to an array of stopping strings. | 245 | * Converts instruct mode sequences to an array of stopping strings. |
| 246 | * @param {Object} options | ||
| 247 | * @param {InstructSettings?} [options.customInstruct=null] - Custom instruct settings. | ||
| 248 | * @param {boolean?} [options.useStopStrings] - Decides whether to use "Chat Start" and "Example Separator" | ||
| 246 | * @returns {string[]} Array of instruct mode stopping strings. | 249 | * @returns {string[]} Array of instruct mode stopping strings. |
| 247 | */ | 250 | */ |
| 248 | export function getInstructStoppingSequences() { | 251 | export function getInstructStoppingSequences({ customInstruct = null, useStopStrings = null } = {}) { |
| 252 | const instruct = structuredClone(customInstruct ?? power_user.instruct); | ||
| 253 | |||
| 249 | /** | 254 | /** |
| 250 | * Adds instruct mode sequence to the result array. | 255 | * Adds instruct mode sequence to the result array. |
| 251 | * @param {string} sequence Sequence string. | 256 | * @param {string} sequence Sequence string. |
| @@ -254,7 +259,7 @@ export function getInstructStoppingSequences() { | |||
| 254 | function addInstructSequence(sequence) { | 259 | function addInstructSequence(sequence) { |
| 255 | // Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string | 260 | // Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string |
| 256 | // But it's a problem for Metharme which doesn't use newlines to separate them. | 261 | // But it's a problem for Metharme which doesn't use newlines to separate them. |
| 257 | const wrap = (s) => power_user.instruct.wrap ? '\n' + s : s; | 262 | const wrap = (s) => instruct.wrap ? '\n' + s : s; |
| 258 | // Sequence must be a non-empty string | 263 | // Sequence must be a non-empty string |
| 259 | if (typeof sequence === 'string' && sequence.length > 0) { | 264 | if (typeof sequence === 'string' && sequence.length > 0) { |
| 260 | // If sequence is just a whitespace or newline - we don't want to make it a stopping string | 265 | // 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() { | |||
| 262 | if (sequence.trim().length > 0) { | 267 | if (sequence.trim().length > 0) { |
| 263 | const wrappedSequence = wrap(sequence); | 268 | const wrappedSequence = wrap(sequence); |
| 264 | // Need to respect "insert macro" setting | 269 | // Need to respect "insert macro" setting |
| 265 | const stopString = power_user.instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence; | 270 | const stopString = instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence; |
| 266 | result.push(stopString); | 271 | result.push(stopString); |
| 267 | } | 272 | } |
| 268 | } | 273 | } |
| @@ -270,14 +275,15 @@ export function getInstructStoppingSequences() { | |||
| 270 | 275 | ||
| 271 | const result = []; | 276 | const result = []; |
| 272 | 277 | ||
| 273 | if (power_user.instruct.enabled) { | 278 | // Since preset's don't have "enabled", we assume it's always enabled |
| 274 | const stop_sequence = power_user.instruct.stop_sequence || ''; | 279 | if (customInstruct ?? instruct.enabled) { |
| 275 | const input_sequence = power_user.instruct.input_sequence?.replace(/{{name}}/gi, name1) || ''; | 280 | const stop_sequence = instruct.stop_sequence || ''; |
| 276 | const output_sequence = power_user.instruct.output_sequence?.replace(/{{name}}/gi, name2) || ''; | 281 | const input_sequence = instruct.input_sequence?.replace(/{{name}}/gi, name1) || ''; |
| 277 | const first_output_sequence = power_user.instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || ''; | 282 | const output_sequence = instruct.output_sequence?.replace(/{{name}}/gi, name2) || ''; |
| 278 | const last_output_sequence = power_user.instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || ''; | 283 | const first_output_sequence = instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || ''; |
| 279 | const system_sequence = power_user.instruct.system_sequence?.replace(/{{name}}/gi, 'System') || ''; | 284 | const last_output_sequence = instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || ''; |
| 280 | const last_system_sequence = power_user.instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || ''; | 285 | const system_sequence = instruct.system_sequence?.replace(/{{name}}/gi, 'System') || ''; |
| 286 | const last_system_sequence = instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || ''; | ||
| 281 | 287 | ||
| 282 | const combined_sequence = [ | 288 | const combined_sequence = [ |
| 283 | stop_sequence, | 289 | stop_sequence, |
| @@ -292,7 +298,7 @@ export function getInstructStoppingSequences() { | |||
| 292 | combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence); | 298 | combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence); |
| 293 | } | 299 | } |
| 294 | 300 | ||
| 295 | if (power_user.context.use_stop_strings) { | 301 | if (useStopStrings ?? power_user.context.use_stop_strings) { |
| 296 | if (power_user.context.chat_start) { | 302 | if (power_user.context.chat_start) { |
| 297 | result.push(`\n${substituteParams(power_user.context.chat_start)}`); | 303 | result.push(`\n${substituteParams(power_user.context.chat_start)}`); |
| 298 | } | 304 | } |
| @@ -111,6 +111,7 @@ export function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigate | |||
| 111 | reloadUserAvatar(); | 111 | reloadUserAvatar(); |
| 112 | updatePersonaUIStates({ navigateToCurrent: navigateToCurrent }); | 112 | updatePersonaUIStates({ navigateToCurrent: navigateToCurrent }); |
| 113 | selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange }); | 113 | selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange }); |
| 114 | retriggerFirstMessageOnEmptyChat(); | ||
| 114 | saveSettingsDebounced(); | 115 | saveSettingsDebounced(); |
| 115 | $('.zoomed_avatar[forchar]').remove(); | 116 | $('.zoomed_avatar[forchar]').remove(); |
| 116 | } | 117 | } |
| @@ -465,7 +466,7 @@ export function initPersona(avatarId, personaName, personaDescription) { | |||
| 465 | * @returns {Promise<boolean>} A promise that resolves to true if the character was converted, false otherwise. | 466 | * @returns {Promise<boolean>} A promise that resolves to true if the character was converted, false otherwise. |
| 466 | */ | 467 | */ |
| 467 | export async function convertCharacterToPersona(characterId = null) { | 468 | export async function convertCharacterToPersona(characterId = null) { |
| 468 | if (null === characterId) characterId = this_chid; | 469 | if (null === characterId) characterId = Number(this_chid); |
| 469 | 470 | ||
| 470 | const avatarUrl = characters[characterId]?.avatar; | 471 | const avatarUrl = characters[characterId]?.avatar; |
| 471 | if (!avatarUrl) { | 472 | if (!avatarUrl) { |
| @@ -1243,7 +1244,7 @@ function getPersonaStates(avatarId) { | |||
| 1243 | /** @type {PersonaConnection[]} */ | 1244 | /** @type {PersonaConnection[]} */ |
| 1244 | const connections = power_user.persona_descriptions[avatarId]?.connections; | 1245 | const connections = power_user.persona_descriptions[avatarId]?.connections; |
| 1245 | const hasCharLock = !!connections?.some(c => | 1246 | const hasCharLock = !!connections?.some(c => |
| 1246 | (!selected_group && c.type === 'character' && c.id === characters[this_chid]?.avatar) | 1247 | (!selected_group && c.type === 'character' && c.id === characters[Number(this_chid)]?.avatar) |
| 1247 | || (selected_group && c.type === 'group' && c.id === selected_group)); | 1248 | || (selected_group && c.type === 'group' && c.id === selected_group)); |
| 1248 | 1249 | ||
| 1249 | return { | 1250 | return { |
| @@ -1481,7 +1482,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) { | |||
| 1481 | * @returns {string[]} - An array of persona keys that are connected to the given character key | 1482 | * @returns {string[]} - An array of persona keys that are connected to the given character key |
| 1482 | */ | 1483 | */ |
| 1483 | export function getConnectedPersonas(characterKey = undefined) { | 1484 | export function getConnectedPersonas(characterKey = undefined) { |
| 1484 | characterKey ??= selected_group || characters[this_chid]?.avatar; | 1485 | characterKey ??= selected_group || characters[Number(this_chid)]?.avatar; |
| 1485 | const connectedPersonas = Object.entries(power_user.persona_descriptions) | 1486 | const connectedPersonas = Object.entries(power_user.persona_descriptions) |
| 1486 | .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) | 1487 | .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) |
| 1487 | .map(([key, _]) => key); | 1488 | .map(([key, _]) => key); |
| @@ -1513,7 +1514,7 @@ export async function showCharConnections() { | |||
| 1513 | console.log(`Unlocking persona ${personaId} from current character ${name2}`); | 1514 | console.log(`Unlocking persona ${personaId} from current character ${name2}`); |
| 1514 | power_user.persona_descriptions[personaId].connections = connections.filter(c => { | 1515 | power_user.persona_descriptions[personaId].connections = connections.filter(c => { |
| 1515 | if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false; | 1516 | if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false; |
| 1516 | else if (c.type == 'character' && c.id == characters[this_chid]?.avatar) return false; | 1517 | else if (c.type == 'character' && c.id == characters[Number(this_chid)]?.avatar) return false; |
| 1517 | return true; | 1518 | return true; |
| 1518 | }); | 1519 | }); |
| 1519 | saveSettingsDebounced(); | 1520 | saveSettingsDebounced(); |
| @@ -1545,8 +1546,8 @@ export async function showCharConnections() { | |||
| 1545 | export function getCurrentConnectionObj() { | 1546 | export function getCurrentConnectionObj() { |
| 1546 | if (selected_group) | 1547 | if (selected_group) |
| 1547 | return { type: 'group', id: selected_group }; | 1548 | return { type: 'group', id: selected_group }; |
| 1548 | if (characters[this_chid]?.avatar) | 1549 | if (characters[Number(this_chid)]?.avatar) |
| 1549 | return { type: 'character', id: characters[this_chid]?.avatar }; | 1550 | return { type: 'character', id: characters[Number(this_chid)]?.avatar }; |
| 1550 | return null; | 1551 | return null; |
| 1551 | } | 1552 | } |
| 1552 | 1553 | ||
| @@ -1664,7 +1665,7 @@ async function syncUserNameToPersona() { | |||
| 1664 | * Only works if only the first message is present, and not in group mode. | 1665 | * Only works if only the first message is present, and not in group mode. |
| 1665 | */ | 1666 | */ |
| 1666 | export function retriggerFirstMessageOnEmptyChat() { | 1667 | export function retriggerFirstMessageOnEmptyChat() { |
| 1667 | if (this_chid >= 0 && !selected_group && chat.length === 1) { | 1668 | if (Number(this_chid) >= 0 && !selected_group && chat.length === 1) { |
| 1668 | $('#firstmessage_textarea').trigger('input'); | 1669 | $('#firstmessage_textarea').trigger('input'); |
| 1669 | } | 1670 | } |
| 1670 | } | 1671 | } |
| @@ -1782,7 +1783,6 @@ function setNameCallback({ mode = 'all' }, name) { | |||
| 1782 | if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; | 1783 | if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; |
| 1783 | if (persona) { | 1784 | if (persona) { |
| 1784 | autoSelectPersona(persona); | 1785 | autoSelectPersona(persona); |
| 1785 | retriggerFirstMessageOnEmptyChat(); | ||
| 1786 | return ''; | 1786 | return ''; |
| 1787 | } else if (mode === 'lookup') { | 1787 | } else if (mode === 'lookup') { |
| 1788 | toastr.warning(`Persona ${name} not found`); | 1788 | toastr.warning(`Persona ${name} not found`); |
| @@ -1793,7 +1793,6 @@ function setNameCallback({ mode = 'all' }, name) { | |||
| 1793 | if (['temp', 'all'].includes(mode)) { | 1793 | if (['temp', 'all'].includes(mode)) { |
| 1794 | // Otherwise, set just the name | 1794 | // Otherwise, set just the name |
| 1795 | setUserName(name); //this prevented quickReply usage | 1795 | setUserName(name); //this prevented quickReply usage |
| 1796 | retriggerFirstMessageOnEmptyChat(); | ||
| 1797 | } | 1796 | } |
| 1798 | 1797 | ||
| 1799 | return ''; | 1798 | return ''; |
| @@ -1944,9 +1943,6 @@ export async function initPersonas() { | |||
| 1944 | $(document).on('click', '#user_avatar_block .avatar-container', function () { | 1943 | $(document).on('click', '#user_avatar_block .avatar-container', function () { |
| 1945 | const imgfile = $(this).attr('data-avatar-id'); | 1944 | const imgfile = $(this).attr('data-avatar-id'); |
| 1946 | setUserAvatar(imgfile); | 1945 | setUserAvatar(imgfile); |
| 1947 | |||
| 1948 | // force firstMes {{user}} update on persona switch | ||
| 1949 | retriggerFirstMessageOnEmptyChat(); | ||
| 1950 | }); | 1946 | }); |
| 1951 | 1947 | ||
| 1952 | $('#persona_rename_button').on('click', () => renamePersona(user_avatar)); | 1948 | $('#persona_rename_button').on('click', () => renamePersona(user_avatar)); |
| @@ -1979,4 +1975,3 @@ export async function initPersonas() { | |||
| 1979 | eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat); | 1975 | eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat); |
| 1980 | switchPersonaGridView(); | 1976 | switchPersonaGridView(); |
| 1981 | } | 1977 | } |
| 1982 | |||
| @@ -1,4 +0,0 @@ | |||
| 1 | <div class="row"> | ||
| 2 | <a class="export-promptmanager-prompts-character list-group-item" data-i18n="Export for character">Export for character</a> | ||
| 3 | <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> | ||
| 4 | </div> | ||
| @@ -1,12 +0,0 @@ | |||
| 1 | <div id="prompt-manager-export-format-popup" class="list-group"> | ||
| 2 | <div class="prompt-manager-export-format-popup-flex"> | ||
| 3 | <div class="row"> | ||
| 4 | <a class="export-promptmanager-prompts-full list-group-item" data-i18n="Export all">Export all</a> | ||
| 5 | <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> | ||
| 6 | </div> | ||
| 7 | {{#if isGlobalStrategy}} | ||
| 8 | {{else}} | ||
| 9 | {{{exportForCharacter}}} | ||
| 10 | {{/if}} | ||
| 11 | </div> | ||
| 12 | </div> | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | <div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 4.5em;"> | 1 | <div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 7.0em;"> |
| 2 | <small class="flex1" data-i18n="Title/Memo">Title/Memo</small> | 2 | <small class="flex1" data-i18n="Title/Memo">Title/Memo</small> |
| 3 | <small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small> | 3 | <small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small> |
| 4 | <small style="width: calc(3.5em + 20px)" data-i18n="Position">Position</small> | 4 | <small style="width: calc(3.5em + 20px)" data-i18n="Position">Position</small> |
| @@ -2208,7 +2208,7 @@ function verifyWorldInfoSearchSortRule() { | |||
| 2208 | * Use `originalWIDataKeyMap` to find the correct value to be set. | 2208 | * Use `originalWIDataKeyMap` to find the correct value to be set. |
| 2209 | * | 2209 | * |
| 2210 | * @param {object} data - The data object containing the original data entries. | 2210 | * @param {object} data - The data object containing the original data entries. |
| 2211 | * @param {string} uid - The unique identifier of the data entry. | 2211 | * @param {number} uid - The unique identifier of the data entry. |
| 2212 | * @param {string} key - The key of the value to be set. | 2212 | * @param {string} key - The key of the value to be set. |
| 2213 | * @param {any} value - The value to be set. | 2213 | * @param {any} value - The value to be set. |
| 2214 | */ | 2214 | */ |
| @@ -2232,7 +2232,9 @@ export function setWIOriginalDataValue(data, uid, key, value) { | |||
| 2232 | */ | 2232 | */ |
| 2233 | export function deleteWIOriginalDataValue(data, uid) { | 2233 | export function deleteWIOriginalDataValue(data, uid) { |
| 2234 | if (data.originalData && Array.isArray(data.originalData.entries)) { | 2234 | if (data.originalData && Array.isArray(data.originalData.entries)) { |
| 2235 | const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid); | 2235 | // Non-strict equality is used here to allow for both string and number comparisons |
| 2236 | // @eslint-disable-next-line eqeqeq | ||
| 2237 | const originalIndex = data.originalData.entries.findIndex(x => x.uid == uid); | ||
| 2236 | 2238 | ||
| 2237 | if (originalIndex >= 0) { | 2239 | if (originalIndex >= 0) { |
| 2238 | data.originalData.entries.splice(originalIndex, 1); | 2240 | data.originalData.entries.splice(originalIndex, 1); |
| @@ -3143,6 +3145,84 @@ export async function getWorldEntry(name, data, entry) { | |||
| 3143 | updateEditor(navigation_option.previous); | 3145 | updateEditor(navigation_option.previous); |
| 3144 | }); | 3146 | }); |
| 3145 | 3147 | ||
| 3148 | // move button | ||
| 3149 | const moveButton = template.find('.move_entry_button'); | ||
| 3150 | moveButton.attr('data-uid', entry.uid); | ||
| 3151 | moveButton.attr('data-current-world', name); | ||
| 3152 | moveButton.on('click', async function (e) { | ||
| 3153 | e.stopPropagation(); | ||
| 3154 | const sourceUid = $(this).attr('data-uid'); | ||
| 3155 | const sourceWorld = $(this).attr('data-current-world'); | ||
| 3156 | const sourceWorldInfo = await loadWorldInfo(sourceWorld); | ||
| 3157 | if (!sourceWorldInfo) { | ||
| 3158 | return; | ||
| 3159 | } | ||
| 3160 | const sourceName = sourceWorldInfo.entries[sourceUid]?.comment; | ||
| 3161 | if (sourceName === undefined) { | ||
| 3162 | return; | ||
| 3163 | } | ||
| 3164 | |||
| 3165 | const select = document.createElement('select'); | ||
| 3166 | select.id = 'move_entry_target_select'; | ||
| 3167 | select.classList.add('text_pole', 'wide100p', 'marginTop10'); | ||
| 3168 | |||
| 3169 | const defaultOption = document.createElement('option'); | ||
| 3170 | defaultOption.value = ''; | ||
| 3171 | defaultOption.textContent = `-- ${t`Select Target Lorebook`} --`; | ||
| 3172 | select.appendChild(defaultOption); | ||
| 3173 | |||
| 3174 | let selectableWorldCount = 0; | ||
| 3175 | world_names.forEach(worldName => { | ||
| 3176 | if (worldName !== sourceWorld) { // Exclude current world | ||
| 3177 | const option = document.createElement('option'); | ||
| 3178 | option.value = world_names.indexOf(worldName).toString(); | ||
| 3179 | option.textContent = worldName; | ||
| 3180 | select.appendChild(option); | ||
| 3181 | selectableWorldCount++; | ||
| 3182 | } | ||
| 3183 | }); | ||
| 3184 | |||
| 3185 | if (selectableWorldCount === 0) { | ||
| 3186 | toastr.warning(t`There are no other lorebooks to move to.`); | ||
| 3187 | return; | ||
| 3188 | } | ||
| 3189 | |||
| 3190 | // Create wrapper div | ||
| 3191 | const wrapper = document.createElement('div'); | ||
| 3192 | wrapper.textContent = t`Move "${sourceName}" to:`; | ||
| 3193 | |||
| 3194 | // Create container and append elements | ||
| 3195 | const container = document.createElement('div'); | ||
| 3196 | container.appendChild(wrapper); | ||
| 3197 | container.appendChild(select); | ||
| 3198 | |||
| 3199 | let selectedWorldIndex = -1; | ||
| 3200 | select.addEventListener('change', function() { | ||
| 3201 | selectedWorldIndex = this.value === '' ? -1 : Number(this.value); | ||
| 3202 | }); | ||
| 3203 | |||
| 3204 | const popupConfirm = await callGenericPopup(container, POPUP_TYPE.CONFIRM, '', { | ||
| 3205 | okButton: t`Move`, | ||
| 3206 | cancelButton: t`Cancel`, | ||
| 3207 | }); | ||
| 3208 | if (!popupConfirm) { | ||
| 3209 | return; | ||
| 3210 | } | ||
| 3211 | |||
| 3212 | if (selectedWorldIndex === -1) { | ||
| 3213 | return; | ||
| 3214 | } | ||
| 3215 | |||
| 3216 | const selectedValue = world_names[selectedWorldIndex]; | ||
| 3217 | |||
| 3218 | if (!selectedValue) { | ||
| 3219 | toastr.warning(t`Please select a target lorebook.`); | ||
| 3220 | return; | ||
| 3221 | } | ||
| 3222 | |||
| 3223 | await moveWorldInfoEntry(sourceWorld, selectedValue, sourceUid); | ||
| 3224 | }); | ||
| 3225 | |||
| 3146 | // scan depth | 3226 | // scan depth |
| 3147 | const scanDepthInput = template.find('input[name="scanDepth"]'); | 3227 | const scanDepthInput = template.find('input[name="scanDepth"]'); |
| 3148 | scanDepthInput.data('uid', entry.uid); | 3228 | scanDepthInput.data('uid', entry.uid); |
| @@ -5271,3 +5351,99 @@ jQuery(() => { | |||
| 5271 | }); | 5351 | }); |
| 5272 | }); | 5352 | }); |
| 5273 | }); | 5353 | }); |
| 5354 | |||
| 5355 | /** | ||
| 5356 | * Moves a World Info entry from a source lorebook to a target lorebook. | ||
| 5357 | * | ||
| 5358 | * @param {string} sourceName - The name of the source lorebook file. | ||
| 5359 | * @param {string} targetName - The name of the target lorebook file. | ||
| 5360 | * @param {string|number} uid - The UID of the entry to move from the source lorebook. | ||
| 5361 | * @returns {Promise<boolean>} True if the move was successful, false otherwise. | ||
| 5362 | */ | ||
| 5363 | export async function moveWorldInfoEntry(sourceName, targetName, uid) { | ||
| 5364 | if (sourceName === targetName) { | ||
| 5365 | return false; | ||
| 5366 | } | ||
| 5367 | |||
| 5368 | if (!world_names.includes(sourceName)) { | ||
| 5369 | toastr.error(t`Source lorebook '${sourceName}' not found.`); | ||
| 5370 | console.error(`[WI Move] Source lorebook '${sourceName}' does not exist.`); | ||
| 5371 | return false; | ||
| 5372 | } | ||
| 5373 | |||
| 5374 | if (!world_names.includes(targetName)) { | ||
| 5375 | toastr.error(t`Target lorebook '${targetName}' not found.`); | ||
| 5376 | console.error(`[WI Move] Target lorebook '${targetName}' does not exist.`); | ||
| 5377 | return false; | ||
| 5378 | } | ||
| 5379 | |||
| 5380 | const entryUidString = String(uid); | ||
| 5381 | |||
| 5382 | try { | ||
| 5383 | const sourceData = await loadWorldInfo(sourceName); | ||
| 5384 | const targetData = await loadWorldInfo(targetName); | ||
| 5385 | |||
| 5386 | if (!sourceData || !sourceData.entries) { | ||
| 5387 | toastr.error(t`Failed to load data for source lorebook '${sourceName}'.`); | ||
| 5388 | console.error(`[WI Move] Could not load source data for '${sourceName}'.`); | ||
| 5389 | return false; | ||
| 5390 | } | ||
| 5391 | if (!targetData || !targetData.entries) { | ||
| 5392 | toastr.error(t`Failed to load data for target lorebook '${targetName}'.`); | ||
| 5393 | console.error(`[WI Move] Could not load target data for '${targetName}'.`); | ||
| 5394 | return false; | ||
| 5395 | } | ||
| 5396 | |||
| 5397 | if (!sourceData.entries[entryUidString]) { | ||
| 5398 | toastr.error(t`Entry not found in source lorebook '${sourceName}'.`); | ||
| 5399 | console.error(`[WI Move] Entry UID ${entryUidString} not found in '${sourceName}'.`); | ||
| 5400 | return false; | ||
| 5401 | } | ||
| 5402 | |||
| 5403 | const entryToMove = structuredClone(sourceData.entries[entryUidString]); | ||
| 5404 | |||
| 5405 | |||
| 5406 | const newUid = getFreeWorldEntryUid(targetData); | ||
| 5407 | if (newUid === null) { | ||
| 5408 | console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`); | ||
| 5409 | return false; | ||
| 5410 | } | ||
| 5411 | |||
| 5412 | entryToMove.uid = newUid; | ||
| 5413 | // Place the entry at the end of the target lorebook | ||
| 5414 | const maxDisplayIndex = Object.values(targetData.entries).reduce((max, entry) => Math.max(max, entry.displayIndex ?? -1), -1); | ||
| 5415 | entryToMove.displayIndex = maxDisplayIndex + 1; | ||
| 5416 | |||
| 5417 | targetData.entries[newUid] = entryToMove; | ||
| 5418 | |||
| 5419 | delete sourceData.entries[entryUidString]; | ||
| 5420 | // Remove from originalData if it exists | ||
| 5421 | deleteWIOriginalDataValue(sourceData, entryUidString); | ||
| 5422 | // TODO: setWIOriginalDataValue | ||
| 5423 | console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`); | ||
| 5424 | |||
| 5425 | |||
| 5426 | await saveWorldInfo(targetName, targetData, true); | ||
| 5427 | console.debug(`[WI Move] Saved target lorebook '${targetName}'.`); | ||
| 5428 | await saveWorldInfo(sourceName, sourceData, true); | ||
| 5429 | console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`); | ||
| 5430 | |||
| 5431 | |||
| 5432 | console.log(`[WI Move] ${entryToMove.comment} moved successfully to '${targetName}'.`); | ||
| 5433 | |||
| 5434 | // Check if the currently viewed book in the editor is the source or target and reload it | ||
| 5435 | const currentEditorBookIndex = Number($('#world_editor_select').val()); | ||
| 5436 | if (!isNaN(currentEditorBookIndex)) { | ||
| 5437 | const currentEditorBookName = world_names[currentEditorBookIndex]; | ||
| 5438 | if (currentEditorBookName === sourceName || currentEditorBookName === targetName) { | ||
| 5439 | reloadEditor(currentEditorBookName); | ||
| 5440 | } | ||
| 5441 | } | ||
| 5442 | |||
| 5443 | return true; | ||
| 5444 | } catch (error) { | ||
| 5445 | toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`); | ||
| 5446 | console.error('[WI Move] Unexpected error:', error); | ||
| 5447 | return false; | ||
| 5448 | } | ||
| 5449 | } | ||