Implement swipe data persistence between reloads and ensure all tracker data commits are based on prior assistant message when generating/swiping
This commit is contained in:
+58
-9
@@ -224,6 +224,26 @@ export function saveChatData() {
|
|||||||
saveChatDebounced();
|
saveChatDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors a tracker data entry into message.swipe_info so it survives page reloads.
|
||||||
|
* ST only serializes swipe_info to disk; message.extra is in-memory only.
|
||||||
|
* Guard: skips silently if swipe_info[swipeId] doesn't exist yet
|
||||||
|
*
|
||||||
|
* @param {Object} message - The chat message object
|
||||||
|
* @param {number} swipeId - The swipe index to mirror into
|
||||||
|
* @param {Object} swipeEntry - { userStats, infoBox, characterThoughts }
|
||||||
|
*/
|
||||||
|
export function mirrorToSwipeInfo(message, swipeId, swipeEntry) {
|
||||||
|
if (!message.swipe_info || !message.swipe_info[swipeId]) return;
|
||||||
|
if (!message.swipe_info[swipeId].extra) {
|
||||||
|
message.swipe_info[swipeId].extra = {};
|
||||||
|
}
|
||||||
|
if (!message.swipe_info[swipeId].extra.rpg_companion_swipes) {
|
||||||
|
message.swipe_info[swipeId].extra.rpg_companion_swipes = {};
|
||||||
|
}
|
||||||
|
message.swipe_info[swipeId].extra.rpg_companion_swipes[swipeId] = swipeEntry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the last assistant message's swipe data with current tracker data.
|
* Updates the last assistant message's swipe data with current tracker data.
|
||||||
* This ensures user edits are preserved across swipes and included in generation context.
|
* This ensures user edits are preserved across swipes and included in generation context.
|
||||||
@@ -255,15 +275,7 @@ export function updateMessageSwipeData() {
|
|||||||
message.extra.rpg_companion_swipes[swipeId] = swipeEntry;
|
message.extra.rpg_companion_swipes[swipeId] = swipeEntry;
|
||||||
|
|
||||||
// Mirror to swipe_info so data survives page reloads regardless of active swipe
|
// Mirror to swipe_info so data survives page reloads regardless of active swipe
|
||||||
if (message.swipe_info && message.swipe_info[swipeId]) {
|
mirrorToSwipeInfo(message, swipeId, swipeEntry);
|
||||||
if (!message.swipe_info[swipeId].extra) {
|
|
||||||
message.swipe_info[swipeId].extra = {};
|
|
||||||
}
|
|
||||||
if (!message.swipe_info[swipeId].extra.rpg_companion_swipes) {
|
|
||||||
message.swipe_info[swipeId].extra.rpg_companion_swipes = {};
|
|
||||||
}
|
|
||||||
message.swipe_info[swipeId].extra.rpg_companion_swipes[swipeId] = swipeEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] Updated message swipe data after user edit');
|
// console.log('[RPG Companion] Updated message swipe data after user edit');
|
||||||
break;
|
break;
|
||||||
@@ -292,6 +304,43 @@ export function getSwipeData(message, swipeId) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits tracker data from the assistant message immediately before currentMessageIndex.
|
||||||
|
* Walks backward through the chat skipping the current message, user messages, and system
|
||||||
|
* messages until it finds the prior assistant message, then loads its active swipe data.
|
||||||
|
* If no prior assistant message exists or exists without a tracker state, nulls out all fields so
|
||||||
|
* the AI generates from an empty context rather than a ghost state.
|
||||||
|
*
|
||||||
|
* @param {number} currentMessageIndex - Index of the message to start searching before
|
||||||
|
*/
|
||||||
|
export function commitTrackerDataFromPriorMessage(currentMessageIndex) {
|
||||||
|
const chat = getContext().chat;
|
||||||
|
if (!chat || chat.length === 0) {
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = currentMessageIndex - 1; i >= 0; i--) {
|
||||||
|
const message = chat[i];
|
||||||
|
if (message.is_user || message.is_system) continue;
|
||||||
|
|
||||||
|
// Found the prior assistant message — commit its active swipe data
|
||||||
|
const swipeId = message.swipe_id || 0;
|
||||||
|
const swipeData = getSwipeData(message, swipeId);
|
||||||
|
committedTrackerData.userStats = swipeData?.userStats || null;
|
||||||
|
committedTrackerData.infoBox = swipeData?.infoBox || null;
|
||||||
|
committedTrackerData.characterThoughts = swipeData?.characterThoughts || null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No prior assistant message found — use empty context
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads RPG data from the current chat's metadata.
|
* Loads RPG data from the current chat's metadata.
|
||||||
* Automatically migrates v1 inventory to v2 format if needed.
|
* Automatically migrates v1 inventory to v2 format if needed.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
setLastActionWasSwipe,
|
setLastActionWasSwipe,
|
||||||
$musicPlayerContainer
|
$musicPlayerContainer
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData } from '../../core/persistence.js';
|
import { saveChatData, mirrorToSwipeInfo } from '../../core/persistence.js';
|
||||||
import {
|
import {
|
||||||
generateSeparateUpdatePrompt
|
generateSeparateUpdatePrompt
|
||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
@@ -317,11 +317,15 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = lastMessage.swipe_id || 0;
|
const currentSwipeId = lastMessage.swipe_id || 0;
|
||||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
const swipeEntry = {
|
||||||
userStats: parsedData.userStats,
|
userStats: parsedData.userStats,
|
||||||
infoBox: parsedData.infoBox,
|
infoBox: parsedData.infoBox,
|
||||||
characterThoughts: parsedData.characterThoughts
|
characterThoughts: parsedData.characterThoughts
|
||||||
};
|
};
|
||||||
|
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = swipeEntry;
|
||||||
|
|
||||||
|
// Mirror to swipe_info so this swipe survives page reload even if never manually edited
|
||||||
|
mirrorToSwipeInfo(lastMessage, currentSwipeId, swipeEntry);
|
||||||
|
|
||||||
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
SPOTIFY_FORMAT_INSTRUCTION
|
SPOTIFY_FORMAT_INSTRUCTION
|
||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||||
|
import { commitTrackerDataFromPriorMessage } from '../../core/persistence.js';
|
||||||
|
|
||||||
// Track suppression state for event handler
|
// Track suppression state for event handler
|
||||||
let currentSuppressionState = false;
|
let currentSuppressionState = false;
|
||||||
@@ -622,25 +623,15 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
|
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
|
||||||
|
|
||||||
if (shouldCommit) {
|
if (shouldCommit) {
|
||||||
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing data from BEFORE user message');
|
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing from N-1 assistant message');
|
||||||
// console.log('[RPG Companion] Chat length:', currentChatLength, 'Last committed:', lastCommittedChatLength);
|
// console.log('[RPG Companion] Chat length:', currentChatLength, 'Last committed:', lastCommittedChatLength);
|
||||||
// console.log('[RPG Companion] BEFORE: committedTrackerData =', {
|
|
||||||
// userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : 'null'
|
|
||||||
// // });
|
|
||||||
// console.log('[RPG Companion] BEFORE: lastGeneratedData =', {
|
|
||||||
// userStats: lastGeneratedData.userStats ? `${lastGeneratedData.userStats.substring(0, 50)}...` : 'null',
|
|
||||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: lastGeneratedData.characterThoughts ? `${lastGeneratedData.characterThoughts.substring(0, 100)}...` : 'null'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Commit displayed data (from before user sent message)
|
// Commit from the prior assistant message's swipe store (N-1 rule).
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
// currentChatLength - 1 is the new AI placeholder; the function walks backward
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
// past it and the user message to find the previous AI message's tracker state.
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
commitTrackerDataFromPriorMessage(currentChatLength - 1);
|
||||||
|
|
||||||
// Track chat length to prevent duplicate commits
|
// Track chat length to prevent duplicate commits from streaming
|
||||||
lastCommittedChatLength = currentChatLength;
|
lastCommittedChatLength = currentChatLength;
|
||||||
|
|
||||||
// console.log('[RPG Companion] AFTER: committedTrackerData =', {
|
// console.log('[RPG Companion] AFTER: committedTrackerData =', {
|
||||||
@@ -668,38 +659,14 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
|
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
|
||||||
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
|
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
|
||||||
if (!lastActionWasSwipe) {
|
if (!lastActionWasSwipe) {
|
||||||
// User sent a new message - commit lastGeneratedData before generation
|
// User sent a new message - commit from the prior assistant message's swipe store
|
||||||
// console.log('[RPG Companion] 📝 COMMIT: New message - committing lastGeneratedData');
|
// (N-1 rule) rather than lastGeneratedData, which may reflect a sibling swipe's
|
||||||
// console.log('[RPG Companion] BEFORE commit - committedTrackerData:', {
|
// outcome and would poison the context for the new generation.
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
// currentChatLength - 1 is the new AI placeholder; search starts before it.
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
commitTrackerDataFromPriorMessage(currentChatLength - 1);
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// // });
|
|
||||||
// console.log('[RPG Companion] BEFORE commit - lastGeneratedData:', {
|
|
||||||
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
|
||||||
// console.log('[RPG Companion] AFTER commit - committedTrackerData:', {
|
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Reset flag after committing (ready for next cycle)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// console.log('[RPG Companion] 🔄 SWIPE: Using existing committedTrackerData (no commit)');
|
|
||||||
// console.log('[RPG Companion] committedTrackerData:', {
|
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
// Reset flag after using it (swipe generation complete, ready for next action)
|
|
||||||
}
|
}
|
||||||
|
// If lastActionWasSwipe, context was already committed by commitTrackerDataFromPriorMessage
|
||||||
|
// in onMessageSwiped before generation started.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the committed tracker data as source for generation
|
// Use the committed tracker data as source for generation
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
updateCommittedTrackerData,
|
updateCommittedTrackerData,
|
||||||
$musicPlayerContainer
|
$musicPlayerContainer
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData, loadChatData, autoSwitchPresetForEntity, getSwipeData } from '../../core/persistence.js';
|
import { saveChatData, loadChatData, autoSwitchPresetForEntity, getSwipeData, commitTrackerDataFromPriorMessage, mirrorToSwipeInfo } from '../../core/persistence.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
// Generation & Parsing
|
// Generation & Parsing
|
||||||
@@ -118,15 +118,12 @@ export function onMessageSent() {
|
|||||||
// The RPG data comes embedded in the main response
|
// The RPG data comes embedded in the main response
|
||||||
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
||||||
|
|
||||||
// For separate mode with auto-update disabled, commit displayed tracker
|
// For separate mode with auto-update disabled, commit from the prior assistant message's
|
||||||
|
// swipe store rather than lastGeneratedData to avoid ghost context from sibling swipes.
|
||||||
|
// At this point chat[chat.length - 1] is the user message, so search from before it.
|
||||||
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
||||||
if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) {
|
commitTrackerDataFromPriorMessage(chat.length - 1);
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
// console.log('[RPG Companion] 💾 SEPARATE MODE: Committed from prior assistant message (auto-update disabled)');
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] 💾 SEPARATE MODE: Committed displayed tracker (auto-update disabled)');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,11 +189,15 @@ export async function onMessageReceived(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = lastMessage.swipe_id || 0;
|
const currentSwipeId = lastMessage.swipe_id || 0;
|
||||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
const swipeEntry = {
|
||||||
userStats: parsedData.userStats,
|
userStats: parsedData.userStats,
|
||||||
infoBox: parsedData.infoBox,
|
infoBox: parsedData.infoBox,
|
||||||
characterThoughts: parsedData.characterThoughts
|
characterThoughts: parsedData.characterThoughts
|
||||||
};
|
};
|
||||||
|
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = swipeEntry;
|
||||||
|
|
||||||
|
// Mirror to swipe_info so this swipe survives page reload even if never manually edited
|
||||||
|
mirrorToSwipeInfo(lastMessage, currentSwipeId, swipeEntry);
|
||||||
|
|
||||||
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
|
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
|
||||||
|
|
||||||
@@ -377,6 +378,9 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
// This is a NEW swipe that will trigger generation
|
// This is a NEW swipe that will trigger generation
|
||||||
setLastActionWasSwipe(true);
|
setLastActionWasSwipe(true);
|
||||||
setIsAwaitingNewMessage(true);
|
setIsAwaitingNewMessage(true);
|
||||||
|
// Immediately commit context from the prior assistant message (N-1) so generation
|
||||||
|
// uses the world state before this message, not the last-viewed sibling swipe.
|
||||||
|
commitTrackerDataFromPriorMessage(messageIndex);
|
||||||
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
|
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
|
||||||
} else {
|
} else {
|
||||||
// This is navigating to an EXISTING swipe - don't change the flag
|
// This is navigating to an EXISTING swipe - don't change the flag
|
||||||
|
|||||||
Reference in New Issue
Block a user