32280d60ef
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
644 lines
28 KiB
JavaScript
644 lines
28 KiB
JavaScript
/**
|
||
* SillyTavern Integration Module
|
||
* Handles all event listeners and integration with SillyTavern's event system
|
||
*/
|
||
|
||
import { getContext } from '../../../../../../extensions.js';
|
||
import { chat, user_avatar, setExtensionPrompt, extension_prompt_types, saveChatDebounced } from '../../../../../../../script.js';
|
||
|
||
// Core modules
|
||
import {
|
||
extensionSettings,
|
||
lastGeneratedData,
|
||
committedTrackerData,
|
||
lastActionWasSwipe,
|
||
isPlotProgression,
|
||
isAwaitingNewMessage,
|
||
setLastActionWasSwipe,
|
||
setIsPlotProgression,
|
||
setIsGenerating,
|
||
setIsAwaitingNewMessage,
|
||
updateLastGeneratedData,
|
||
updateCommittedTrackerData,
|
||
$musicPlayerContainer,
|
||
incrementSeparateGenerationId
|
||
} from '../../core/state.js';
|
||
import { saveChatData, loadChatData, autoSwitchPresetForEntity, getSwipeData, commitTrackerDataFromPriorMessage, inheritSwipeDataFromPriorMessage, mirrorToSwipeInfo } from '../../core/persistence.js';
|
||
import { i18n } from '../../core/i18n.js';
|
||
|
||
// Generation & Parsing
|
||
import { parseResponse, parseUserStats } from '../generation/parser.js';
|
||
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
||
import { updateRPGData } from '../generation/apiClient.js';
|
||
import { removeLocks } from '../generation/lockManager.js';
|
||
import { onGenerationStarted, initHistoryInjectionListeners } from '../generation/injector.js';
|
||
|
||
// Rendering
|
||
import { renderUserStats } from '../rendering/userStats.js';
|
||
import { renderInfoBox } from '../rendering/infoBox.js';
|
||
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
|
||
import { renderInventory } from '../rendering/inventory.js';
|
||
import { renderQuests } from '../rendering/quests.js';
|
||
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||
|
||
// Utils
|
||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||
|
||
// UI
|
||
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||
import { updateStripWidgets } from '../ui/desktop.js';
|
||
|
||
// Chapter checkpoint
|
||
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||
|
||
/**
|
||
* Reads the swipe store of the last assistant message in `currentChat` and
|
||
* writes its data into `lastGeneratedData`, including syncing stat bars via
|
||
* `parseUserStats`. If no assistant message exists, or none has stored swipe
|
||
* data, `lastGeneratedData` is left unchanged.
|
||
*
|
||
* Use this wherever the displayed tracker state must be re-derived from the
|
||
* authoritative swipe store rather than from chat_metadata (e.g. after a
|
||
* CHAT_CHANGED caused by branching, or after a message deletion).
|
||
*
|
||
* @param {Array} currentChat - Live chat array from getContext().chat
|
||
* @returns {boolean} True if swipe data was found and applied
|
||
*/
|
||
function syncLastGeneratedDataFromSwipeStore(currentChat) {
|
||
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||
const msg = currentChat[i];
|
||
if (!msg.is_user && !msg.is_system) {
|
||
const swipeId = msg.swipe_id || 0;
|
||
const swipeData = getSwipeData(msg, swipeId);
|
||
if (swipeData) {
|
||
lastGeneratedData.userStats = swipeData.userStats || null;
|
||
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||
// Normalise characterThoughts to string (backward compat with old object format).
|
||
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
|
||
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
|
||
} else {
|
||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||
}
|
||
if (swipeData.userStats) {
|
||
parseUserStats(swipeData.userStats);
|
||
}
|
||
return true;
|
||
}
|
||
return false; // Last assistant message exists but has no swipe data yet
|
||
}
|
||
}
|
||
return false; // No assistant messages in chat
|
||
}
|
||
|
||
/**
|
||
* Commits the tracker data from the last assistant message to be used as source for next generation.
|
||
* This should be called when the user has replied to a message, ensuring all swipes of the next
|
||
* response use the same committed context.
|
||
*/
|
||
export function commitTrackerData() {
|
||
const chat = getContext().chat;
|
||
if (!chat || chat.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// Find the last assistant message
|
||
for (let i = chat.length - 1; i >= 0; i--) {
|
||
const message = chat[i];
|
||
if (!message.is_user && !message.is_system) {
|
||
// Found last assistant message - commit its tracker data
|
||
const swipeId = message.swipe_id || 0;
|
||
const swipeData = getSwipeData(message, swipeId);
|
||
|
||
if (swipeData) {
|
||
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
|
||
committedTrackerData.userStats = swipeData.userStats || null;
|
||
committedTrackerData.infoBox = swipeData.infoBox || null;
|
||
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
|
||
} else {
|
||
// No saved swipe data — treat as empty (e.g. first message, no prior generation)
|
||
committedTrackerData.userStats = null;
|
||
committedTrackerData.infoBox = null;
|
||
committedTrackerData.characterThoughts = null;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Event handler for when the user sends a message.
|
||
* Sets the flag to indicate this is NOT a swipe.
|
||
* In together mode, commits displayed data (only for real messages, not streaming placeholders).
|
||
*/
|
||
export function onMessageSent() {
|
||
if (!extensionSettings.enabled) return;
|
||
|
||
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent - lastActionWasSwipe =', lastActionWasSwipe);
|
||
|
||
// Check if this is a streaming placeholder message (content = "...")
|
||
// When streaming is on, ST sends a "..." placeholder before generation starts
|
||
const context = getContext();
|
||
const chat = context.chat;
|
||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||
|
||
if (lastMessage && lastMessage.mes === '...') {
|
||
// console.log('[RPG Companion] 🟢 Ignoring onMessageSent: streaming placeholder message');
|
||
return;
|
||
}
|
||
|
||
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent (after placeholder check)');
|
||
// console.log('[RPG Companion] 🟢 NOTE: lastActionWasSwipe will be reset in onMessageReceived after generation completes');
|
||
|
||
// Set flag to indicate we're expecting a new message from generation
|
||
// This allows auto-update to distinguish between new generations and loading chat history
|
||
setIsAwaitingNewMessage(true);
|
||
|
||
// Note: FAB spinning is NOT shown for together mode since no extra API request is made
|
||
// The RPG data comes embedded in the main response
|
||
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
||
|
||
// 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) {
|
||
commitTrackerDataFromPriorMessage(chat.length - 1);
|
||
// console.log('[RPG Companion] 💾 SEPARATE MODE: Committed from prior assistant message (auto-update disabled)');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Event handler for when a message is generated.
|
||
*/
|
||
export async function onMessageReceived(data) {
|
||
// console.log('[RPG Companion] onMessageReceived called, lastActionWasSwipe:', lastActionWasSwipe);
|
||
|
||
if (!extensionSettings.enabled) {
|
||
return;
|
||
}
|
||
|
||
// Reset swipe flag after generation completes
|
||
// This ensures next user message (whether from original or swipe) triggers commit
|
||
setLastActionWasSwipe(false);
|
||
// console.log('[RPG Companion] 🟢 Reset lastActionWasSwipe = false (generation completed)');
|
||
|
||
if (extensionSettings.generationMode === 'together') {
|
||
// In together mode, parse the response to extract RPG data
|
||
// Commit happens in onMessageSent (when user sends message, before generation)
|
||
const lastMessage = chat[chat.length - 1];
|
||
if (lastMessage && !lastMessage.is_user) {
|
||
const responseText = lastMessage.mes;
|
||
const parsedData = parseResponse(responseText);
|
||
|
||
// Note: Don't show parsing error here - this event fires when loading chat history too
|
||
// Error notification is handled in apiClient.js for fresh generations only
|
||
|
||
// Remove locks from parsed data (JSON format only, text format is unaffected)
|
||
if (parsedData.userStats) {
|
||
parsedData.userStats = removeLocks(parsedData.userStats);
|
||
}
|
||
if (parsedData.infoBox) {
|
||
parsedData.infoBox = removeLocks(parsedData.infoBox);
|
||
}
|
||
if (parsedData.characterThoughts) {
|
||
parsedData.characterThoughts = removeLocks(parsedData.characterThoughts);
|
||
}
|
||
|
||
// Parse and store Spotify URL if feature is enabled
|
||
parseAndStoreSpotifyUrl(responseText);
|
||
|
||
// Update display data with newly parsed response
|
||
// console.log('[RPG Companion] 📝 TOGETHER MODE: Updating lastGeneratedData with parsed response');
|
||
if (parsedData.userStats) {
|
||
lastGeneratedData.userStats = parsedData.userStats;
|
||
parseUserStats(parsedData.userStats);
|
||
}
|
||
if (parsedData.infoBox) {
|
||
lastGeneratedData.infoBox = parsedData.infoBox;
|
||
}
|
||
if (parsedData.characterThoughts) {
|
||
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
||
}
|
||
|
||
// Store RPG data for this specific swipe in the message's extra field
|
||
if (!lastMessage.extra) {
|
||
lastMessage.extra = {};
|
||
}
|
||
if (!lastMessage.extra.rpg_companion_swipes) {
|
||
lastMessage.extra.rpg_companion_swipes = {};
|
||
}
|
||
|
||
const currentSwipeId = lastMessage.swipe_id || 0;
|
||
const swipeEntry = {
|
||
userStats: parsedData.userStats,
|
||
infoBox: parsedData.infoBox,
|
||
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);
|
||
|
||
// Remove the tracker code blocks from the visible message
|
||
let cleanedMessage = responseText;
|
||
|
||
// Note: JSON code blocks are hidden from display by regex script (but preserved in message data)
|
||
|
||
// Remove old text format code blocks (legacy support)
|
||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, '');
|
||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, '');
|
||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, '');
|
||
// Remove any stray "---" dividers that might appear after the code blocks
|
||
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, '');
|
||
// Clean up multiple consecutive newlines
|
||
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
|
||
// Note: <trackers> XML tags are automatically hidden by SillyTavern
|
||
// Note: <Song - Artist/> tags are also automatically hidden by SillyTavern
|
||
|
||
// Update the message in chat history
|
||
lastMessage.mes = cleanedMessage.trim();
|
||
|
||
// Update the swipe text as well
|
||
if (lastMessage.swipes && lastMessage.swipes[currentSwipeId] !== undefined) {
|
||
lastMessage.swipes[currentSwipeId] = cleanedMessage.trim();
|
||
}
|
||
|
||
// Render the updated data FIRST (before cleaning DOM)
|
||
renderUserStats();
|
||
renderInfoBox();
|
||
renderThoughts();
|
||
renderInventory();
|
||
renderQuests();
|
||
renderMusicPlayer($musicPlayerContainer[0]);
|
||
|
||
// Update FAB widgets and strip widgets with newly parsed data
|
||
updateFabWidgets();
|
||
updateStripWidgets();
|
||
|
||
// Then update the DOM to reflect the cleaned message
|
||
// Using updateMessageBlock to perform macro substitutions + regex formatting
|
||
const messageId = chat.length - 1;
|
||
updateMessageBlock(messageId, lastMessage, { rerenderMessage: true });
|
||
|
||
// console.log('[RPG Companion] Cleaned message, removed tracker code blocks from DOM');
|
||
|
||
// Save to chat metadata
|
||
saveChatData();
|
||
}
|
||
} else if (extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') {
|
||
// In separate/external mode, also parse Spotify URLs from the main roleplay response
|
||
const lastMessage = chat[chat.length - 1];
|
||
if (lastMessage && !lastMessage.is_user) {
|
||
const responseText = lastMessage.mes;
|
||
|
||
// Parse and store Spotify URL
|
||
const foundSpotifyUrl = parseAndStoreSpotifyUrl(responseText);
|
||
|
||
// No need to clean message - SillyTavern auto-hides <Song - Artist/> tags
|
||
if (foundSpotifyUrl && extensionSettings.enableSpotifyMusic) {
|
||
// Just render the music player
|
||
renderMusicPlayer($musicPlayerContainer[0]);
|
||
}
|
||
|
||
// When auto-update is disabled, no tracker API call will run for this message.
|
||
// Inherit the prior assistant message's tracker data into this swipe slot so that
|
||
// commitTrackerDataFromPriorMessage can find a valid state next turn instead of nulling everything
|
||
if (!extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
||
inheritSwipeDataFromPriorMessage(lastMessage, chat.length - 1);
|
||
}
|
||
}
|
||
|
||
// Trigger auto-update if enabled (for both separate and external modes)
|
||
// Only trigger if this is a newly generated message, not loading chat history
|
||
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
||
// Capture the current generation ID before the async gap so that any
|
||
// message deletion (or a newer generation) that increments the counter
|
||
// while the 500ms timer or the API call is in-flight will cause
|
||
// updateRPGData to discard its result rather than stomping the UI.
|
||
const genId = incrementSeparateGenerationId();
|
||
setTimeout(async () => {
|
||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, genId);
|
||
// Update FAB widgets and strip widgets after separate/external mode update completes
|
||
setFabLoadingState(false);
|
||
updateFabWidgets();
|
||
updateStripWidgets();
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
// Reset the awaiting flag after processing the message
|
||
setIsAwaitingNewMessage(false);
|
||
|
||
// Reset the swipe flag after generation completes
|
||
// This ensures that if the user swiped → auto-reply generated → flag is now cleared
|
||
// so the next user message will be treated as a new message (not a swipe)
|
||
if (lastActionWasSwipe) {
|
||
// console.log('[RPG Companion] 🔄 Generation complete after swipe - resetting lastActionWasSwipe to false');
|
||
setLastActionWasSwipe(false);
|
||
}
|
||
|
||
// Clear plot progression flag if this was a plot progression generation
|
||
// Note: No need to clear extension prompt since we used quiet_prompt option
|
||
if (isPlotProgression) {
|
||
setIsPlotProgression(false);
|
||
// console.log('[RPG Companion] Plot progression generation completed');
|
||
}
|
||
|
||
// Stop FAB loading state and update widgets
|
||
setFabLoadingState(false);
|
||
updateFabWidgets();
|
||
updateStripWidgets();
|
||
|
||
// Re-apply checkpoint in case SillyTavern unhid messages during generation
|
||
await restoreCheckpointOnLoad();
|
||
}
|
||
|
||
/**
|
||
* Event handler for character change.
|
||
*/
|
||
export function onCharacterChanged() {
|
||
// Remove thought panel and icon when changing characters
|
||
$('#rpg-thought-panel').remove();
|
||
$('#rpg-thought-icon').remove();
|
||
$('#chat').off('scroll.thoughtPanel');
|
||
$(window).off('resize.thoughtPanel');
|
||
$(document).off('click.thoughtPanel');
|
||
|
||
// Auto-switch to the preset associated with this character/group (if any)
|
||
const presetSwitched = autoSwitchPresetForEntity();
|
||
// if (presetSwitched) {
|
||
// console.log('[RPG Companion] Auto-switched preset for character');
|
||
// }
|
||
|
||
// Load chat-specific data when switching chats
|
||
loadChatData();
|
||
|
||
// chat_metadata may not reflect the actual chat tail for branches, so
|
||
// loadChatData() may have just restored stale data from the parent chat.
|
||
// Override lastGeneratedData from the swipe store of the last assistant message.
|
||
// The message objects in the branch already carry their full swipe stores, making this authoritative.
|
||
// If no swipe data exists (e.g. branching at message 0, or a chat with no generations yet),
|
||
// null out lastGeneratedData and committedTrackerData so we don't display stale values from the parent chat.
|
||
const hadSwipeData = syncLastGeneratedDataFromSwipeStore(getContext().chat);
|
||
if (!hadSwipeData) {
|
||
lastGeneratedData.userStats = null;
|
||
lastGeneratedData.infoBox = null;
|
||
lastGeneratedData.characterThoughts = null;
|
||
committedTrackerData.userStats = null;
|
||
committedTrackerData.infoBox = null;
|
||
committedTrackerData.characterThoughts = null;
|
||
}
|
||
|
||
// Don't call commitTrackerData() here - it would overwrite the loaded committedTrackerData
|
||
// with data from the last message, which may be null/empty. The loaded committedTrackerData
|
||
// already contains the committed state from when we last left this chat.
|
||
// commitTrackerData() will be called naturally when new messages arrive.
|
||
|
||
// Re-render with the loaded data
|
||
renderUserStats();
|
||
renderInfoBox();
|
||
renderThoughts();
|
||
renderInventory();
|
||
renderQuests();
|
||
renderMusicPlayer($musicPlayerContainer[0]);
|
||
|
||
// Update FAB widgets and strip widgets with loaded data
|
||
updateFabWidgets();
|
||
updateStripWidgets();
|
||
|
||
// Update chat thought overlays
|
||
updateChatThoughts();
|
||
|
||
// Update checkpoint indicators for the loaded chat
|
||
updateAllCheckpointIndicators();
|
||
}
|
||
|
||
/**
|
||
* Event handler for when a message is swiped.
|
||
* Loads the RPG data for the swipe the user navigated to.
|
||
*/
|
||
export function onMessageSwiped(messageIndex) {
|
||
if (!extensionSettings.enabled) {
|
||
return;
|
||
}
|
||
|
||
// console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped at index:', messageIndex);
|
||
|
||
// Get the message that was swiped
|
||
const message = chat[messageIndex];
|
||
if (!message || message.is_user) {
|
||
// console.log('[RPG Companion] 🔵 Ignoring swipe - message is user or undefined');
|
||
return;
|
||
}
|
||
|
||
const currentSwipeId = message.swipe_id || 0;
|
||
|
||
// Only set flag to true if this swipe will trigger a NEW generation
|
||
// Check if the swipe already exists (has content in the swipes array)
|
||
const isExistingSwipe = message.swipes &&
|
||
message.swipes[currentSwipeId] !== undefined &&
|
||
message.swipes[currentSwipeId] !== null &&
|
||
message.swipes[currentSwipeId].length > 0;
|
||
|
||
if (!isExistingSwipe) {
|
||
// This is a NEW swipe that will trigger generation
|
||
setLastActionWasSwipe(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');
|
||
} else {
|
||
// This is navigating to an EXISTING swipe - don't change the flag
|
||
// console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe);
|
||
}
|
||
|
||
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
||
|
||
// Load saved swipe data into both display (lastGeneratedData) and extensionSettings.
|
||
// Safe to call parseUserStats() unconditionally because updateMessageSwipeData() is called
|
||
// on every manual edit, so the swipe store always reflects the latest user changes before
|
||
// any navigation can overwrite them.
|
||
const swipeData = getSwipeData(message, currentSwipeId);
|
||
if (swipeData) {
|
||
// Load swipe data into lastGeneratedData for display (both modes)
|
||
lastGeneratedData.userStats = swipeData.userStats || null;
|
||
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||
|
||
// Normalize characterThoughts to string format (for backward compatibility with old object format)
|
||
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
|
||
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
|
||
} else {
|
||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||
}
|
||
|
||
// Sync extensionSettings.userStats so stat bars reflect this swipe
|
||
if (swipeData.userStats) {
|
||
parseUserStats(swipeData.userStats);
|
||
}
|
||
|
||
// console.log('[RPG Companion] 🔄 Loaded swipe data for swipe:', currentSwipeId);
|
||
} else {
|
||
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
||
}
|
||
|
||
// Re-render the panels
|
||
renderUserStats();
|
||
renderInfoBox();
|
||
renderThoughts();
|
||
renderInventory();
|
||
renderQuests();
|
||
renderMusicPlayer($musicPlayerContainer[0]);
|
||
|
||
// Update widget strips with the newly loaded swipe data
|
||
updateFabWidgets();
|
||
updateStripWidgets();
|
||
|
||
// Update chat thought overlays
|
||
updateChatThoughts();
|
||
}
|
||
|
||
/**
|
||
* Event handler for when a message is deleted.
|
||
* Re-syncs lastGeneratedData, committedTrackerData, and all UI panels to the
|
||
* new last assistant message's active swipe — or clears everything if no
|
||
* assistant messages remain.
|
||
*/
|
||
export function onMessageDeleted() {
|
||
if (!extensionSettings.enabled) return;
|
||
|
||
// console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted');
|
||
|
||
// Invalidate any pending or in-flight separate-mode generation so
|
||
// its result is not applied to the (now-changed) chat tail.
|
||
incrementSeparateGenerationId();
|
||
|
||
const currentChat = getContext().chat;
|
||
|
||
// Walk backward to find the new last assistant message.
|
||
let lastAssistantIndex = -1;
|
||
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||
lastAssistantIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (lastAssistantIndex === -1) {
|
||
// No assistant messages remain — clear all state.
|
||
lastGeneratedData.userStats = null;
|
||
lastGeneratedData.infoBox = null;
|
||
lastGeneratedData.characterThoughts = null;
|
||
committedTrackerData.userStats = null;
|
||
committedTrackerData.infoBox = null;
|
||
committedTrackerData.characterThoughts = null;
|
||
// console.log('[RPG Companion] 🗑️ No assistant messages remain — cleared all tracker state.');
|
||
} else {
|
||
// Restore display state from the new tail message's active swipe.
|
||
// If the message has no swipe data yet, null the fields so we
|
||
// don't show stale data from the deleted message.
|
||
const hadSwipeData = syncLastGeneratedDataFromSwipeStore(currentChat);
|
||
if (!hadSwipeData) {
|
||
lastGeneratedData.userStats = null;
|
||
lastGeneratedData.infoBox = null;
|
||
lastGeneratedData.characterThoughts = null;
|
||
committedTrackerData.userStats = null;
|
||
committedTrackerData.infoBox = null;
|
||
committedTrackerData.characterThoughts = null;
|
||
// console.log('[RPG Companion] 🗑️ No swipe data for last assistant message — cleared display state.');
|
||
}
|
||
|
||
// Commit context from the message *before* the new tail assistant message,
|
||
// so any subsequent generation uses the correct N-1 world state.
|
||
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||
}
|
||
|
||
// Re-render all panels.
|
||
renderUserStats();
|
||
renderInfoBox();
|
||
renderThoughts();
|
||
renderInventory();
|
||
renderQuests();
|
||
renderMusicPlayer($musicPlayerContainer[0]);
|
||
|
||
// Update widget strips.
|
||
updateFabWidgets();
|
||
updateStripWidgets();
|
||
|
||
// Persist updated state.
|
||
saveChatData();
|
||
}
|
||
|
||
/**
|
||
* Update the persona avatar image when user switches personas
|
||
*/
|
||
export function updatePersonaAvatar() {
|
||
const portraitImg = document.querySelector('.rpg-user-portrait');
|
||
if (!portraitImg) {
|
||
// console.log('[RPG Companion] Portrait image element not found in DOM');
|
||
return;
|
||
}
|
||
|
||
// Get current user_avatar from context instead of using imported value
|
||
const context = getContext();
|
||
const currentUserAvatar = context.user_avatar || user_avatar;
|
||
|
||
// console.log('[RPG Companion] Attempting to update persona avatar:', currentUserAvatar);
|
||
|
||
// Try to get a valid thumbnail URL using our safe helper
|
||
if (currentUserAvatar) {
|
||
const thumbnailUrl = getSafeThumbnailUrl('persona', currentUserAvatar);
|
||
|
||
if (thumbnailUrl) {
|
||
// Only update the src if we got a valid URL
|
||
portraitImg.src = thumbnailUrl;
|
||
// console.log('[RPG Companion] Persona avatar updated successfully');
|
||
} else {
|
||
// Don't update the src if we couldn't get a valid URL
|
||
// This prevents 400 errors and keeps the existing image
|
||
// console.warn('[RPG Companion] Could not get valid thumbnail URL for persona avatar, keeping existing image');
|
||
}
|
||
} else {
|
||
// console.log('[RPG Companion] No user avatar configured, keeping existing image');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clears all extension prompts.
|
||
*/
|
||
export function clearExtensionPrompts() {
|
||
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
|
||
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||
// Note: rpg-companion-plot is not cleared here since it's passed via quiet_prompt option
|
||
// console.log('[RPG Companion] Cleared all extension prompts');
|
||
}
|
||
|
||
/**
|
||
* Event handler for when generation stops or ends
|
||
* Re-applies checkpoint if SillyTavern unhid messages
|
||
*/
|
||
export async function onGenerationEnded() {
|
||
// console.log('[RPG Companion] 🏁 onGenerationEnded called');
|
||
|
||
// Note: isGenerating flag is cleared in onMessageReceived after parsing (together mode)
|
||
// or in apiClient.js after separate generation completes (separate mode)
|
||
|
||
// SillyTavern may auto-unhide messages when generation stops
|
||
// Re-apply checkpoint if one exists
|
||
await restoreCheckpointOnLoad();
|
||
}
|
||
|
||
/**
|
||
* Initialize history injection event listeners.
|
||
* Should be called once during extension initialization.
|
||
*/
|
||
export function initHistoryInjection() {
|
||
initHistoryInjectionListeners();
|
||
}
|