diff --git a/public/index.html b/public/index.html
index 12ae48143..4c8b40d29 100644
--- a/public/index.html
+++ b/public/index.html
@@ -6013,6 +6013,7 @@
+
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js
index 07625437a..35e4b064e 100644
--- a/public/scripts/world-info.js
+++ b/public/scripts/world-info.js
@@ -3139,6 +3139,70 @@ export async function getWorldEntry(name, data, entry) {
updateEditor(navigation_option.previous);
});
+ // move button
+ const moveButton = template.find('.move_entry_button');
+ moveButton.data('uid', entry.uid);
+ moveButton.data('current-world', name);
+ moveButton.on('click', async function (e) {
+ e.stopPropagation();
+ const sourceUid = $(this).data('uid');
+ const sourceWorld = $(this).data('current-world');
+ // Loading world info is bad, do we have cache variable?
+ const sourceName = (await loadWorldInfo(sourceWorld)).entries[sourceUid].comment;
+
+ let optionsHtml = ``;
+ let selectableWorldCount = 0;
+ world_names.forEach(worldName => {
+ if (worldName !== sourceWorld) { // Exclude the current world
+ optionsHtml += ``;
+ selectableWorldCount += 1;
+ }
+ });
+
+ if (selectableWorldCount === 0) {
+ toastr.warning(t`There are no other lorebooks to move to.`);
+ return;
+ }
+
+ const content = `
+
${t`Move ${sourceName} to:`}
+
+ `;
+
+ const popupPromise = callGenericPopup(content, POPUP_TYPE.CONFIRM, '', {
+ okButton: t`Move`,
+ cancelButton: t`Cancel`,
+ });
+
+ let selectedWorldIndex = -1;
+ $('#move_entry_target_select').on('change', function () {
+ /** @type {string} */
+ // @ts-ignore
+ const value = $(this).val();
+ selectedWorldIndex = value === '' ? -1 : Number(value);
+ });
+
+ const popupConfirm = await popupPromise;
+ 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
const scanDepthInput = template.find('input[name="scanDepth"]');
scanDepthInput.data('uid', entry.uid);
@@ -5267,3 +5331,110 @@ 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 {number|string} uid - The UID of the entry to move from the source lorebook.
+ * @returns {Promise} True if the move was successful, false otherwise.
+ */
+export async function moveWorldInfoEntry(sourceName, targetName, uid) {
+ console.log(`[WI] Attempting to move entry UID ${uid} from '${sourceName}' to '${targetName}'`);
+
+ if (!sourceName || !targetName || uid === undefined || uid === null) {
+ console.error('[WI Move] Missing required arguments.');
+ return false;
+ }
+
+ if (sourceName === targetName) {
+ toastr.warning(t`Source and target lorebooks cannot be the same.`);
+ 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;
+ // Reset displayIndex or let it be recalculated based on target book's sorting?
+ // For simplicity, let's assign a high index initially, assuming it might be sorted later.
+ // Or maybe better, find the max displayIndex in target and add 1?
+ 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, using the original UID
+ deleteWIOriginalDataValue(sourceData, entryUidString);
+ console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
+
+
+ // Save immediately to reduce chances of inconsistency if the browser is closed
+ // Note: This is not truly atomic. If one save fails, state could be inconsistent.
+ 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}'.`);
+
+
+ toastr.success(t`${entryToMove.comment} moved successfully!`);
+
+ // 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;
+ }
+}