feat: extract generation and parsing systems into modules

Extract AI generation and parsing logic from monolithic index.js into
modular architecture under src/systems/generation/.

**Modules Created:**
- promptBuilder.js (319 lines) - AI prompt generation functions
- parser.js (152 lines) - Response parsing and stats extraction
- apiClient.js (154 lines) - Separate mode API call handler
- injector.js (216 lines) - Prompt injection for both modes

**Changes:**
- All functions preserve exact behavior from original
- Import paths calculated for browser module resolution
- Zero functionality changes, pure code organization

Reduces index.js by ~700 lines when combined with function removal
(to be committed separately).
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-17 10:38:35 +11:00
parent 5c34407d2c
commit 17736d9140
5 changed files with 856 additions and 725 deletions
+14 -725
View File
@@ -36,6 +36,18 @@ import {
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js';
import { on as eventOn, event_types as coreEventTypes } from './src/core/events.js';
// Generation & Parsing modules
import {
generateTrackerExample,
generateTrackerInstructions,
generateContextualSummary,
generateRPGPromptText,
generateSeparateUpdatePrompt
} from './src/systems/generation/promptBuilder.js';
import { parseResponse, parseUserStats } from './src/systems/generation/parser.js';
import { updateRPGData } from './src/systems/generation/apiClient.js';
import { onGenerationStarted } from './src/systems/generation/injector.js';
// Old state variable declarations removed - now imported from core modules
// (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js)
@@ -276,7 +288,7 @@ async function initUI() {
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
return;
}
await updateRPGData();
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts);
});
$('#rpg-stat-bar-color-low').on('change', function() {
@@ -2058,534 +2070,6 @@ function updateGenerationModeUI() {
}
}
/**
* Generates just the example portion - previous tracker data without tags or explanations.
* This will be appended to the last assistant message to show the format.
* Each section is wrapped in markdown code blocks.
*/
function generateTrackerExample() {
let example = '';
// Use COMMITTED data for generation context, not displayed data
// Wrap each tracker section in markdown code blocks
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
example += '```\n' + committedTrackerData.userStats + '\n```\n\n';
}
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
example += '```\n' + committedTrackerData.infoBox + '\n```\n\n';
}
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
example += '```\n' + committedTrackerData.characterThoughts + '\n```';
}
return example.trim();
}
/**
* Generates the instruction portion - format specifications and guidelines.
* @param {boolean} includeHtmlPrompt - Whether to include the HTML prompt (true for main generation, false for separate tracker generation)
* @param {boolean} includeContinuation - Whether to include "After updating the trackers, continue..." instruction
*/
function generateTrackerInstructions(includeHtmlPrompt = true, includeContinuation = true) {
const userName = getContext().name1;
const classicStats = extensionSettings.classicStats;
let instructions = '';
// Check if any trackers are enabled
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts;
// Only add tracker instructions if at least one tracker is enabled
if (hasAnyTrackers) {
// Universal instruction header
instructions += `\nYou must start your response with an appropriate update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with proper numbers and placeholders in [brackets] with in-world details ${userName} perceives about the current scene and the present characters. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences:\n`;
// Add format specifications for each enabled tracker
if (extensionSettings.showUserStats) {
instructions += '```\n';
instructions += `${userName}'s Stats\n`;
instructions += '---\n';
instructions += '- Health: X%\n';
instructions += '- Satiety: X%\n';
instructions += '- Energy: X%\n';
instructions += '- Hygiene: X%\n';
instructions += '- Arousal: X%\n';
instructions += '[Mood Emoji]: [Conditions (up to three traits)]\n';
instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items/none)]\n';
instructions += '```\n\n';
}
if (extensionSettings.showInfoBox) {
instructions += '```\n';
instructions += 'Info Box\n';
instructions += '---\n';
instructions += '🗓️: [Weekday, Month, Year]\n';
instructions += '[Weather Emoji]: [Forecast]\n';
instructions += '🌡️: [Temperature in °C]\n';
instructions += '🕒: [Time Start → Time End]\n';
instructions += '🗺️: [Location]\n';
instructions += '```\n\n';
}
if (extensionSettings.showCharacterThoughts) {
instructions += '```\n';
instructions += 'Present Characters\n';
instructions += '---\n';
instructions += `[Present Character's Emoji (do not include ${userName}; state "Unavailable" if no major characters are present in the scene)]: [Name, Visible Physical State (up to three traits), Observable Demeanor Cue (one trait)] | [Enemy/Neutral/Friend/Lover] | [Internal Monologue (in first person POV, up to three sentences long)]\n`;
instructions += '```\n\n';
}
// Only add continuation instruction if includeContinuation is true
if (includeContinuation) {
instructions += `After updating the trackers, continue directly from where the last message in the chat history left off. Ensure the trackers you provide naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, a character's emotional state coloring their responses, and so on.\n\n`;
}
// Include attributes and dice roll only if there was a dice roll
if (extensionSettings.lastDiceRoll) {
const roll = extensionSettings.lastDiceRoll;
instructions += `${userName}'s attributes: STR ${classicStats.str}, DEX ${classicStats.dex}, CON ${classicStats.con}, INT ${classicStats.int}, WIS ${classicStats.wis}, CHA ${classicStats.cha}\n`;
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
}
}
// Append HTML prompt if enabled AND includeHtmlPrompt is true
if (extensionSettings.enableHtmlPrompt && includeHtmlPrompt) {
// Add newlines only if we had tracker instructions
if (hasAnyTrackers) {
instructions += ``;
} else {
instructions += `\n`;
}
instructions += `If appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response:
- Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on.
- Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible.
- Embed all resources directly (e.g., inline SVGs) so nothing relies on external fonts or libraries.
- Place elements naturally in the narrative where characters would see or use them, with no limits on format or application.
- These HTML/CSS/JS elements must be rendered directly without enclosing them in code fences.`;
}
return instructions;
}
/**
* Generates a formatted contextual summary for SEPARATE mode injection.
* This creates a hybrid summary with clean formatting for main roleplay generation.
*/
function generateContextualSummary() {
// Use COMMITTED data for generation context, not displayed data
const userName = getContext().name1;
let summary = '';
// console.log('[RPG Companion] generateContextualSummary called');
// console.log('[RPG Companion] committedTrackerData.userStats:', committedTrackerData.userStats);
// console.log('[RPG Companion] extensionSettings.userStats:', JSON.stringify(extensionSettings.userStats));
// Parse the data into readable format
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
const stats = extensionSettings.userStats;
// console.log('[RPG Companion] Building stats summary with:', stats);
summary += `${userName}'s Stats:\n`;
summary += `Condition: Health ${stats.health}%, Satiety ${stats.satiety}%, Energy ${stats.energy}%, Hygiene ${stats.hygiene}%, Arousal ${stats.arousal}% | ${stats.mood} ${stats.conditions}\n`;
if (stats.inventory && stats.inventory !== 'None') {
summary += `Inventory: ${stats.inventory}\n`;
}
// Include classic stats (attributes) and dice roll only if there was a dice roll
if (extensionSettings.lastDiceRoll) {
const classicStats = extensionSettings.classicStats;
const roll = extensionSettings.lastDiceRoll;
summary += `Attributes: STR ${classicStats.str}, DEX ${classicStats.dex}, CON ${classicStats.con}, INT ${classicStats.int}, WIS ${classicStats.wis}, CHA ${classicStats.cha}\n`;
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeed or fail the action they attempt.\n`;
}
summary += `\n`;
}
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
// Parse info box data
const lines = committedTrackerData.infoBox.split('\n');
let date = '', weather = '', temp = '', time = '', location = '';
// console.log('[RPG Companion] 🔍 Parsing Info Box lines:', lines);
for (const line of lines) {
// console.log('[RPG Companion] 🔍 Processing line:', line);
// Use separate if statements (not else if) so each line is checked against all conditions
if (line.includes('🗓️:')) {
date = line.replace('🗓️:', '').trim();
// console.log('[RPG Companion] 📅 Found date:', date);
}
if (line.includes('🌡️:')) {
temp = line.replace('🌡️:', '').trim();
// console.log('[RPG Companion] 🌡️ Found temp:', temp);
}
if (line.includes('🕒:')) {
time = line.replace('🕒:', '').trim();
// console.log('[RPG Companion] 🕒 Found time:', time);
}
if (line.includes('🗺️:')) {
location = line.replace('🗺️:', '').trim();
// console.log('[RPG Companion] 🗺️ Found location:', location);
}
// Check for weather emojis - use a simpler approach
const weatherEmojis = ['🌤️', '☀️', '⛅', '🌦️', '🌧️', '⛈️', '🌩️', '🌨️', '❄️', '🌫️'];
const startsWithWeatherEmoji = weatherEmojis.some(emoji => line.startsWith(emoji + ':'));
if (startsWithWeatherEmoji && !line.includes('🌡️') && !line.includes('🗺️')) {
// Extract weather description (remove emoji and colon)
weather = line.substring(line.indexOf(':') + 1).trim();
// console.log('[RPG Companion] 🌧️ Found weather:', weather);
}
}
// console.log('[RPG Companion] 🔍 Parsed values - date:', date, 'weather:', weather, 'temp:', temp, 'time:', time, 'location:', location);
if (date || weather || temp || time || location) {
summary += `Information:\n`;
summary += `Scene: `;
if (date) summary += `${date}`;
if (location) summary += ` | ${location}`;
if (time) summary += ` | ${time}`;
if (weather) summary += ` | ${weather}`;
if (temp) summary += ` | ${temp}`;
summary += `\n\n`;
}
}
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
const lines = committedTrackerData.characterThoughts.split('\n').filter(l => l.trim() && !l.includes('---') && !l.includes('Present Characters'));
if (lines.length > 0 && !lines[0].toLowerCase().includes('unavailable')) {
summary += `Present Characters And Their Thoughts:\n`;
for (const line of lines) {
const parts = line.split('|').map(p => p.trim());
if (parts.length >= 3) {
const nameAndState = parts[0]; // Emoji, name, physical state, demeanor
const relationship = parts[1];
const thoughts = parts[2];
summary += `${nameAndState} (${relationship}) | ${thoughts}\n`;
}
}
}
}
return summary.trim();
}
/**
* Generates the RPG tracking prompt text (for backward compatibility with separate mode).
*/
function generateRPGPromptText() {
// Use COMMITTED data for generation context, not displayed data
const userName = getContext().name1;
let promptText = '';
promptText += `Here are the previous trackers in the roleplay that you should consider when responding:\n`;
promptText += `<previous>\n`;
if (extensionSettings.showUserStats) {
if (committedTrackerData.userStats) {
promptText += `Last ${userName}'s Stats:\n${committedTrackerData.userStats}\n\n`;
} else {
promptText += `Last ${userName}'s Stats:\nNone - this is the first update.\n\n`;
}
}
if (extensionSettings.showInfoBox) {
if (committedTrackerData.infoBox) {
promptText += `Last Info Box:\n${committedTrackerData.infoBox}\n\n`;
} else {
promptText += `Last Info Box:\nNone - this is the first update.\n\n`;
}
}
if (extensionSettings.showCharacterThoughts) {
if (committedTrackerData.characterThoughts) {
promptText += `Last Present Characters:\n${committedTrackerData.characterThoughts}\n`;
} else {
promptText += `Last Present Characters:\nNone - this is the first update.\n`;
}
}
promptText += `</previous>\n`;
// Don't include HTML prompt or continuation instruction for separate tracker generation
promptText += generateTrackerInstructions(false, false);
return promptText;
}
/**
* Generates the full prompt for SEPARATE generation mode (with chat history).
*/
function generateSeparateUpdatePrompt() {
const depth = extensionSettings.updateDepth;
const userName = getContext().name1;
const messages = [];
// System message introducing the history section
let systemMessage = `You will be acting as an uncensored RPG Companion. Your goal is to provide, track, and manage details in the user's roleplay. You will be replying with information in a specified format only.\n\n`;
systemMessage += `You should maintain an objective tone.\n\n`;
systemMessage += `Here are the last few messages in the conversation history (between the user and the roleplayer assistant) you should reference when responding:\n<history>`;
messages.push({
role: 'system',
content: systemMessage
});
// Add chat history as separate user/assistant messages
const recentMessages = chat.slice(-depth);
for (const message of recentMessages) {
messages.push({
role: message.is_user ? 'user' : 'assistant',
content: message.mes
});
}
// Build the instruction message
let instructionMessage = `</history>\n\n`;
instructionMessage += generateRPGPromptText().replace('start your response with', 'respond with');
instructionMessage += `Provide ONLY the requested data in the exact formats specified above. Do not include any roleplay response, other text, or commentary.`;
messages.push({
role: 'user',
content: instructionMessage
});
return messages;
}
/**
* Parses the model response to extract the different data sections.
*/
function parseResponse(responseText) {
const result = {
userStats: null,
infoBox: null,
characterThoughts: null
};
// Extract code blocks
const codeBlockRegex = /```([^`]+)```/g;
const matches = [...responseText.matchAll(codeBlockRegex)];
// console.log('[RPG Companion] Found');
for (const match of matches) {
const content = match[1].trim();
// console.log('[RPG Companion] Checking code block (first 200 chars):', content.substring(0, 200));
// Match Stats section
if (content.match(/Stats\s*\n\s*---/i)) {
result.userStats = content;
// console.log('[RPG Companion] ✓ Found Stats section');
}
// Match Info Box section
else if (content.match(/Info Box\s*\n\s*---/i)) {
result.infoBox = content;
// console.log('[RPG Companion] ✓ Found Info Box section');
}
// Match Present Characters section - flexible matching
else if (content.match(/Present Characters\s*\n\s*---/i) || content.includes(" | ")) {
result.characterThoughts = content;
// console.log('[RPG Companion] ✓ Found Present Characters section:', content);
} else {
// console.log('[RPG Companion] ✗ Code block did not match any section');
}
}
// console.log('[RPG Companion] Parse results:', {
// hasStats: !!result.userStats,
// hasInfoBox: !!result.infoBox,
// hasThoughts: !!result.characterThoughts
// });
return result;
}
/**
* Main function to update RPG data by calling the AI model (SEPARATE MODE ONLY).
*/
async function updateRPGData() {
if (isGenerating) {
// console.log('[RPG Companion] Already generating, skipping...');
return;
}
if (!extensionSettings.enabled) {
return;
}
if (extensionSettings.generationMode !== 'separate') {
// console.log('[RPG Companion] Not in separate mode, skipping manual update');
return;
}
try {
isGenerating = true;
// Update button to show "Updating..." state
const $updateBtn = $('#rpg-manual-update');
const originalHtml = $updateBtn.html();
$updateBtn.html('<i class="fa-solid fa-spinner fa-spin"></i> Updating...').prop('disabled', true);
const prompt = generateSeparateUpdatePrompt();
// Generate using raw prompt (uses current preset, no chat history)
const response = await generateRaw({
prompt: prompt,
quietToLoud: false
});
if (response) {
// console.log('[RPG Companion] Raw AI response:', response);
const parsedData = parseResponse(response);
// console.log('[RPG Companion] Parsed data:', parsedData);
// console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null');
// DON'T update lastGeneratedData here - it should only reflect the data
// from the assistant message the user replied to, not auto-generated updates
// This ensures swipes/regenerations use consistent source data
// Store RPG data for the last assistant message (separate mode)
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
// console.log('[RPG Companion] Last message is_user:', lastMessage ? lastMessage.is_user : 'no message');
if (lastMessage && !lastMessage.is_user) {
if (!lastMessage.extra) {
lastMessage.extra = {};
}
if (!lastMessage.extra.rpg_companion_swipes) {
lastMessage.extra.rpg_companion_swipes = {};
}
const currentSwipeId = lastMessage.swipe_id || 0;
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
userStats: parsedData.userStats,
infoBox: parsedData.infoBox,
characterThoughts: parsedData.characterThoughts
};
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
// Update lastGeneratedData for display AND future commit
if (parsedData.userStats) {
lastGeneratedData.userStats = parsedData.userStats;
parseUserStats(parsedData.userStats);
}
if (parsedData.infoBox) {
lastGeneratedData.infoBox = parsedData.infoBox;
}
if (parsedData.characterThoughts) {
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
}
// console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', {
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
// });
// If there's no committed data yet (first time) or only has placeholder data, commit immediately
const hasNoRealData = !committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts;
const hasOnlyPlaceholderData = (
(!committedTrackerData.userStats || committedTrackerData.userStats === '') &&
(!committedTrackerData.infoBox || committedTrackerData.infoBox === 'Info Box\n---\n' || committedTrackerData.infoBox === '') &&
(!committedTrackerData.characterThoughts || committedTrackerData.characterThoughts === 'Present Characters\n---\n' || committedTrackerData.characterThoughts === '')
);
if (hasNoRealData || hasOnlyPlaceholderData) {
committedTrackerData.userStats = parsedData.userStats;
committedTrackerData.infoBox = parsedData.infoBox;
committedTrackerData.characterThoughts = parsedData.characterThoughts;
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
}
// Render the updated data
renderUserStats();
renderInfoBox();
renderThoughts();
} else {
// No assistant message to attach to - just update display
if (parsedData.userStats) {
parseUserStats(parsedData.userStats);
}
renderUserStats();
renderInfoBox();
renderThoughts();
}
// Save to chat metadata
saveChatData();
}
} catch (error) {
console.error('[RPG Companion] Error updating RPG data:', error);
} finally {
isGenerating = false;
// Restore button to original state
const $updateBtn = $('#rpg-manual-update');
$updateBtn.html('<i class="fa-solid fa-sync"></i> Refresh RPG Info').prop('disabled', false);
// Reset the flag after tracker generation completes
// This ensures the flag persists through both main generation AND tracker generation
// console.log('[RPG Companion] 🔄 Tracker generation complete - resetting lastActionWasSwipe to false');
lastActionWasSwipe = false;
}
}
/**
* Parses user stats from the text and updates the settings.
*/
function parseUserStats(statsText) {
try {
// Extract percentages and mood/conditions
const healthMatch = statsText.match(/Health:\s*(\d+)%/);
const satietyMatch = statsText.match(/Satiety:\s*(\d+)%/);
const energyMatch = statsText.match(/Energy:\s*(\d+)%/);
const hygieneMatch = statsText.match(/Hygiene:\s*(\d+)%/);
const arousalMatch = statsText.match(/Arousal:\s*(\d+)%/);
// Match new format: [Emoji]: [Conditions]
// Look for a line after Arousal that has format [something]: [text]
// Split by lines and find the line after percentages
const lines = statsText.split('\n');
let moodMatch = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip lines with percentages or "Inventory:"
if (line.includes('%') || line.toLowerCase().startsWith('inventory:')) continue;
// Match emoji followed by colon and conditions
const match = line.match(/^(.+?):\s*(.+)$/);
if (match) {
moodMatch = match;
break;
}
}
// Extract inventory
const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i);
if (healthMatch) extensionSettings.userStats.health = parseInt(healthMatch[1]);
if (satietyMatch) extensionSettings.userStats.satiety = parseInt(satietyMatch[1]);
if (energyMatch) extensionSettings.userStats.energy = parseInt(energyMatch[1]);
if (hygieneMatch) extensionSettings.userStats.hygiene = parseInt(hygieneMatch[1]);
if (arousalMatch) extensionSettings.userStats.arousal = parseInt(arousalMatch[1]);
if (moodMatch) {
extensionSettings.userStats.mood = moodMatch[1].trim(); // Emoji
extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions
}
if (inventoryMatch) {
extensionSettings.userStats.inventory = inventoryMatch[1].trim();
}
saveSettings();
} catch (error) {
console.error('[RPG Companion] Error parsing user stats:', error);
}
}
/**
* Renders the user stats with fancy progress bars.
@@ -3964,201 +3448,6 @@ function createThoughtPanel($message, thoughtsArray) {
});
}
/**
* Event handler for when generation is about to start (TOGETHER MODE).
* Injects RPG tracking prompt into the generation.
* @param {string} type - Generation type
* @param {object} data - Generation data including quietImage flag
*/
function onGenerationStarted(type, data) {
// console.log('[RPG Companion] onGenerationStarted called');
// console.log('[RPG Companion] enabled:', extensionSettings.enabled);
// console.log('[RPG Companion] generationMode:', extensionSettings.generationMode);
// console.log('[RPG Companion] ⚡ EVENT: onGenerationStarted - lastActionWasSwipe =', lastActionWasSwipe, '| isGenerating =', isGenerating);
// Skip tracker injection for image generation requests
if (data?.quietImage) {
// console.log('[RPG Companion] Detected image generation (quietImage=true), skipping tracker injection');
return;
}
if (!extensionSettings.enabled) {
return;
}
const chat = getContext().chat;
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
// For SEPARATE mode only: Check if we need to commit extension data
// BUT: Only do this for the MAIN generation, not the tracker update generation
// If isGenerating is true, this is the tracker update generation (second call), so skip flag logic
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
if (extensionSettings.generationMode === 'separate' && !isGenerating) {
if (!lastActionWasSwipe) {
// User sent a new message - commit lastGeneratedData before generation
// console.log('[RPG Companion] 📝 COMMIT: New message - committing lastGeneratedData');
// console.log('[RPG Companion] BEFORE commit - committedTrackerData:', {
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
// 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)
}
}
// For TOGETHER mode: Check if we need to commit extension data
// Same logic as separate mode - commit on new messages, keep existing data on swipes
if (extensionSettings.generationMode === 'together') {
if (!lastActionWasSwipe) {
// User sent a new message - commit lastGeneratedData before generation
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: New message - committing lastGeneratedData');
committedTrackerData.userStats = lastGeneratedData.userStats;
committedTrackerData.infoBox = lastGeneratedData.infoBox;
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
} else {
// console.log('[RPG Companion] 🔄 TOGETHER MODE SWIPE: Using existing committedTrackerData (no commit)');
}
}
// Use the committed tracker data as source for generation
// console.log('[RPG Companion] Using committedTrackerData for generation');
// console.log('[RPG Companion] committedTrackerData.userStats:', committedTrackerData.userStats);
// Parse stats from committed data to update the extensionSettings for prompt generation
if (committedTrackerData.userStats) {
// console.log('[RPG Companion] Parsing committed userStats into extensionSettings');
parseUserStats(committedTrackerData.userStats);
// console.log('[RPG Companion] After parsing, extensionSettings.userStats:', JSON.stringify(extensionSettings.userStats));
}
if (extensionSettings.generationMode === 'together') {
// console.log('[RPG Companion] In together mode, generating prompts...');
const example = generateTrackerExample();
// Don't include HTML prompt in instructions - inject it separately to avoid duplication on swipes
const instructions = generateTrackerInstructions(false, true);
// console.log('[RPG Companion] Example:', example ? 'exists' : 'empty');
// console.log('[RPG Companion] Chat length:', chat ? chat.length : 'chat is null');
// Find the last assistant message in the chat history
let lastAssistantDepth = -1; // -1 means not found
if (chat && chat.length > 0) {
// console.log('[RPG Companion] Searching for last assistant message...');
// Start from depth 1 (skip depth 0 which is usually user's message or prefill)
for (let depth = 1; depth < chat.length; depth++) {
const index = chat.length - 1 - depth; // Convert depth to index
const message = chat[index];
// console.log('[RPG Companion] Checking depth', depth, 'index', index, 'message properties:', Object.keys(message));
// Check for assistant message: not user and not system
if (!message.is_user && !message.is_system) {
// Found assistant message at this depth
// Inject at the SAME depth to prepend to this assistant message
lastAssistantDepth = depth;
// console.log('[RPG Companion] Found last assistant message at depth', depth, '-> injecting at same depth:', lastAssistantDepth);
break;
}
}
}
// If we have previous tracker data and found an assistant message, inject it as an assistant message
if (example && lastAssistantDepth > 0) {
setExtensionPrompt('rpg-companion-example', example, extension_prompt_types.IN_CHAT, lastAssistantDepth, false, extension_prompt_roles.ASSISTANT);
// console.log('[RPG Companion] Injected tracker example as assistant message at depth:', lastAssistantDepth);
} else {
// console.log('[RPG Companion] NOT injecting example. example:', !!example, 'lastAssistantDepth:', lastAssistantDepth);
}
// Inject the instructions as a user message at depth 0 (right before generation)
setExtensionPrompt('rpg-companion-inject', instructions, extension_prompt_types.IN_CHAT, 0, false, extension_prompt_roles.USER);
// console.log('[RPG Companion] Injected RPG tracking instructions at depth 0 (right before generation)');
// Inject HTML prompt separately at depth 0 if enabled (prevents duplication on swipes)
if (extensionSettings.enableHtmlPrompt) {
const htmlPrompt = `\nIf appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response:
- Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on.
- Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible.
- Embed all resources directly (e.g., inline SVGs) so nothing relies on external fonts or libraries.
- Place elements naturally in the narrative where characters would see or use them, with no limits on format or application.
- These HTML/CSS/JS elements must be rendered directly without enclosing them in code fences.`;
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for together mode');
} else {
// Clear HTML prompt if disabled
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
}
} else if (extensionSettings.generationMode === 'separate') {
// In SEPARATE mode, inject the contextual summary for main roleplay generation
const contextSummary = generateContextualSummary();
if (contextSummary) {
const wrappedContext = `Here is context information about the current scene, and what follows is the last message in the chat history:
<context>
${contextSummary}
Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.
</context>
`;
// Inject context at depth 1 (before last user message) as SYSTEM
setExtensionPrompt('rpg-companion-context', wrappedContext, extension_prompt_types.IN_CHAT, 1, false);
// console.log('[RPG Companion] Injected contextual summary for separate mode:', contextSummary);
} else {
// Clear if no data yet
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
}
// Inject HTML prompt separately at depth 0 if enabled (same as together mode pattern)
if (extensionSettings.enableHtmlPrompt) {
const htmlPrompt = `\nIf appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response:
- Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on.
- Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible.
- Embed all resources directly (e.g., inline SVGs) so nothing relies on external fonts or libraries.
- Place elements naturally in the narrative where characters would see or use them, with no limits on format or application.
- These HTML/CSS/JS elements must be rendered directly without enclosing them in code fences.`;
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for separate mode');
} else {
// Clear HTML prompt if disabled
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Clear together mode injections
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
} else {
// Clear all injections
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-context', '', extension_prompt_types.IN_CHAT, 1, false);
}
}
/**
* 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
@@ -4296,7 +3585,7 @@ async function onMessageReceived(data) {
} else if (extensionSettings.generationMode === 'separate' && extensionSettings.autoUpdate) {
// In separate mode with auto-update, trigger update after message
setTimeout(async () => {
await updateRPGData();
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts);
}, 500);
}