Renaming a lorebook now re-links itself to cards using it (with a confirmation prompt) (#5323) * renaming a lorebook prompts to update existing links * used suggested api and logic * add world property to shallow function * Fix type error in assignLorebookToChat invoke * Remove debug console logs * Fix activeCharacterUpdated * Extract updateWorldInfoLinks into a func * Invert if for an early return --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { Fuse } from '../lib.js'; |
| 2 | 2 | |
| 3 | 3 | import { saveSettings, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles, create_save, createOrEditCharacter, name1, getOneCharacter, select_selected_character } from '../script.js'; |
| 4 | 4 | import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce, findChar, onlyUnique, equalsIgnoreCaseAndAccents, uuidv4, normalizeArray, getUniqueName, logSlashCommandWarn, addLongPressEvent } from './utils.js'; |
| 5 | 5 | import { extension_settings, getContext } from './extensions.js'; |
| 6 | 6 | import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; |
| @@ -4109,6 +4109,27 @@ async function renameWorldInfo(name, data) { | ||
| 4109 | 4109 | await saveWorldInfo(newName, data, true); |
| 4110 | 4110 | await deleteWorldInfo(oldName); |
| 4111 | 4111 | |
| 4112 | + await updateWorldInfoLinks(oldName, newName); | |
| 4113 | + | |
| 4114 | + if (entryPreviouslySelected !== -1) { | |
| 4115 | + const wiElement = getWIElement(newName); | |
| 4116 | + wiElement.prop('selected', true); | |
| 4117 | + $('#world_info').trigger('change'); | |
| 4118 | + } | |
| 4119 | + | |
| 4120 | + const selectedIndex = world_names.indexOf(newName); | |
| 4121 | + if (selectedIndex !== -1) { | |
| 4122 | + $('#world_editor_select').val(selectedIndex).trigger('change'); | |
| 4123 | + } | |
| 4124 | +} | |
| 4125 | + | |
| 4126 | +/** | |
| 4127 | + * Retargets all character lore links from an old world info name to a new one, with an optional confirmation for primary lorebook links | |
| 4128 | + * @param {string} oldName Previous WI file name | |
| 4129 | + * @param {string} newName New WI file name | |
| 4130 | + * @returns {Promise<void>} | |
| 4131 | + */ | |
| 4132 | +async function updateWorldInfoLinks(oldName, newName) { | |
| 4112 | 4133 | const existingCharLores = world_info.charLore?.filter((e) => e.extraBooks.includes(oldName)); |
| 4113 | 4134 | if (existingCharLores && existingCharLores.length > 0) { |
| 4114 | 4135 | existingCharLores.forEach((charLore) => { |
| @@ -4119,15 +4140,70 @@ async function renameWorldInfo(name, data) { | ||
| 4119 | 4140 | saveSettingsDebounced(); |
| 4120 | 4141 | } |
| 4121 | 4142 | |
| 4122 | - if (entryPreviouslySelected !== -1) { | |
| 4143 | + // find all characters using the old lorebook name as their primary world | |
| 4123 | 4144 | const wiElementlinkedChIDs = getWIElement(newName)[]; |
| 4124 | - wiElement.prop('selected', true); | |
| 4145 | + characters.forEach((character, chid) => { | |
| 4125 | - $('#world_info').trigger('change'); | |
| 4146 | + if (character.data?.extensions?.world === oldName) { | |
| 4147 | + linkedChIDs.push(chid); | |
| 4126 | 4148 | } |
| 4149 | + }); | |
| 4127 | 4150 | |
| 4128 | - const selectedIndex = world_names.indexOf(newName); | |
| 4151 | + if (!linkedChIDs.length) { | |
| 4129 | - if (selectedIndex !== -1) { | |
| 4152 | + return; | |
| 4130 | - $('#world_editor_select').val(selectedIndex).trigger('change'); | |
| 4153 | + } | |
| 4154 | + | |
| 4155 | + // Trigger the confirmation popup | |
| 4156 | + const updatePastLinksConfirm = await Popup.show.confirm( | |
| 4157 | + t`World/Lorebook renamed!`, | |
| 4158 | + `<p>${t`Auxiliary Lorebook links have been updated. Would you like to update primary lorebook links for ${linkedChIDs.length} character(s) as well?`}</p>`, | |
| 4159 | + ) == POPUP_RESULT.AFFIRMATIVE; | |
| 4160 | + | |
| 4161 | + if (updatePastLinksConfirm) { | |
| 4162 | + let activeCharacterUpdated = false; | |
| 4163 | + | |
| 4164 | + for (const chid of linkedChIDs) { | |
| 4165 | + const character = characters[chid]; | |
| 4166 | + | |
| 4167 | + try { | |
| 4168 | + // /merge-attributes API call to update the file on the backend silently | |
| 4169 | + const response = await fetch('/api/characters/merge-attributes', { | |
| 4170 | + method: 'POST', | |
| 4171 | + headers: getRequestHeaders(), | |
| 4172 | + body: JSON.stringify({ | |
| 4173 | + avatar: character.avatar, | |
| 4174 | + data: { | |
| 4175 | + extensions: { | |
| 4176 | + world: newName, | |
| 4177 | + }, | |
| 4178 | + }, | |
| 4179 | + }), | |
| 4180 | + }); | |
| 4181 | + | |
| 4182 | + if (!response.ok) { | |
| 4183 | + throw new Error(`Merge API returned ${response.status}`); | |
| 4184 | + } | |
| 4185 | + | |
| 4186 | + // used to update the data in the browser's memory | |
| 4187 | + await getOneCharacter(character.avatar); | |
| 4188 | + | |
| 4189 | + // Flag if the currently open character was affected | |
| 4190 | + if (String(chid) === String(this_chid)) { | |
| 4191 | + activeCharacterUpdated = true; | |
| 4192 | + } | |
| 4193 | + | |
| 4194 | + toastr.success(`Successfully updated link for ${character.name}.`); | |
| 4195 | + } catch (e) { | |
| 4196 | + toastr.error(`Failed to update link for ${character.name}.`); | |
| 4197 | + console.error(`Backend update for character ${character.name} failed:`, e); | |
| 4198 | + } | |
| 4199 | + } | |
| 4200 | + | |
| 4201 | + // update the UI fields | |
| 4202 | + // only required if the currently selected character was changed | |
| 4203 | + if (activeCharacterUpdated) { | |
| 4204 | + select_selected_character(this_chid, { switchMenu: false }); | |
| 4205 | + setWorldInfoButtonClass(this_chid, true); | |
| 4206 | + } | |
| 4131 | 4207 | } |
| 4132 | 4208 | } |
| 4133 | 4209 | |
| @@ -5732,13 +5808,15 @@ export function openWorldInfoEditor(worldName) { | ||
| 5732 | 5808 | |
| 5733 | 5809 | /** |
| 5734 | 5810 | * Assigns a lorebook to the current chat. |
| 5735 | - * @param {JQuery.ClickEvent<Document, undefined, any, any>} event Pointer event | |
| 5811 | + * @param {Object} options - The options for assigning the lorebook. | |
| 5812 | + * @param {boolean} options.shiftKey - Whether the Shift key is pressed. | |
| 5813 | + * @param {boolean} options.altKey - Whether the Alt key is pressed. | |
| 5736 | 5814 | * @returns {Promise<void>} |
| 5737 | 5815 | */ |
| 5738 | 5816 | export async function assignLorebookToChat(event{ shiftKey, altKey }) { |
| 5739 | 5817 | const selectedName = chat_metadata[METADATA_KEY]; |
| 5740 | 5818 | |
| 5741 | 5819 | if (selectedName && !event.shiftKey && !event.altKey) { |
| 5742 | 5820 | openWorldInfoEditor(selectedName); |
| 5743 | 5821 | return; |
| 5744 | 5822 | } |
| @@ -388,6 +388,7 @@ const toShallow = (character) => { | ||
| 388 | 388 | tags: _.get(character, 'data.tags', []), |
| 389 | 389 | extensions: { |
| 390 | 390 | fav: _.get(character, 'data.extensions.fav', false), |
| 391 | + world: _.get(character, 'data.extensions.world', ''), | |
| 391 | 392 | }, |
| 392 | 393 | }, |
| 393 | 394 | }; |