Add new WI position 'outlet' to expose WI entries triggered to be placed as macros literally anywhere (#4523) * feat: add outlet prompt support for world info entries * feat: add outlet name field with autocomplete for world info entries * feat: add outlet entries for world info support to WI parsing * feat: add tooltip and validation for WI outlet name entries * Trim macro outlet name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor: consolidate WI injection cleanup and fix strict equality comparison * fix: update outlet macro regex pattern to use double colon delimiter * fix: replace carriage returns with newlines in WI entry outlet tooltip * fix: add null checks and array validation for WI outlet entries handling * feat: add outletName field to world info entry definition schema (supports /setwientryfield command) * fix: add outletName field to export of character lore books * do not remove outlet name on position change, but filter out for auto complete * Match WI extensions field name Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> * outlet name being used in char WI import too * docs: add outlet macro documentation to help template * be explicit about risu/novel/agnai wi not supporting outlets * Inject A/N and TC sysprompt after populating outlets --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -301,6 +301,10 @@ select.keyselect+span.select2-container .select2-selection--multiple { | |||
| 301 | display: none; | 301 | display: none; |
| 302 | } | 302 | } |
| 303 | 303 | ||
| 304 | .world_entry:not(:has(select[name="position"] option[value="7"]:checked)) .world_entry_form_control:has(input[name="outletName"]) { | ||
| 305 | display: none; | ||
| 306 | } | ||
| 307 | |||
| 304 | .world_entry label[for="__invisible"] { | 308 | .world_entry label[for="__invisible"] { |
| 305 | visibility: hidden; | 309 | visibility: hidden; |
| 306 | pointer-events: none; | 310 | pointer-events: none; |
| @@ -6476,6 +6476,13 @@ | |||
| 6476 | </div> | 6476 | </div> |
| 6477 | </div> | 6477 | </div> |
| 6478 | <div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter"> | 6478 | <div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter"> |
| 6479 | <div class="world_entry_form_control flex1" title="Set the outlet name for this WI entry. WI entries with position 'outlet' will not be added to the prompt automatically. Instead, they will be collected and can be used as a macro in the prompt. Add {{outlet::YourName}} to the prompt in any place you want to add all WI entries this specific outlet." data-i18n="[title]wi_outlet_name"> | ||
| 6480 | <small class="textAlignCenter"> | ||
| 6481 | <span data-i18n="Outlet Name">Outlet Name</span> | ||
| 6482 | <div class="fa-solid fa-circle-info opacity50p"></div> | ||
| 6483 | </small> | ||
| 6484 | <input class="text_pole margin0" name="outletName" type="text" placeholder="Outlet Name" data-i18n="[placeholder]Outlet Name"> | ||
| 6485 | </div> | ||
| 6479 | <div class="world_entry_form_control flex1"> | 6486 | <div class="world_entry_form_control flex1"> |
| 6480 | <small class="textAlignCenter" data-i18n="Scan Depth">Scan Depth</small> | 6487 | <small class="textAlignCenter" data-i18n="Scan Depth">Scan Depth</small> |
| 6481 | <input class="text_pole margin0" name="scanDepth" type="number" placeholder="Use global setting" data-i18n="[placeholder]Use global setting" max="1000"> | 6488 | <input class="text_pole margin0" name="scanDepth" type="number" placeholder="Use global setting" data-i18n="[placeholder]Use global setting" max="1000"> |
| @@ -6813,6 +6820,9 @@ | |||
| 6813 | <option value="4" data-role="2" data-i18n="at Depth AI"> | 6820 | <option value="4" data-role="2" data-i18n="at Depth AI"> |
| 6814 | @D 🤖 | 6821 | @D 🤖 |
| 6815 | </option> | 6822 | </option> |
| 6823 | <option value="7" data-role="" data-i18n="Outlet"> | ||
| 6824 | ➡️ Outlet | ||
| 6825 | </option> | ||
| 6816 | </select> | 6826 | </select> |
| 6817 | </div> | 6827 | </div> |
| 6818 | <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap"> | 6828 | <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap"> |
| @@ -3575,18 +3575,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3575 | creatorNotes, | 3575 | creatorNotes, |
| 3576 | } = getCharacterCardFields(); | 3576 | } = getCharacterCardFields(); |
| 3577 | 3577 | ||
| 3578 | if (main_api !== 'openai') { | ||
| 3579 | if (power_user.sysprompt.enabled) { | ||
| 3580 | system = power_user.prefer_character_prompt && system | ||
| 3581 | ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? '')) | ||
| 3582 | : baseChatReplace(power_user.sysprompt.content, name1, name2); | ||
| 3583 | system = isInstruct ? substituteParams(system, name1, name2, power_user.sysprompt.content) : system; | ||
| 3584 | } else { | ||
| 3585 | // Nullify if it's not enabled | ||
| 3586 | system = ''; | ||
| 3587 | } | ||
| 3588 | } | ||
| 3589 | |||
| 3590 | // Depth prompt (character-specific A/N) | 3578 | // Depth prompt (character-specific A/N) |
| 3591 | removeDepthPrompts(); | 3579 | removeDepthPrompts(); |
| 3592 | const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid)); | 3580 | const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid)); |
| @@ -3717,13 +3705,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3717 | 3705 | ||
| 3718 | let mesExamplesArray = parseMesExamples(mesExamples, isInstruct); | 3706 | let mesExamplesArray = parseMesExamples(mesExamples, isInstruct); |
| 3719 | 3707 | ||
| 3720 | ////////////////////////////////// | ||
| 3721 | // Extension added strings | ||
| 3722 | // Set non-WI AN | ||
| 3723 | setFloatingPrompt(); | ||
| 3724 | // Add persona description to prompt | ||
| 3725 | addPersonaDescriptionExtensionPrompt(); | ||
| 3726 | |||
| 3727 | // Add WI to prompt (and also inject WI to AN value via hijack) | 3708 | // Add WI to prompt (and also inject WI to AN value via hijack) |
| 3728 | // Make quiet prompt available for WIAN | 3709 | // Make quiet prompt available for WIAN |
| 3729 | setExtensionPrompt(inject_ids.QUIET_PROMPT, quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true); | 3710 | setExtensionPrompt(inject_ids.QUIET_PROMPT, quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true); |
| @@ -3738,7 +3719,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3738 | creatorNotes: creatorNotes, | 3719 | creatorNotes: creatorNotes, |
| 3739 | trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal', | 3720 | trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal', |
| 3740 | }; | 3721 | }; |
| 3741 | const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData); | 3722 | const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth, outletEntries } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData); |
| 3742 | setExtensionPrompt(inject_ids.QUIET_PROMPT, '', extension_prompt_types.IN_PROMPT, 0, true); | 3723 | setExtensionPrompt(inject_ids.QUIET_PROMPT, '', extension_prompt_types.IN_PROMPT, 0, true); |
| 3743 | 3724 | ||
| 3744 | // Add message example WI | 3725 | // Add message example WI |
| @@ -3770,17 +3751,41 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3770 | if (skipWIAN !== true) { | 3751 | if (skipWIAN !== true) { |
| 3771 | console.log('skipWIAN not active, adding WIAN'); | 3752 | console.log('skipWIAN not active, adding WIAN'); |
| 3772 | // Add all depth WI entries to prompt | 3753 | // Add all depth WI entries to prompt |
| 3773 | flushWIDepthInjections(); | 3754 | flushWIInjections(); |
| 3774 | if (Array.isArray(worldInfoDepth)) { | 3755 | if (Array.isArray(worldInfoDepth)) { |
| 3775 | worldInfoDepth.forEach((e) => { | 3756 | worldInfoDepth.forEach((e) => { |
| 3776 | const joinedEntries = e.entries.join('\n'); | 3757 | const joinedEntries = e.entries.join('\n'); |
| 3777 | setExtensionPrompt(inject_ids.CUSTOM_WI_DEPTH_ROLE(e.depth, e.role), joinedEntries, extension_prompt_types.IN_CHAT, e.depth, false, e.role); | 3758 | setExtensionPrompt(inject_ids.CUSTOM_WI_DEPTH_ROLE(e.depth, e.role), joinedEntries, extension_prompt_types.IN_CHAT, e.depth, false, e.role); |
| 3778 | }); | 3759 | }); |
| 3779 | } | 3760 | } |
| 3761 | if (outletEntries && typeof outletEntries === 'object' && Object.keys(outletEntries).length > 0) { | ||
| 3762 | Object.entries(outletEntries).forEach(([key, value]) => { | ||
| 3763 | setExtensionPrompt(inject_ids.CUSTOM_WI_OUTLET(key), value.join('\n'), extension_prompt_types.NONE, 0); | ||
| 3764 | }); | ||
| 3765 | } | ||
| 3780 | } else { | 3766 | } else { |
| 3781 | console.log('skipping WIAN'); | 3767 | console.log('skipping WIAN'); |
| 3782 | } | 3768 | } |
| 3783 | 3769 | ||
| 3770 | // Set non-WI AN | ||
| 3771 | setFloatingPrompt(); | ||
| 3772 | |||
| 3773 | // Add persona description to prompt | ||
| 3774 | addPersonaDescriptionExtensionPrompt(); | ||
| 3775 | |||
| 3776 | // Prepare the system prompt for Text Completion APIs | ||
| 3777 | if (main_api !== 'openai') { | ||
| 3778 | if (power_user.sysprompt.enabled) { | ||
| 3779 | system = power_user.prefer_character_prompt && system | ||
| 3780 | ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? '')) | ||
| 3781 | : baseChatReplace(power_user.sysprompt.content, name1, name2); | ||
| 3782 | system = isInstruct ? substituteParams(system, name1, name2, power_user.sysprompt.content) : system; | ||
| 3783 | } else { | ||
| 3784 | // Nullify if it's not enabled | ||
| 3785 | system = ''; | ||
| 3786 | } | ||
| 3787 | } | ||
| 3788 | |||
| 3784 | // Collect before / after story string injections | 3789 | // Collect before / after story string injections |
| 3785 | const beforeScenarioAnchor = await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT); | 3790 | const beforeScenarioAnchor = await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT); |
| 3786 | const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT); | 3791 | const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT); |
| @@ -4751,10 +4756,12 @@ async function doChatInject(messages, isContinue) { | |||
| 4751 | return injectedIndices; | 4756 | return injectedIndices; |
| 4752 | } | 4757 | } |
| 4753 | 4758 | ||
| 4754 | function flushWIDepthInjections() { | 4759 | function flushWIInjections() { |
| 4755 | //prevent custom depth WI entries (which have unique random key names) from duplicating | 4760 | const depthPrefix = inject_ids.CUSTOM_WI_DEPTH; |
| 4761 | const outletPrefix = inject_ids.CUSTOM_WI_OUTLET(''); | ||
| 4762 | |||
| 4756 | for (const key of Object.keys(extension_prompts)) { | 4763 | for (const key of Object.keys(extension_prompts)) { |
| 4757 | if (key.startsWith(inject_ids.CUSTOM_WI_DEPTH)) { | 4764 | if (key.startsWith(depthPrefix) || key.startsWith(outletPrefix)) { |
| 4758 | delete extension_prompts[key]; | 4765 | delete extension_prompts[key]; |
| 4759 | } | 4766 | } |
| 4760 | } | 4767 | } |
| @@ -4775,7 +4782,7 @@ function unblockGeneration(type) { | |||
| 4775 | showSwipeButtons(); | 4782 | showSwipeButtons(); |
| 4776 | setGenerationProgress(0); | 4783 | setGenerationProgress(0); |
| 4777 | flushEphemeralStoppingStrings(); | 4784 | flushEphemeralStoppingStrings(); |
| 4778 | flushWIDepthInjections(); | 4785 | flushWIInjections(); |
| 4779 | } | 4786 | } |
| 4780 | 4787 | ||
| 4781 | export function getNextMessageId(type) { | 4788 | export function getNextMessageId(type) { |
| @@ -51,6 +51,7 @@ export const inject_ids = { | |||
| 51 | DEPTH_PROMPT_INDEX: (index) => `DEPTH_PROMPT_${index}`, | 51 | DEPTH_PROMPT_INDEX: (index) => `DEPTH_PROMPT_${index}`, |
| 52 | CUSTOM_WI_DEPTH: 'customDepthWI', | 52 | CUSTOM_WI_DEPTH: 'customDepthWI', |
| 53 | CUSTOM_WI_DEPTH_ROLE: (depth, role) => `customDepthWI_${depth}_${role}`, | 53 | CUSTOM_WI_DEPTH_ROLE: (depth, role) => `customDepthWI_${depth}_${role}`, |
| 54 | CUSTOM_WI_OUTLET: (key) => `customWIOutlet_${key}`, | ||
| 54 | }; | 55 | }; |
| 55 | 56 | ||
| 56 | export const COMETAPI_IGNORE_PATTERNS = [ | 57 | export const COMETAPI_IGNORE_PATTERNS = [ |
| @@ -1,10 +1,11 @@ | |||
| 1 | import { Handlebars, moment, seedrandom, droll } from '../lib.js'; | 1 | import { Handlebars, moment, seedrandom, droll } from '../lib.js'; |
| 2 | import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams, eventSource, event_types } from '../script.js'; | 2 | import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams, eventSource, event_types, extension_prompts } from '../script.js'; |
| 3 | import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; | 3 | import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; |
| 4 | import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; | 4 | import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; |
| 5 | import { getInstructMacros } from './instruct-mode.js'; | 5 | import { getInstructMacros } from './instruct-mode.js'; |
| 6 | import { getVariableMacros } from './variables.js'; | 6 | import { getVariableMacros } from './variables.js'; |
| 7 | import { isMobile } from './RossAscends-mods.js'; | 7 | import { isMobile } from './RossAscends-mods.js'; |
| 8 | import { inject_ids } from './constants.js'; | ||
| 8 | 9 | ||
| 9 | /** | 10 | /** |
| 10 | * @typedef Macro | 11 | * @typedef Macro |
| @@ -459,6 +460,17 @@ function getTimeDiffMacro() { | |||
| 459 | } | 460 | } |
| 460 | 461 | ||
| 461 | /** | 462 | /** |
| 463 | * Returns the outlet prompt for a given outlet key. | ||
| 464 | * @param {string} key - The outlet key | ||
| 465 | * @returns {string} The outlet prompt | ||
| 466 | */ | ||
| 467 | function getOutletPrompt(key) { | ||
| 468 | const value = extension_prompts[inject_ids.CUSTOM_WI_OUTLET(key)]?.value; | ||
| 469 | // Macros should already be parsed, but we'll do it again just in case | ||
| 470 | return substituteParams(value); | ||
| 471 | } | ||
| 472 | |||
| 473 | /** | ||
| 462 | * Substitutes {{macro}} parameters in a string. | 474 | * Substitutes {{macro}} parameters in a string. |
| 463 | * @param {string} content - The string to substitute parameters in. | 475 | * @param {string} content - The string to substitute parameters in. |
| 464 | * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param | 476 | * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param |
| @@ -518,6 +530,7 @@ export function evaluateMacros(content, env, postProcessFn) { | |||
| 518 | { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) }, | 530 | { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) }, |
| 519 | { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() }, | 531 | { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() }, |
| 520 | { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') }, | 532 | { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') }, |
| 533 | { regex: /{{outlet::(.+?)}}/gi, replace: (_, key) => getOutletPrompt(key.trim()) || '' }, | ||
| 521 | getTimeDiffMacro(), | 534 | getTimeDiffMacro(), |
| 522 | getBannedWordsMacro(), | 535 | getBannedWordsMacro(), |
| 523 | getRandomReplaceMacro(), | 536 | getRandomReplaceMacro(), |
| @@ -22,6 +22,7 @@ | |||
| 22 | <li><tt>{{char}}</tt> – <span data-i18n="help_macros_16">the Character's name</span></li> | 22 | <li><tt>{{char}}</tt> – <span data-i18n="help_macros_16">the Character's name</span></li> |
| 23 | <li><tt>{{version}}</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li> | 23 | <li><tt>{{version}}</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li> |
| 24 | <li><tt>{{charDepthPrompt}}</tt> – <span data-i18n="help_macros_charDepthPrompt">the Character's @ Depth Note</span></li> | 24 | <li><tt>{{charDepthPrompt}}</tt> – <span data-i18n="help_macros_charDepthPrompt">the Character's @ Depth Note</span></li> |
| 25 | <li><tt>{{outlet::(name)}}</tt> – <span data-i18n="help_macros_outletName">the WI entry content for the outlet with the specified name</span></li> | ||
| 25 | <li><tt>{{group}}</tt> – <span data-i18n="help_macros_18">a comma-separated list of group member names (including muted) or the character name in solo chats. Alias: {{charIfNotGroup}}</span></li> | 26 | <li><tt>{{group}}</tt> – <span data-i18n="help_macros_18">a comma-separated list of group member names (including muted) or the character name in solo chats. Alias: {{charIfNotGroup}}</span></li> |
| 26 | <li><tt>{{groupNotMuted}}</tt> – <span data-i18n="help_groupNotMuted">the same as {{group}}, but excludes muted members</span></li> | 27 | <li><tt>{{groupNotMuted}}</tt> – <span data-i18n="help_groupNotMuted">the same as {{group}}, but excludes muted members</span></li> |
| 27 | <li><tt>{{model}}</tt> – <span data-i18n="help_macros_19">a text generation model name for the currently selected API. </span><b data-i18n="Can be inaccurate!">Can be inaccurate!</b></li> | 28 | <li><tt>{{model}}</tt> – <span data-i18n="help_macros_19">a text generation model name for the currently selected API. </span><b data-i18n="Can be inaccurate!">Can be inaccurate!</b></li> |
| @@ -156,6 +156,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; | |||
| 156 | * @property {Array} worldInfoDepth - Array of depth entries | 156 | * @property {Array} worldInfoDepth - Array of depth entries |
| 157 | * @property {Array} anBefore - Array of entries before Author's Note | 157 | * @property {Array} anBefore - Array of entries before Author's Note |
| 158 | * @property {Array} anAfter - Array of entries after Author's Note | 158 | * @property {Array} anAfter - Array of entries after Author's Note |
| 159 | * @property {{[key: string]: string[]}} outletEntries - Array of entries to be added to an outlet | ||
| 159 | */ | 160 | */ |
| 160 | 161 | ||
| 161 | /** | 162 | /** |
| @@ -166,6 +167,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; | |||
| 166 | * @property {any[]} WIDepthEntries The depth entries. | 167 | * @property {any[]} WIDepthEntries The depth entries. |
| 167 | * @property {any[]} ANBeforeEntries The entries before Author's Note. | 168 | * @property {any[]} ANBeforeEntries The entries before Author's Note. |
| 168 | * @property {any[]} ANAfterEntries The entries after Author's Note. | 169 | * @property {any[]} ANAfterEntries The entries after Author's Note. |
| 170 | * @property {{[key: string]: string[]}} outletEntries - Array of entries to be added to an outlet | ||
| 169 | * @property {Set<any>} allActivatedEntries All entries. | 171 | * @property {Set<any>} allActivatedEntries All entries. |
| 170 | */ | 172 | */ |
| 171 | 173 | ||
| @@ -817,6 +819,7 @@ export const world_info_position = { | |||
| 817 | atDepth: 4, | 819 | atDepth: 4, |
| 818 | EMTop: 5, | 820 | EMTop: 5, |
| 819 | EMBottom: 6, | 821 | EMBottom: 6, |
| 822 | outlet: 7, | ||
| 820 | }; | 823 | }; |
| 821 | 824 | ||
| 822 | export const wi_anchor_position = { | 825 | export const wi_anchor_position = { |
| @@ -866,6 +869,7 @@ export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanD | |||
| 866 | worldInfoDepth: activatedWorldInfo.WIDepthEntries ?? [], | 869 | worldInfoDepth: activatedWorldInfo.WIDepthEntries ?? [], |
| 867 | anBefore: activatedWorldInfo.ANBeforeEntries ?? [], | 870 | anBefore: activatedWorldInfo.ANBeforeEntries ?? [], |
| 868 | anAfter: activatedWorldInfo.ANAfterEntries ?? [], | 871 | anAfter: activatedWorldInfo.ANAfterEntries ?? [], |
| 872 | outletEntries: activatedWorldInfo.outletEntries ?? {}, | ||
| 869 | }; | 873 | }; |
| 870 | } | 874 | } |
| 871 | 875 | ||
| @@ -3425,6 +3429,19 @@ export async function getWorldEntry(name, data, entry) { | |||
| 3425 | contentInput.val(entry.content).trigger('input', { skipCount: true, noSave: true }); | 3429 | contentInput.val(entry.content).trigger('input', { skipCount: true, noSave: true }); |
| 3426 | editTemplate.find('.editor_maximize').attr('data-for', contentInputId); | 3430 | editTemplate.find('.editor_maximize').attr('data-for', contentInputId); |
| 3427 | 3431 | ||
| 3432 | // Outlet name | ||
| 3433 | const outletNameInput = editTemplate.find('input[name="outletName"]'); | ||
| 3434 | outletNameInput.data('uid', entry.uid); | ||
| 3435 | outletNameInput.on('input', async function (_, { noSave = false } = {}) { | ||
| 3436 | const uid = $(this).data('uid'); | ||
| 3437 | const value = $(this).val(); | ||
| 3438 | data.entries[uid].outletName = value; | ||
| 3439 | setWIOriginalDataValue(data, uid, 'extensions.outlet_name', data.entries[uid].outletName); | ||
| 3440 | !noSave && await saveWorldInfo(name, data); | ||
| 3441 | }); | ||
| 3442 | outletNameInput.val(entry.outletName ?? '').trigger('input', { noSave: true }); | ||
| 3443 | setTimeout(() => createEntryInputAutocomplete(outletNameInput, getOutletNameCallback(data), { allowMultiple: true }), 1); | ||
| 3444 | |||
| 3428 | // Scan depth | 3445 | // Scan depth |
| 3429 | const scanDepthInput = editTemplate.find('input[name="scanDepth"]'); | 3446 | const scanDepthInput = editTemplate.find('input[name="scanDepth"]'); |
| 3430 | scanDepthInput.data('uid', entry.uid); | 3447 | scanDepthInput.data('uid', entry.uid); |
| @@ -3597,63 +3614,105 @@ export async function getWorldEntry(name, data, entry) { | |||
| 3597 | return headerTemplate; | 3614 | return headerTemplate; |
| 3598 | } | 3615 | } |
| 3599 | 3616 | ||
| 3617 | |||
| 3600 | /** | 3618 | /** |
| 3601 | * Get the inclusion groups for the autocomplete. | 3619 | * Builds a jQuery UI autocomplete callback: (control, request, response) => void |
| 3602 | * @param {any} data WI data | 3620 | * @param {object} [opt={}] - Optional arguments |
| 3603 | * @returns {(input: any, output: any) => any} Callback function for the autocomplete | 3621 | * @param {{entries: Record<string, any>}} [opt.data] - Your WI data |
| 3622 | * @param {(entry:any)=>string|string[]|null|undefined} [opt.collectValues] - Extract values from one entry | ||
| 3623 | * @param {() => Iterable<string>} [opt.includeExtras] - Optional global extras to include | ||
| 3624 | * @param {(ctx:{result:string[], control:JQuery, input:any, haystack:string[]})=>string[]} [opt.postFilter] - Optional final filter step (for special rules like your "group" de-dupe logic) | ||
| 3604 | */ | 3625 | */ |
| 3605 | function getInclusionGroupCallback(data) { | 3626 | function buildAutocompleteCallback({ data, collectValues, includeExtras = () => [], postFilter } = {}) { |
| 3606 | return function (control, input, output) { | 3627 | return function (control, input, output) { |
| 3607 | const uid = $(control).data('uid'); | 3628 | const uid = $(control).data('uid'); |
| 3608 | const thisGroups = String($(control).val()).split(/,\s*/).filter(x => x).map(x => x.toLowerCase()); | 3629 | |
| 3609 | const groups = new Set(); | 3630 | // Collect unique values from all *other* entries |
| 3610 | for (const entry of Object.values(data.entries)) { | 3631 | const values = new Set(); |
| 3611 | // Skip the groups of this entry, because auto-complete should only suggest the ones that are already available on other entries | 3632 | for (const entry of Object.values(data.entries ?? {})) { |
| 3612 | if (entry.uid == uid) continue; | 3633 | if (entry?.uid == uid) continue; |
| 3613 | if (entry.group) { | 3634 | const raw = collectValues(entry); |
| 3614 | entry.group.split(/,\s*/).filter(x => x).forEach(x => groups.add(x)); | 3635 | if (raw == null) continue; |
| 3636 | const arr = Array.isArray(raw) ? raw : [raw]; | ||
| 3637 | for (const v of arr) { | ||
| 3638 | const s = String(v).trim(); | ||
| 3639 | if (s) values.add(s); | ||
| 3615 | } | 3640 | } |
| 3616 | } | 3641 | } |
| 3617 | 3642 | ||
| 3618 | const haystack = Array.from(groups); | 3643 | // Add optional global extras |
| 3619 | haystack.sort((a, b) => a.localeCompare(b)); | 3644 | for (const v of includeExtras()) { |
| 3620 | const needle = input.term.toLowerCase(); | 3645 | const s = String(v).trim(); |
| 3621 | const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1; | 3646 | if (s) values.add(s); |
| 3622 | const result = haystack.filter(x => x.toLowerCase().includes(needle) && (!thisGroups.includes(x) || hasExactMatch && thisGroups.filter(g => g == x).length == 1)); | 3647 | } |
| 3648 | |||
| 3649 | // Sort stable & locale-aware | ||
| 3650 | const haystack = Array.from(values).sort((a, b) => a.localeCompare(b)); | ||
| 3651 | |||
| 3652 | // Case-insensitive contains | ||
| 3653 | const needle = String(input.term ?? '').toLowerCase(); | ||
| 3654 | let result = haystack.filter(x => x.toLowerCase().includes(needle)); | ||
| 3655 | |||
| 3656 | // Optional final-pass semantics | ||
| 3657 | if (postFilter) { | ||
| 3658 | result = postFilter({ result, control: $(control), input, haystack }); | ||
| 3659 | } | ||
| 3623 | 3660 | ||
| 3624 | output(result); | 3661 | output(result); |
| 3625 | }; | 3662 | }; |
| 3626 | } | 3663 | } |
| 3627 | 3664 | ||
| 3628 | function getAutomationIdCallback(data) { | 3665 | /** |
| 3629 | return function (control, input, output) { | 3666 | * Splits a string into an array of strings, separated by commas and trimmed |
| 3630 | const uid = $(control).data('uid'); | 3667 | * @param {string} s - The string to split |
| 3631 | const ids = new Set(); | 3668 | * @returns {string[]} An array of strings, separated by commas and trimmed |
| 3632 | for (const entry of Object.values(data.entries)) { | 3669 | */ |
| 3633 | // Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries | 3670 | const splitCsv = s => String(s ?? '').split(/,\s*/).filter(Boolean); |
| 3634 | if (entry.uid == uid) continue; | ||
| 3635 | if (entry.automationId) { | ||
| 3636 | ids.add(String(entry.automationId)); | ||
| 3637 | } | ||
| 3638 | } | ||
| 3639 | 3671 | ||
| 3640 | if ('quickReplyApi' in globalThis) { | 3672 | /** |
| 3641 | for (const automationId of globalThis.quickReplyApi.listAutomationIds()) { | 3673 | * Get the inclusion groups for the autocomplete. |
| 3642 | ids.add(String(automationId)); | 3674 | * @param {any} data WI data |
| 3643 | } | 3675 | * @returns {(input: any, output: any) => any} Callback function for the autocomplete |
| 3676 | */ | ||
| 3677 | function getInclusionGroupCallback(data) { | ||
| 3678 | return buildAutocompleteCallback({ | ||
| 3679 | data, | ||
| 3680 | collectValues: entry => entry.group ? splitCsv(entry.group) : [], | ||
| 3681 | postFilter: ({ result, control, input, haystack }) => { | ||
| 3682 | const thisGroups = splitCsv(String($(control).val())); | ||
| 3683 | const needle = String(input.term ?? '').toLowerCase(); | ||
| 3684 | const hasExactMatch = haystack.some(x => x.toLowerCase() === needle); | ||
| 3685 | |||
| 3686 | // include suggestion if it contains the needle AND | ||
| 3687 | // (not already present OR (exact match typed && appears only once)) | ||
| 3688 | return result.filter(x => | ||
| 3689 | !thisGroups.includes(x) || | ||
| 3690 | (hasExactMatch && thisGroups.filter(g => g === x).length === 1), | ||
| 3691 | ); | ||
| 3692 | }, | ||
| 3693 | }); | ||
| 3644 | } | 3694 | } |
| 3645 | 3695 | ||
| 3646 | const haystack = Array.from(ids); | 3696 | function getAutomationIdCallback(data) { |
| 3647 | haystack.sort((a, b) => a.localeCompare(b)); | 3697 | return buildAutocompleteCallback({ |
| 3648 | const needle = input.term.toLowerCase(); | 3698 | data, |
| 3649 | const result = haystack.filter(x => x.toLowerCase().includes(needle)); | 3699 | collectValues: entry => entry.automationId != null ? [String(entry.automationId)] : [], |
| 3700 | includeExtras: () => | ||
| 3701 | ('quickReplyApi' in globalThis && globalThis.quickReplyApi?.listAutomationIds) | ||
| 3702 | ? globalThis.quickReplyApi.listAutomationIds() | ||
| 3703 | : [], | ||
| 3704 | }); | ||
| 3705 | } | ||
| 3650 | 3706 | ||
| 3651 | output(result); | 3707 | function getOutletNameCallback(data) { |
| 3652 | }; | 3708 | return buildAutocompleteCallback({ |
| 3709 | data, | ||
| 3710 | collectValues: entry => entry.position === world_info_position.outlet && entry.outletName ? [entry.outletName] : [], | ||
| 3711 | }); | ||
| 3653 | } | 3712 | } |
| 3654 | 3713 | ||
| 3655 | /** | 3714 | /** |
| 3656 | * Create an autocomplete for the inclusion group. | 3715 | * Create an autocomplete for an input element. |
| 3657 | * @param {JQuery<HTMLElement>} input - Input element to attach the autocomplete to | 3716 | * @param {JQuery<HTMLElement>} input - Input element to attach the autocomplete to |
| 3658 | * @param {(control: JQuery<HTMLElement>, input: any, output: any) => any} callback - Source data callbacks | 3717 | * @param {(control: JQuery<HTMLElement>, input: any, output: any) => any} callback - Source data callbacks |
| 3659 | * @param {object} [options={}] - Optional arguments | 3718 | * @param {object} [options={}] - Optional arguments |
| @@ -3770,6 +3829,7 @@ export const newWorldInfoEntryDefinition = { | |||
| 3770 | probability: { default: 100, type: 'number' }, | 3829 | probability: { default: 100, type: 'number' }, |
| 3771 | useProbability: { default: true, type: 'boolean' }, | 3830 | useProbability: { default: true, type: 'boolean' }, |
| 3772 | depth: { default: DEFAULT_DEPTH, type: 'number' }, | 3831 | depth: { default: DEFAULT_DEPTH, type: 'number' }, |
| 3832 | outletName: { default: '', type: 'string' }, | ||
| 3773 | group: { default: '', type: 'string' }, | 3833 | group: { default: '', type: 'string' }, |
| 3774 | groupOverride: { default: false, type: 'boolean' }, | 3834 | groupOverride: { default: false, type: 'boolean' }, |
| 3775 | groupWeight: { default: DEFAULT_WEIGHT, type: 'number' }, | 3835 | groupWeight: { default: DEFAULT_WEIGHT, type: 'number' }, |
| @@ -4281,7 +4341,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | |||
| 4281 | timedEffects.checkTimedEffects(); | 4341 | timedEffects.checkTimedEffects(); |
| 4282 | 4342 | ||
| 4283 | if (sortedEntries.length === 0) { | 4343 | if (sortedEntries.length === 0) { |
| 4284 | return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], ANBeforeEntries: [], ANAfterEntries: [], allActivatedEntries: new Set() }; | 4344 | return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], ANBeforeEntries: [], ANAfterEntries: [], outletEntries: {}, allActivatedEntries: new Set() }; |
| 4285 | } | 4345 | } |
| 4286 | 4346 | ||
| 4287 | /** @type {number[]} Represents the delay levels for entries that are delayed until recursion */ | 4347 | /** @type {number[]} Represents the delay levels for entries that are delayed until recursion */ |
| @@ -4682,6 +4742,8 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | |||
| 4682 | const ANTopEntries = []; | 4742 | const ANTopEntries = []; |
| 4683 | const ANBottomEntries = []; | 4743 | const ANBottomEntries = []; |
| 4684 | const WIDepthEntries = []; | 4744 | const WIDepthEntries = []; |
| 4745 | /** @type {{[key: string]: string[]}} */ | ||
| 4746 | const WIOutletEntries = {}; | ||
| 4685 | 4747 | ||
| 4686 | // Appends from insertion order 999 to 1. Use unshift for this purpose | 4748 | // Appends from insertion order 999 to 1. Use unshift for this purpose |
| 4687 | // TODO (kingbri): Change to use WI Anchor positioning instead of separate top/bottom arrays | 4749 | // TODO (kingbri): Change to use WI Anchor positioning instead of separate top/bottom arrays |
| @@ -4730,6 +4792,18 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | |||
| 4730 | } | 4792 | } |
| 4731 | break; | 4793 | break; |
| 4732 | } | 4794 | } |
| 4795 | case world_info_position.outlet: { | ||
| 4796 | if (!entry.outletName) { | ||
| 4797 | console.warn(`[WI] Entry ${entry.uid} has position 'outlet' but no outlet name. Skipping.`); | ||
| 4798 | break; | ||
| 4799 | } | ||
| 4800 | if (Array.isArray(WIOutletEntries[entry.outletName])) { | ||
| 4801 | WIOutletEntries[entry.outletName].push(content); | ||
| 4802 | } else { | ||
| 4803 | WIOutletEntries[entry.outletName] = [content]; | ||
| 4804 | } | ||
| 4805 | break; | ||
| 4806 | } | ||
| 4733 | default: | 4807 | default: |
| 4734 | break; | 4808 | break; |
| 4735 | } | 4809 | } |
| @@ -4751,7 +4825,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | |||
| 4751 | console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values())); | 4825 | console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values())); |
| 4752 | console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`); | 4826 | console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`); |
| 4753 | 4827 | ||
| 4754 | return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, ANBeforeEntries: ANTopEntries, ANAfterEntries: ANBottomEntries, allActivatedEntries: new Set(allActivatedEntries.values()) }; | 4828 | return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, ANBeforeEntries: ANTopEntries, ANAfterEntries: ANBottomEntries, outletEntries: WIOutletEntries, allActivatedEntries: new Set(allActivatedEntries.values()) }; |
| 4755 | } | 4829 | } |
| 4756 | 4830 | ||
| 4757 | /** | 4831 | /** |
| @@ -4971,6 +5045,7 @@ function convertAgnaiMemoryBook(inputObj) { | |||
| 4971 | displayIndex: index, | 5045 | displayIndex: index, |
| 4972 | probability: 100, | 5046 | probability: 100, |
| 4973 | useProbability: true, | 5047 | useProbability: true, |
| 5048 | outletName: '', | ||
| 4974 | group: '', | 5049 | group: '', |
| 4975 | groupOverride: false, | 5050 | groupOverride: false, |
| 4976 | groupWeight: DEFAULT_WEIGHT, | 5051 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5015,6 +5090,7 @@ function convertRisuLorebook(inputObj) { | |||
| 5015 | displayIndex: index, | 5090 | displayIndex: index, |
| 5016 | probability: entry.activationPercent ?? 100, | 5091 | probability: entry.activationPercent ?? 100, |
| 5017 | useProbability: entry.activationPercent ?? true, | 5092 | useProbability: entry.activationPercent ?? true, |
| 5093 | outletName: '', | ||
| 5018 | group: '', | 5094 | group: '', |
| 5019 | groupOverride: false, | 5095 | groupOverride: false, |
| 5020 | groupWeight: DEFAULT_WEIGHT, | 5096 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5064,6 +5140,7 @@ function convertNovelLorebook(inputObj) { | |||
| 5064 | displayIndex: index, | 5140 | displayIndex: index, |
| 5065 | probability: 100, | 5141 | probability: 100, |
| 5066 | useProbability: true, | 5142 | useProbability: true, |
| 5143 | outletName: '', | ||
| 5067 | group: '', | 5144 | group: '', |
| 5068 | groupOverride: false, | 5145 | groupOverride: false, |
| 5069 | groupWeight: DEFAULT_WEIGHT, | 5146 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5114,6 +5191,7 @@ export function convertCharacterBook(characterBook) { | |||
| 5114 | useProbability: entry.extensions?.useProbability ?? true, | 5191 | useProbability: entry.extensions?.useProbability ?? true, |
| 5115 | depth: entry.extensions?.depth ?? DEFAULT_DEPTH, | 5192 | depth: entry.extensions?.depth ?? DEFAULT_DEPTH, |
| 5116 | selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY, | 5193 | selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY, |
| 5194 | outletName: entry.extensions?.outlet_name ?? '', | ||
| 5117 | group: entry.extensions?.group ?? '', | 5195 | group: entry.extensions?.group ?? '', |
| 5118 | groupOverride: entry.extensions?.group_override ?? false, | 5196 | groupOverride: entry.extensions?.group_override ?? false, |
| 5119 | groupWeight: entry.extensions?.group_weight ?? DEFAULT_WEIGHT, | 5197 | groupWeight: entry.extensions?.group_weight ?? DEFAULT_WEIGHT, |
| @@ -689,6 +689,7 @@ function convertWorldInfoToCharacterBook(name, entries) { | |||
| 689 | useProbability: entry.useProbability ?? false, | 689 | useProbability: entry.useProbability ?? false, |
| 690 | depth: entry.depth ?? 4, | 690 | depth: entry.depth ?? 4, |
| 691 | selectiveLogic: entry.selectiveLogic ?? 0, | 691 | selectiveLogic: entry.selectiveLogic ?? 0, |
| 692 | outlet_name: entry.outletName ?? '', | ||
| 692 | group: entry.group ?? '', | 693 | group: entry.group ?? '', |
| 693 | group_override: entry.groupOverride ?? false, | 694 | group_override: entry.groupOverride ?? false, |
| 694 | group_weight: entry.groupWeight ?? null, | 695 | group_weight: entry.groupWeight ?? null, |