Release v3.0.0 - Major update with JSON format, lock/unlock trackers, reorganized UI, colored dialogues, editable prompts, and numerous bug fixes
This commit is contained in:
@@ -23,6 +23,7 @@ import { parseResponse, parseUserStats } from './parser.js';
|
||||
import { parseAndStoreSpotifyUrl } from '../features/musicPlayer.js';
|
||||
import { renderUserStats } from '../rendering/userStats.js';
|
||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||
import { removeLocks } from './lockManager.js';
|
||||
import { renderThoughts } from '../rendering/thoughts.js';
|
||||
import { renderInventory } from '../rendering/inventory.js';
|
||||
import { renderQuests } from '../rendering/quests.js';
|
||||
@@ -235,22 +236,6 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
const updatingText = i18n.getTranslation('template.mainPanel.updating') || 'Updating...';
|
||||
$updateBtn.html(`<i class="fa-solid fa-spinner fa-spin"></i> ${updatingText}`).prop('disabled', true);
|
||||
|
||||
// Save current preset name before switching (if we're going to switch)
|
||||
// Note: Preset switching is only used in separate mode, not external mode
|
||||
if (!isExternalMode && extensionSettings.useSeparatePreset) {
|
||||
originalPresetName = await getCurrentPresetName();
|
||||
console.log(`[RPG Companion] Saved original preset: "${originalPresetName}"`);
|
||||
}
|
||||
|
||||
// Switch to separate preset if enabled (separate mode only)
|
||||
if (!isExternalMode && extensionSettings.useSeparatePreset) {
|
||||
const switched = await switchToPreset('RPG Companion Trackers');
|
||||
if (!switched) {
|
||||
console.warn('[RPG Companion] Failed to switch to RPG Companion Trackers preset. Using current preset.');
|
||||
originalPresetName = null; // Don't try to restore if we didn't switch
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = await generateSeparateUpdatePrompt();
|
||||
|
||||
// Generate response based on mode
|
||||
@@ -270,6 +255,18 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
if (response) {
|
||||
// console.log('[RPG Companion] Raw AI response:', response);
|
||||
const parsedData = parseResponse(response);
|
||||
|
||||
// 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(response);
|
||||
// console.log('[RPG Companion] Parsed data:', parsedData);
|
||||
@@ -383,13 +380,6 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
toastr.error(error.message, 'RPG Companion External API Error');
|
||||
}
|
||||
} finally {
|
||||
// Restore original preset if we switched to a separate one
|
||||
if (originalPresetName && extensionSettings.useSeparatePreset) {
|
||||
console.log(`[RPG Companion] Restoring original preset: "${originalPresetName}"`);
|
||||
await switchToPreset(originalPresetName);
|
||||
originalPresetName = null; // Clear after restoring
|
||||
}
|
||||
|
||||
setIsGenerating(false);
|
||||
|
||||
// Restore button to original state
|
||||
|
||||
@@ -8,7 +8,9 @@ import { chat, characters, this_chid, substituteParams } from '../../../../../..
|
||||
import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js';
|
||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||
import { currentEncounter } from '../features/encounterState.js';
|
||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
|
||||
import { applyLocks } from './lockManager.js';
|
||||
|
||||
/**
|
||||
* Gets character information from the current chat
|
||||
@@ -233,7 +235,9 @@ export async function buildEncounterInitPrompt() {
|
||||
if (extensionSettings.classicStats) {
|
||||
const stats = extensionSettings.classicStats;
|
||||
userStatsInfo += `${userName}'s Attributes: `;
|
||||
userStatsInfo += `STR ${stats.str}, DEX ${stats.dex}, CON ${stats.con}, INT ${stats.int}, WIS ${stats.wis}, CHA ${stats.cha}, LVL ${extensionSettings.level}\n\n`;
|
||||
const showLevel = extensionSettings.trackerConfig?.userStats?.showLevel !== false;
|
||||
const levelStr = showLevel ? `, LVL ${extensionSettings.level}` : '';
|
||||
userStatsInfo += `STR ${stats.str}, DEX ${stats.dex}, CON ${stats.con}, INT ${stats.int}, WIS ${stats.wis}, CHA ${stats.cha}${levelStr}\n\n`;
|
||||
}
|
||||
|
||||
// Add present characters info for party members
|
||||
@@ -417,7 +421,18 @@ export async function buildCombatActionPrompt(action, combatStats) {
|
||||
// Add ONLY classic stats/attributes if enabled
|
||||
if (extensionSettings.classicStats) {
|
||||
const stats = extensionSettings.classicStats;
|
||||
systemMessage += `\nAttributes: STR ${stats.str}, DEX ${stats.dex}, CON ${stats.con}, INT ${stats.int}, WIS ${stats.wis}, CHA ${stats.cha}, LVL ${extensionSettings.level}\n`;
|
||||
const config = extensionSettings.trackerConfig?.userStats;
|
||||
const rpgAttributes = (config?.rpgAttributes && config.rpgAttributes.length > 0) ? config.rpgAttributes : [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
];
|
||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||
const attributeStrings = enabledAttributes.map(attr => `${attr.name} ${stats[attr.id] || 10}`);
|
||||
systemMessage += `\nAttributes: ${attributeStrings.join(', ')}, LVL ${extensionSettings.level}\n`;
|
||||
}
|
||||
|
||||
systemMessage += `</persona>\n\n`;
|
||||
@@ -658,15 +673,24 @@ export async function buildCombatSummaryPrompt(combatLog, result) {
|
||||
summaryMessage += `<previous>\n`;
|
||||
|
||||
if (committedTrackerData.userStats) {
|
||||
summaryMessage += `${userName}'s Stats:\n${committedTrackerData.userStats}\n\n`;
|
||||
const statsJSON = typeof committedTrackerData.userStats === 'object'
|
||||
? JSON.stringify(committedTrackerData.userStats, null, 2)
|
||||
: committedTrackerData.userStats;
|
||||
summaryMessage += statsJSON + '\n';
|
||||
}
|
||||
|
||||
if (committedTrackerData.infoBox) {
|
||||
summaryMessage += `Info Box:\n${committedTrackerData.infoBox}\n\n`;
|
||||
const infoBoxJSON = typeof committedTrackerData.infoBox === 'object'
|
||||
? JSON.stringify(committedTrackerData.infoBox, null, 2)
|
||||
: committedTrackerData.infoBox;
|
||||
summaryMessage += infoBoxJSON + '\n';
|
||||
}
|
||||
|
||||
if (committedTrackerData.characterThoughts) {
|
||||
summaryMessage += `Present Characters:\n${committedTrackerData.characterThoughts}\n\n`;
|
||||
const charactersJSON = typeof committedTrackerData.characterThoughts === 'object'
|
||||
? JSON.stringify(committedTrackerData.characterThoughts, null, 2)
|
||||
: committedTrackerData.characterThoughts;
|
||||
summaryMessage += charactersJSON + '\n';
|
||||
}
|
||||
|
||||
summaryMessage += `</previous>\n\n`;
|
||||
@@ -712,7 +736,22 @@ export function parseEncounterJSON(response) {
|
||||
cleaned = cleaned.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
return JSON.parse(cleaned);
|
||||
// Try to parse directly first
|
||||
try {
|
||||
return JSON.parse(cleaned);
|
||||
} catch (initialError) {
|
||||
// If direct parsing fails, try JSON repair
|
||||
console.warn('[RPG Companion] Initial parse failed, attempting JSON repair...');
|
||||
const repaired = repairJSON(cleaned);
|
||||
|
||||
if (repaired) {
|
||||
console.log('[RPG Companion] ✓ Successfully repaired encounter JSON');
|
||||
return repaired;
|
||||
}
|
||||
|
||||
// If repair also failed, throw the original error
|
||||
throw initialError;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Failed to parse encounter JSON:', error);
|
||||
console.error('[RPG Companion] Response was:', response);
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
lastGeneratedData,
|
||||
isGenerating,
|
||||
lastActionWasSwipe,
|
||||
setLastActionWasSwipe
|
||||
setLastActionWasSwipe,
|
||||
setIsGenerating
|
||||
} from '../../core/state.js';
|
||||
import { evaluateSuppression } from './suppression.js';
|
||||
import { parseUserStats } from './parser.js';
|
||||
@@ -20,24 +21,38 @@ import {
|
||||
generateTrackerInstructions,
|
||||
generateContextualSummary,
|
||||
DEFAULT_HTML_PROMPT,
|
||||
DEFAULT_DIALOGUE_COLORING_PROMPT,
|
||||
DEFAULT_SPOTIFY_PROMPT,
|
||||
SPOTIFY_FORMAT_INSTRUCTION
|
||||
} from './promptBuilder.js';
|
||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||
|
||||
// Type imports
|
||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||
|
||||
// Track last chat length we committed at to prevent duplicate commits from streaming
|
||||
let lastCommittedChatLength = -1;
|
||||
|
||||
/**
|
||||
* Event handler for generation start.
|
||||
* Manages tracker data commitment and prompt injection based on generation mode.
|
||||
*
|
||||
* @param {string} type - Event type
|
||||
* @param {Object} data - Event data
|
||||
* @param {boolean} dryRun - If true, this is a dry run (page reload, prompt preview, etc.) - skip all logic
|
||||
*/
|
||||
export async 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);
|
||||
// console.log('[RPG Companion] Committed Prompt:', committedTrackerData);
|
||||
export async function onGenerationStarted(type, data, dryRun) {
|
||||
// Skip dry runs (page reload, prompt manager preview, etc.)
|
||||
if (dryRun) {
|
||||
console.log('[RPG Companion] Skipping onGenerationStarted: dry run detected');
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
console.log('[RPG Companion] Committed Prompt:', committedTrackerData);
|
||||
|
||||
// Skip tracker injection for image generation requests
|
||||
if (data?.quietImage) {
|
||||
@@ -46,6 +61,13 @@ export async function onGenerationStarted(type, data) {
|
||||
}
|
||||
|
||||
if (!extensionSettings.enabled) {
|
||||
// Extension is disabled - clear any existing prompts to ensure nothing is injected
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,8 +98,63 @@ export async function onGenerationStarted(type, data) {
|
||||
// Ensure checkpoint is applied before generation
|
||||
await restoreCheckpointOnLoad();
|
||||
|
||||
const currentChatLength = chat ? chat.length : 0;
|
||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||||
|
||||
// For TOGETHER mode: Commit when user sends message (before first generation)
|
||||
if (extensionSettings.generationMode === 'together') {
|
||||
// By the time onGenerationStarted fires, ST has already added the placeholder AI message
|
||||
// So we check the second-to-last message to see if user just sent a message
|
||||
const secondToLastMessage = chat && chat.length > 1 ? chat[chat.length - 2] : null;
|
||||
const isUserMessage = secondToLastMessage && secondToLastMessage.is_user;
|
||||
|
||||
// Commit if:
|
||||
// 1. Second-to-last message is from USER (user just sent message)
|
||||
// 2. Not a swipe (lastActionWasSwipe = false)
|
||||
// 3. Haven't already committed for this chat length (prevent streaming duplicates)
|
||||
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
|
||||
|
||||
if (shouldCommit) {
|
||||
console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing data from BEFORE user message');
|
||||
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)
|
||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
||||
|
||||
// Track chat length to prevent duplicate commits
|
||||
lastCommittedChatLength = currentChatLength;
|
||||
|
||||
console.log('[RPG Companion] AFTER: committedTrackerData =', {
|
||||
userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
||||
infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
||||
characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : 'null'
|
||||
});
|
||||
} else if (lastActionWasSwipe) {
|
||||
console.log('[RPG Companion] ⏭️ Skipping commit: swipe (using previous committed data)');
|
||||
} else if (!isUserMessage) {
|
||||
console.log('[RPG Companion] ⏭️ Skipping commit: second-to-last message is not user message (likely swipe or continuation)');
|
||||
}
|
||||
|
||||
console.log('[RPG Companion] 📦 TOGETHER MODE: Injecting committed tracker data into prompt');
|
||||
console.log('[RPG Companion] committedTrackerData =', {
|
||||
userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
||||
infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
||||
characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : '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
|
||||
@@ -85,83 +162,39 @@ export async function onGenerationStarted(type, data) {
|
||||
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'
|
||||
// });
|
||||
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'
|
||||
// });
|
||||
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'
|
||||
// });
|
||||
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
|
||||
// Only commit when user sends a new message (not on swipes)
|
||||
if (extensionSettings.generationMode === 'together') {
|
||||
if (!lastActionWasSwipe) {
|
||||
// User sent a new message - commit data from the last assistant message they replied to
|
||||
// This ensures swipes use consistent data from before the first swipe
|
||||
console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: New message - committing from last assistant message');
|
||||
|
||||
// Find the last assistant message (before the user's new message)
|
||||
const chat = getContext().chat;
|
||||
let foundAssistantMessage = false;
|
||||
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
const message = chat[i];
|
||||
if (!message.is_user) {
|
||||
// Found last assistant message - commit its stored tracker data
|
||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||
const swipeId = message.swipe_id || 0;
|
||||
const swipeData = message.extra.rpg_companion_swipes[swipeId];
|
||||
|
||||
if (swipeData) {
|
||||
committedTrackerData.userStats = swipeData.userStats || null;
|
||||
committedTrackerData.infoBox = swipeData.infoBox || null;
|
||||
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
|
||||
foundAssistantMessage = true;
|
||||
console.log('[RPG Companion] ✓ Committed tracker data from message swipe', swipeId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no stored data found, use lastGeneratedData (for first message)
|
||||
if (!foundAssistantMessage) {
|
||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
||||
console.log('[RPG Companion] ⚠ No stored message data found, using lastGeneratedData as fallback');
|
||||
}
|
||||
} 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);
|
||||
@@ -175,7 +208,10 @@ export async function onGenerationStarted(type, data) {
|
||||
|
||||
if (extensionSettings.generationMode === 'together') {
|
||||
// console.log('[RPG Companion] In together mode, generating prompts...');
|
||||
const example = generateTrackerExample();
|
||||
const exampleRaw = generateTrackerExample();
|
||||
// Wrap example in ```json``` code blocks for consistency with format instructions
|
||||
// Add only 1 newline after the closing ``` (ST adds its own newline when injecting)
|
||||
const example = exampleRaw ? `\`\`\`json\n${exampleRaw}\n\`\`\`\n` : null;
|
||||
// Don't include HTML prompt in instructions - inject it separately to avoid duplication on swipes
|
||||
const instructions = generateTrackerInstructions(false, true);
|
||||
|
||||
@@ -234,6 +270,19 @@ export async function onGenerationStarted(type, data) {
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Dialogue Coloring prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableDialogueColoring && !shouldSuppress) {
|
||||
// Use custom Dialogue Coloring prompt if set, otherwise use default
|
||||
const dialogueColoringPromptText = extensionSettings.customDialogueColoringPrompt || DEFAULT_DIALOGUE_COLORING_PROMPT;
|
||||
const dialogueColoringPrompt = `\n${dialogueColoringPromptText}`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-dialogue-coloring', dialogueColoringPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Dialogue Coloring prompt at depth 0 for together mode');
|
||||
} else {
|
||||
// Clear Dialogue Coloring prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Spotify prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||
// Use custom Spotify prompt if set, otherwise use default
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* JSON Prompt Builder Helpers
|
||||
* Helper functions for building JSON format tracker prompts
|
||||
*/
|
||||
|
||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
|
||||
/**
|
||||
* Converts a field name to snake_case for use as JSON key
|
||||
* Example: "Test Tracker" -> "test_tracker"
|
||||
* @param {string} name - Field name to convert
|
||||
* @returns {string} snake_case version
|
||||
*/
|
||||
function toSnakeCase(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds User Stats JSON format instruction
|
||||
* @returns {string} JSON format instruction for user stats
|
||||
*/
|
||||
export function buildUserStatsJSONInstruction() {
|
||||
const userName = getContext().name1;
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
const userStatsConfig = trackerConfig?.userStats;
|
||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
|
||||
let instruction = '{\n';
|
||||
instruction += ' "stats": [\n';
|
||||
|
||||
// Add stats dynamically
|
||||
for (let i = 0; i < enabledStats.length; i++) {
|
||||
const stat = enabledStats[i];
|
||||
const comma = i < enabledStats.length - 1 ? ',' : '';
|
||||
instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma}\n`;
|
||||
}
|
||||
|
||||
instruction += ' ],\n';
|
||||
|
||||
// Status section
|
||||
if (userStatsConfig?.statusSection?.enabled) {
|
||||
instruction += ' "status": {\n';
|
||||
if (userStatsConfig.statusSection.showMoodEmoji) {
|
||||
instruction += ' "mood": "Mood Emoji",\n';
|
||||
}
|
||||
instruction += ' "conditions": "[Condition1, Condition2]"\n';
|
||||
instruction += ' },\n';
|
||||
}
|
||||
|
||||
// Skills section
|
||||
if (userStatsConfig?.skillsSection?.enabled) {
|
||||
instruction += ' "skills": [\n';
|
||||
instruction += ' {"name": "Skill1"},\n';
|
||||
instruction += ' {"name": "Skill2"}\n';
|
||||
instruction += ' ],\n';
|
||||
}
|
||||
|
||||
// Inventory section
|
||||
if (extensionSettings.showInventory) {
|
||||
instruction += ' "inventory": {\n';
|
||||
instruction += ' "onPerson": [\n';
|
||||
instruction += ' {"name": "Item1", "quantity": X},\n';
|
||||
instruction += ' {"name": "Item2", "quantity": X}\n';
|
||||
instruction += ' ],\n';
|
||||
instruction += ' "clothing": [\n';
|
||||
instruction += ' {"name": "Clothing1"}\n';
|
||||
instruction += ' ],\n';
|
||||
instruction += ' "stored": {\n';
|
||||
instruction += ' "Location1": [\n';
|
||||
instruction += ' {"name": "Item", "quantity": X}\n';
|
||||
instruction += ' ]\n';
|
||||
instruction += ' },\n';
|
||||
instruction += ' "assets": [\n';
|
||||
instruction += ' {"name": "Asset1", "location": "Location"}\n';
|
||||
instruction += ' ]\n';
|
||||
instruction += ' },\n';
|
||||
}
|
||||
|
||||
// Quests section
|
||||
instruction += ' "quests": {\n';
|
||||
instruction += ' "main": {"title": "Quest title"},\n';
|
||||
instruction += ' "optional": [\n';
|
||||
instruction += ' {"title": "Quest1"},\n';
|
||||
instruction += ' {"title": "Quest2"}\n';
|
||||
instruction += ' ]\n';
|
||||
instruction += ' }\n';
|
||||
instruction += '}';
|
||||
|
||||
return instruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds Info Box JSON format instruction
|
||||
* @returns {string} JSON format instruction for info box
|
||||
*/
|
||||
export function buildInfoBoxJSONInstruction() {
|
||||
const infoBoxConfig = extensionSettings.trackerConfig?.infoBox;
|
||||
const widgets = infoBoxConfig?.widgets || {};
|
||||
|
||||
let instruction = '{\n';
|
||||
let hasFields = false;
|
||||
|
||||
if (widgets.date?.enabled) {
|
||||
instruction += ' "date": {"value": "Weekday, Month, Year"}';
|
||||
hasFields = true;
|
||||
}
|
||||
|
||||
if (widgets.weather?.enabled) {
|
||||
instruction += (hasFields ? ',\n' : '') + ' "weather": {"emoji": "Weather Emoji", "forecast": "Forecast"}';
|
||||
hasFields = true;
|
||||
}
|
||||
|
||||
if (widgets.temperature?.enabled) {
|
||||
const unit = widgets.temperature.unit === 'F' ? 'F' : 'C';
|
||||
instruction += (hasFields ? ',\n' : '') + ` "temperature": {"value": X, "unit": "${unit}"}`;
|
||||
hasFields = true;
|
||||
}
|
||||
|
||||
if (widgets.time?.enabled) {
|
||||
instruction += (hasFields ? ',\n' : '') + ' "time": {"start": "TimeStart", "end": "TimeEnd"}';
|
||||
hasFields = true;
|
||||
}
|
||||
|
||||
if (widgets.location?.enabled) {
|
||||
instruction += (hasFields ? ',\n' : '') + ' "location": {"value": "Location"}';
|
||||
hasFields = true;
|
||||
}
|
||||
|
||||
if (widgets.recentEvents?.enabled) {
|
||||
instruction += (hasFields ? ',\n' : '') + ' "recentEvents": ["Event1", "Event2", "Event3"]';
|
||||
hasFields = true;
|
||||
}
|
||||
|
||||
instruction += '\n}';
|
||||
return instruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds Present Characters JSON format instruction
|
||||
* @returns {string} JSON format instruction for present characters
|
||||
*/
|
||||
export function buildCharactersJSONInstruction() {
|
||||
const userName = getContext().name1;
|
||||
const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
|
||||
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
|
||||
const relationshipsEnabled = presentCharsConfig?.relationships?.enabled !== false;
|
||||
const thoughtsConfig = presentCharsConfig?.thoughts;
|
||||
const characterStats = presentCharsConfig?.characterStats;
|
||||
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
|
||||
let instruction = '[\n';
|
||||
instruction += ' {\n';
|
||||
instruction += ' "name": "CharacterName",\n';
|
||||
instruction += ' "emoji": "Character Emoji"';
|
||||
|
||||
// Details fields
|
||||
if (enabledFields.length > 0) {
|
||||
instruction += ',\n "details": {\n';
|
||||
for (let i = 0; i < enabledFields.length; i++) {
|
||||
const field = enabledFields[i];
|
||||
const fieldKey = toSnakeCase(field.name);
|
||||
const comma = i < enabledFields.length - 1 ? ',' : '';
|
||||
instruction += ` "${fieldKey}": "${field.description}"${comma}\n`;
|
||||
}
|
||||
instruction += ' }';
|
||||
}
|
||||
|
||||
// Relationship
|
||||
if (relationshipsEnabled) {
|
||||
const relationshipFields = presentCharsConfig?.relationshipFields || [];
|
||||
const options = relationshipFields.join('/');
|
||||
instruction += ',\n "relationship": {"status": "(choose one: ' + options + ')"}';
|
||||
}
|
||||
|
||||
// Stats
|
||||
if (enabledCharStats.length > 0) {
|
||||
instruction += ',\n "stats": [\n';
|
||||
for (let i = 0; i < enabledCharStats.length; i++) {
|
||||
const stat = enabledCharStats[i];
|
||||
const comma = i < enabledCharStats.length - 1 ? ',' : '';
|
||||
instruction += ` {"name": "${stat.name}", "value": X}${comma}\n`;
|
||||
}
|
||||
instruction += ' ]';
|
||||
}
|
||||
|
||||
// Thoughts
|
||||
if (thoughtsConfig?.enabled) {
|
||||
const thoughtsDescription = thoughtsConfig.description || 'Internal monologue';
|
||||
instruction += `,\n "thoughts": {"content": "${thoughtsDescription}"}`;
|
||||
}
|
||||
|
||||
instruction += '\n }\n';
|
||||
instruction += ']';
|
||||
|
||||
return instruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds lock information to instruction text
|
||||
* @param {string} baseInstruction - Base instruction text
|
||||
* @returns {string} Instruction with lock information added
|
||||
*/
|
||||
export function addLockInstruction(baseInstruction) {
|
||||
return baseInstruction + '\n\nIMPORTANT: If an item, stat, quest, or field has "locked": true in its object, you MUST NOT change its value. Keep it exactly as it appears in the previous trackers. Only unlocked items can be modified. The "locked" field should ONLY be included if the item is actually locked - omit it for unlocked items.';
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* Lock Manager
|
||||
* Handles applying and removing locks for tracker items
|
||||
* Locks prevent AI from modifying specific values
|
||||
*/
|
||||
|
||||
import { extensionSettings } from '../../core/state.js';
|
||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||
|
||||
/**
|
||||
* Apply locks to tracker data before sending to AI.
|
||||
* Adds "locked": true to locked items in JSON format.
|
||||
*
|
||||
* @param {string} trackerData - JSON string of tracker data
|
||||
* @param {string} trackerType - Type of tracker ('userStats', 'infoBox', 'characters')
|
||||
* @returns {string} Tracker data with locks applied
|
||||
*/
|
||||
export function applyLocks(trackerData, trackerType) {
|
||||
if (!trackerData) return trackerData;
|
||||
|
||||
// Try to parse as JSON
|
||||
const parsed = repairJSON(trackerData);
|
||||
if (!parsed) {
|
||||
// Not JSON format, return as-is (text format doesn't support locks)
|
||||
return trackerData;
|
||||
}
|
||||
|
||||
// Get locked items for this tracker type
|
||||
const lockedItems = extensionSettings.lockedItems?.[trackerType] || {};
|
||||
|
||||
// Apply locks based on tracker type
|
||||
switch (trackerType) {
|
||||
case 'userStats':
|
||||
return applyUserStatsLocks(parsed, lockedItems);
|
||||
case 'infoBox':
|
||||
return applyInfoBoxLocks(parsed, lockedItems);
|
||||
case 'characters':
|
||||
return applyCharactersLocks(parsed, lockedItems);
|
||||
default:
|
||||
return trackerData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply locks to User Stats tracker
|
||||
* @param {Object} data - Parsed user stats data
|
||||
* @param {Object} lockedItems - Locked items configuration
|
||||
* @returns {string} JSON string with locks applied
|
||||
*/
|
||||
function applyUserStatsLocks(data, lockedItems) {
|
||||
// Lock individual stats within stats object
|
||||
if (data.stats && lockedItems.stats) {
|
||||
// Handle both section lock and individual stat locks
|
||||
const isStatsLocked = lockedItems.stats === true;
|
||||
if (isStatsLocked) {
|
||||
// Lock entire stats section
|
||||
for (const statName in data.stats) {
|
||||
data.stats[statName] = {
|
||||
value: data.stats[statName].value || data.stats[statName],
|
||||
locked: true
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Lock individual stats
|
||||
for (const statName in lockedItems.stats) {
|
||||
if (lockedItems.stats[statName] && data.stats[statName] !== undefined) {
|
||||
data.stats[statName] = {
|
||||
value: data.stats[statName].value || data.stats[statName],
|
||||
locked: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lock status field
|
||||
if (data.status && lockedItems.status) {
|
||||
data.status = {
|
||||
...data.status,
|
||||
locked: true
|
||||
};
|
||||
}
|
||||
|
||||
// Lock individual skills
|
||||
if (data.skills && lockedItems.skills) {
|
||||
if (Array.isArray(data.skills)) {
|
||||
data.skills = data.skills.map(skill => {
|
||||
if (typeof skill === 'string') {
|
||||
if (lockedItems.skills[skill]) {
|
||||
return { name: skill, locked: true };
|
||||
}
|
||||
return skill;
|
||||
} else if (skill.name && lockedItems.skills[skill.name]) {
|
||||
return { ...skill, locked: true };
|
||||
}
|
||||
return skill;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Lock inventory items - handle bracket notation paths like "inventory.onPerson[0]"
|
||||
if (data.inventory && lockedItems.inventory) {
|
||||
// Helper function to parse bracket notation and apply lock
|
||||
const applyInventoryLocks = (items, category) => {
|
||||
if (!Array.isArray(items)) return items;
|
||||
|
||||
return items.map((item, index) => {
|
||||
// Check if this specific item is locked using bracket notation with inventory prefix
|
||||
const bracketPath = `${category}[${index}]`;
|
||||
if (lockedItems.inventory[bracketPath]) {
|
||||
return typeof item === 'string'
|
||||
? { item, locked: true }
|
||||
: { ...item, locked: true };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
// Apply locks to onPerson items
|
||||
if (data.inventory.onPerson) {
|
||||
data.inventory.onPerson = applyInventoryLocks(data.inventory.onPerson, 'onPerson');
|
||||
}
|
||||
|
||||
// Apply locks to clothing items
|
||||
if (data.inventory.clothing) {
|
||||
data.inventory.clothing = applyInventoryLocks(data.inventory.clothing, 'clothing');
|
||||
}
|
||||
|
||||
// Apply locks to assets
|
||||
if (data.inventory.assets) {
|
||||
data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets');
|
||||
}
|
||||
|
||||
// Apply locks to stored items (nested structure with inventory.stored.location[index])
|
||||
if (data.inventory.stored && lockedItems.inventory.stored) {
|
||||
for (const location in data.inventory.stored) {
|
||||
if (Array.isArray(data.inventory.stored[location])) {
|
||||
data.inventory.stored[location] = data.inventory.stored[location].map((item, index) => {
|
||||
const bracketPath = `${location}[${index}]`;
|
||||
if (lockedItems.inventory.stored[bracketPath]) {
|
||||
return typeof item === 'string'
|
||||
? { item, locked: true }
|
||||
: { ...item, locked: true };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lock individual quests - handle paths like "quests.main" and "quests.optional[0]"
|
||||
if (data.quests && lockedItems.quests) {
|
||||
// Check if main quest is locked (entire section)
|
||||
if (data.quests.main && lockedItems.quests.main === true) {
|
||||
data.quests.main = { value: data.quests.main, locked: true };
|
||||
}
|
||||
|
||||
// Check individual optional quests
|
||||
if (data.quests.optional && Array.isArray(data.quests.optional)) {
|
||||
data.quests.optional = data.quests.optional.map((quest, index) => {
|
||||
const bracketPath = `optional[${index}]`;
|
||||
if (lockedItems.quests[bracketPath]) {
|
||||
return typeof quest === 'string'
|
||||
? { title: quest, locked: true }
|
||||
: { ...quest, locked: true };
|
||||
}
|
||||
return quest;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply locks to Info Box tracker
|
||||
* @param {Object} data - Parsed info box data
|
||||
* @param {Object} lockedItems - Locked items configuration
|
||||
* @returns {string} JSON string with locks applied
|
||||
*/
|
||||
function applyInfoBoxLocks(data, lockedItems) {
|
||||
if (lockedItems.date && data.date) {
|
||||
data.date = { ...data.date, locked: true };
|
||||
}
|
||||
|
||||
if (lockedItems.weather && data.weather) {
|
||||
data.weather = { ...data.weather, locked: true };
|
||||
}
|
||||
|
||||
if (lockedItems.temperature && data.temperature) {
|
||||
data.temperature = { ...data.temperature, locked: true };
|
||||
}
|
||||
|
||||
if (lockedItems.time && data.time) {
|
||||
data.time = { ...data.time, locked: true };
|
||||
}
|
||||
|
||||
if (lockedItems.location && data.location) {
|
||||
data.location = { ...data.location, locked: true };
|
||||
}
|
||||
|
||||
if (lockedItems.recentEvents && data.recentEvents) {
|
||||
data.recentEvents = { ...data.recentEvents, locked: true };
|
||||
}
|
||||
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply locks to Characters tracker
|
||||
* @param {Object} data - Parsed characters data
|
||||
* @param {Object} lockedItems - Locked items configuration
|
||||
* @returns {string} JSON string with locks applied
|
||||
*/
|
||||
function applyCharactersLocks(data, lockedItems) {
|
||||
console.log('[Lock Manager] applyCharactersLocks called');
|
||||
console.log('[Lock Manager] Locked items:', JSON.stringify(lockedItems, null, 2));
|
||||
console.log('[Lock Manager] Input data:', JSON.stringify(data, null, 2));
|
||||
|
||||
// Handle both array format and object format
|
||||
let characters = Array.isArray(data) ? data : (data.characters || []);
|
||||
|
||||
characters = characters.map((char, index) => {
|
||||
const charName = char.name || char.characterName;
|
||||
|
||||
// Check if entire character is locked (index-based)
|
||||
if (lockedItems[index] === true) {
|
||||
console.log('[Lock Manager] Locking entire character by index:', index);
|
||||
return { ...char, locked: true };
|
||||
}
|
||||
|
||||
// Check if character name exists in locked items (could be nested object for field locks or boolean for full lock)
|
||||
const charLocks = lockedItems[charName];
|
||||
|
||||
if (charLocks === true) {
|
||||
// Entire character is locked
|
||||
console.log('[Lock Manager] Locking entire character:', charName);
|
||||
return { ...char, locked: true };
|
||||
} else if (charLocks && typeof charLocks === 'object') {
|
||||
// Character has field-level locks
|
||||
const modifiedChar = { ...char };
|
||||
|
||||
for (const fieldName in charLocks) {
|
||||
if (charLocks[fieldName] === true) {
|
||||
// Check both the original field name and snake_case version
|
||||
// (AI returns snake_case, but locks are stored with original configured names)
|
||||
// Use the same conversion as toSnakeCase in thoughts.js
|
||||
const snakeCaseFieldName = fieldName
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
|
||||
let locked = false;
|
||||
|
||||
// Check at root level first (backward compatibility)
|
||||
if (modifiedChar[fieldName] !== undefined) {
|
||||
console.log('[Lock Manager] Applying lock to field:', `${charName}.${fieldName}`);
|
||||
modifiedChar[fieldName] = {
|
||||
value: modifiedChar[fieldName],
|
||||
locked: true
|
||||
};
|
||||
locked = true;
|
||||
} else if (modifiedChar[snakeCaseFieldName] !== undefined) {
|
||||
console.log('[Lock Manager] Applying lock to snake_case field:', `${charName}.${snakeCaseFieldName} (from ${fieldName})`);
|
||||
modifiedChar[snakeCaseFieldName] = {
|
||||
value: modifiedChar[snakeCaseFieldName],
|
||||
locked: true
|
||||
};
|
||||
locked = true;
|
||||
}
|
||||
|
||||
// Check in nested objects (details, relationship, thoughts)
|
||||
if (!locked && modifiedChar.details) {
|
||||
if (modifiedChar.details[fieldName] !== undefined) {
|
||||
console.log('[Lock Manager] Applying lock to details field:', `${charName}.details.${fieldName}`);
|
||||
if (!modifiedChar.details || typeof modifiedChar.details !== 'object') {
|
||||
modifiedChar.details = {};
|
||||
} else {
|
||||
modifiedChar.details = { ...modifiedChar.details };
|
||||
}
|
||||
modifiedChar.details[fieldName] = {
|
||||
value: modifiedChar.details[fieldName],
|
||||
locked: true
|
||||
};
|
||||
locked = true;
|
||||
} else if (modifiedChar.details[snakeCaseFieldName] !== undefined) {
|
||||
console.log('[Lock Manager] Applying lock to details snake_case field:', `${charName}.details.${snakeCaseFieldName} (from ${fieldName})`);
|
||||
if (!modifiedChar.details || typeof modifiedChar.details !== 'object') {
|
||||
modifiedChar.details = {};
|
||||
} else {
|
||||
modifiedChar.details = { ...modifiedChar.details };
|
||||
}
|
||||
modifiedChar.details[snakeCaseFieldName] = {
|
||||
value: modifiedChar.details[snakeCaseFieldName],
|
||||
locked: true
|
||||
};
|
||||
locked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check in relationship object
|
||||
if (!locked && modifiedChar.relationship) {
|
||||
if (modifiedChar.relationship[fieldName] !== undefined) {
|
||||
console.log('[Lock Manager] Applying lock to relationship field:', `${charName}.relationship.${fieldName}`);
|
||||
modifiedChar.relationship = { ...modifiedChar.relationship };
|
||||
modifiedChar.relationship[fieldName] = {
|
||||
value: modifiedChar.relationship[fieldName],
|
||||
locked: true
|
||||
};
|
||||
locked = true;
|
||||
} else if (modifiedChar.relationship[snakeCaseFieldName] !== undefined) {
|
||||
console.log('[Lock Manager] Applying lock to relationship snake_case field:', `${charName}.relationship.${snakeCaseFieldName} (from ${fieldName})`);
|
||||
modifiedChar.relationship = { ...modifiedChar.relationship };
|
||||
modifiedChar.relationship[snakeCaseFieldName] = {
|
||||
value: modifiedChar.relationship[snakeCaseFieldName],
|
||||
locked: true
|
||||
};
|
||||
locked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check in thoughts object
|
||||
if (!locked && modifiedChar.thoughts) {
|
||||
if (modifiedChar.thoughts[fieldName] !== undefined) {
|
||||
console.log('[Lock Manager] Applying lock to thoughts field:', `${charName}.thoughts.${fieldName}`);
|
||||
modifiedChar.thoughts = { ...modifiedChar.thoughts };
|
||||
modifiedChar.thoughts[fieldName] = {
|
||||
value: modifiedChar.thoughts[fieldName],
|
||||
locked: true
|
||||
};
|
||||
locked = true;
|
||||
} else if (modifiedChar.thoughts[snakeCaseFieldName] !== undefined) {
|
||||
console.log('[Lock Manager] Applying lock to thoughts snake_case field:', `${charName}.thoughts.${snakeCaseFieldName} (from ${fieldName})`);
|
||||
modifiedChar.thoughts = { ...modifiedChar.thoughts };
|
||||
modifiedChar.thoughts[snakeCaseFieldName] = {
|
||||
value: modifiedChar.thoughts[snakeCaseFieldName],
|
||||
locked: true
|
||||
};
|
||||
locked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedChar;
|
||||
}
|
||||
|
||||
// No locks for this character
|
||||
return char;
|
||||
});
|
||||
|
||||
const result = Array.isArray(data)
|
||||
? JSON.stringify(characters, null, 2)
|
||||
: JSON.stringify({ ...data, characters }, null, 2);
|
||||
|
||||
console.log('[Lock Manager] Output data:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove locks from tracker data received from AI.
|
||||
* Strips "locked": true from all items to clean up the data.
|
||||
*
|
||||
* @param {string} trackerData - JSON string of tracker data
|
||||
* @returns {string} Tracker data with locks removed
|
||||
*/
|
||||
export function removeLocks(trackerData) {
|
||||
if (!trackerData) return trackerData;
|
||||
|
||||
// Try to parse as JSON
|
||||
const parsed = repairJSON(trackerData);
|
||||
if (!parsed) {
|
||||
// Not JSON format, return as-is
|
||||
return trackerData;
|
||||
}
|
||||
|
||||
// Recursively remove all "locked" properties
|
||||
const cleaned = removeLockedProperties(parsed);
|
||||
|
||||
return JSON.stringify(cleaned, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove "locked" properties from an object
|
||||
* @param {*} obj - Object to clean
|
||||
* @returns {*} Object with locked properties removed
|
||||
*/
|
||||
function removeLockedProperties(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => removeLockedProperties(item));
|
||||
} else if (obj !== null && typeof obj === 'object') {
|
||||
const cleaned = {};
|
||||
for (const key in obj) {
|
||||
if (key !== 'locked') {
|
||||
cleaned[key] = removeLockedProperties(obj[key]);
|
||||
}
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific item is locked
|
||||
* @param {string} trackerType - Type of tracker
|
||||
* @param {string} itemPath - Path to the item (e.g., 'stats.Health', 'quests.main.0')
|
||||
* @returns {boolean} Whether the item is locked
|
||||
*/
|
||||
export function isItemLocked(trackerType, itemPath) {
|
||||
const lockedItems = extensionSettings.lockedItems?.[trackerType];
|
||||
if (!lockedItems) return false;
|
||||
|
||||
const parts = itemPath.split('.');
|
||||
let current = lockedItems;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current[part] === undefined) return false;
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
return !!current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle lock state for a specific item
|
||||
* @param {string} trackerType - Type of tracker
|
||||
* @param {string} itemPath - Path to the item
|
||||
* @param {boolean} locked - New lock state
|
||||
*/
|
||||
export function setItemLock(trackerType, itemPath, locked) {
|
||||
console.log('[Lock Manager] setItemLock called:', { trackerType, itemPath, locked });
|
||||
|
||||
if (!extensionSettings.lockedItems) {
|
||||
extensionSettings.lockedItems = {};
|
||||
}
|
||||
|
||||
if (!extensionSettings.lockedItems[trackerType]) {
|
||||
extensionSettings.lockedItems[trackerType] = {};
|
||||
}
|
||||
|
||||
const parts = itemPath.split('.');
|
||||
let current = extensionSettings.lockedItems[trackerType];
|
||||
|
||||
// Navigate to parent of target
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (!current[part]) {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
// Set or remove lock
|
||||
const finalKey = parts[parts.length - 1];
|
||||
if (locked) {
|
||||
current[finalKey] = true;
|
||||
} else {
|
||||
delete current[finalKey];
|
||||
}
|
||||
|
||||
console.log('[Lock Manager] Locked items after set:', JSON.stringify(extensionSettings.lockedItems, null, 2));
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Parser Module
|
||||
* Handles parsing of AI responses to extract tracker data
|
||||
* Supports both legacy text format and new v3 JSON format
|
||||
*/
|
||||
|
||||
import { extensionSettings, FEATURE_FLAGS, addDebugLog } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { extractInventory } from './inventoryParser.js';
|
||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||
|
||||
/**
|
||||
* Helper to separate emoji from text in a string
|
||||
@@ -159,36 +161,246 @@ export function parseResponse(responseText) {
|
||||
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
||||
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars');
|
||||
|
||||
// Check if response uses XML <trackers> tags (new format)
|
||||
// Remove "FORMAT:" markers that the model might accidentally output
|
||||
cleanedResponse = cleanedResponse.replace(/FORMAT:\s*/gi, '');
|
||||
debugLog('[RPG Parser] Removed FORMAT: markers, new length:', cleanedResponse.length + ' chars');
|
||||
|
||||
// First, try to extract raw JSON objects (v3 format)
|
||||
// Note: Prompts now instruct models to use ```json``` code blocks, but we extract
|
||||
// from any JSON found using brace-matching for maximum compatibility
|
||||
// Use brace-matching to find complete JSON objects
|
||||
const extractedObjects = [];
|
||||
let i = 0;
|
||||
while (i < cleanedResponse.length) {
|
||||
if (cleanedResponse[i] === '{') {
|
||||
// Found opening brace, find matching closing brace
|
||||
let depth = 1;
|
||||
let j = i + 1;
|
||||
let inString = false;
|
||||
let escapeNext = false;
|
||||
|
||||
while (j < cleanedResponse.length && depth > 0) {
|
||||
const char = cleanedResponse[j];
|
||||
|
||||
if (escapeNext) {
|
||||
escapeNext = false;
|
||||
} else if (char === '\\') {
|
||||
escapeNext = true;
|
||||
} else if (char === '"') {
|
||||
inString = !inString;
|
||||
} else if (!inString) {
|
||||
if (char === '{') depth++;
|
||||
else if (char === '}') depth--;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
if (depth === 0) {
|
||||
// Found complete JSON object
|
||||
const jsonContent = cleanedResponse.substring(i, j).trim();
|
||||
extractedObjects.push(jsonContent);
|
||||
i = j;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (extractedObjects.length > 0) {
|
||||
console.log(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
|
||||
debugLog(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
|
||||
|
||||
// First, try to parse as unified JSON structure (new v3.1 format)
|
||||
if (extractedObjects.length === 1) {
|
||||
const parsed = repairJSON(extractedObjects[0]);
|
||||
if (parsed && (parsed.userStats || parsed.infoBox || parsed.characters)) {
|
||||
console.log('[RPG Parser] ✓ Detected unified JSON structure (v3.1 format)');
|
||||
|
||||
if (parsed.userStats) {
|
||||
result.userStats = JSON.stringify(parsed.userStats);
|
||||
console.log('[RPG Parser] ✓ Extracted userStats from unified structure');
|
||||
}
|
||||
if (parsed.infoBox) {
|
||||
result.infoBox = JSON.stringify(parsed.infoBox);
|
||||
console.log('[RPG Parser] ✓ Extracted infoBox from unified structure');
|
||||
}
|
||||
if (parsed.characters) {
|
||||
result.characterThoughts = JSON.stringify(parsed.characters);
|
||||
console.log('[RPG Parser] ✓ Extracted characters from unified structure');
|
||||
}
|
||||
|
||||
if (result.userStats || result.infoBox || result.characterThoughts) {
|
||||
console.log('[RPG Parser] ✓ Returning unified JSON parse results');
|
||||
debugLog('[RPG Parser] Returning unified JSON parse results');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to parsing multiple separate JSON objects (legacy v3.0 format)
|
||||
for (let idx = 0; idx < extractedObjects.length; idx++) {
|
||||
const jsonContent = extractedObjects[idx];
|
||||
console.log(`[RPG Parser] Parsing object ${idx + 1}:`, jsonContent.substring(0, 100) + '...');
|
||||
console.log(`[RPG Parser] Full object ${idx + 1} length:`, jsonContent.length);
|
||||
|
||||
const parsed = repairJSON(jsonContent);
|
||||
|
||||
if (parsed) {
|
||||
console.log(`[RPG Parser] Object ${idx + 1} parsed successfully, keys:`, Object.keys(parsed));
|
||||
|
||||
// Check if object is wrapped (e.g., {"userStats": {...}})
|
||||
// Unwrap single-key objects that match our tracker types
|
||||
let unwrapped = parsed;
|
||||
if (Object.keys(parsed).length === 1) {
|
||||
const key = Object.keys(parsed)[0];
|
||||
if (key === 'userStats' || key === 'infoBox' || key === 'characters') {
|
||||
unwrapped = parsed[key];
|
||||
console.log(`[RPG Parser] ✓ Unwrapped ${key} object`);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect tracker type by checking for top-level fields
|
||||
if (unwrapped.stats || unwrapped.status || unwrapped.skills || unwrapped.inventory || unwrapped.quests) {
|
||||
result.userStats = jsonContent;
|
||||
console.log('[RPG Parser] ✓ Assigned to User Stats');
|
||||
debugLog('[RPG Parser] ✓ Extracted raw JSON User Stats');
|
||||
} else if (unwrapped.date || unwrapped.location || unwrapped.weather || unwrapped.temperature || unwrapped.time) {
|
||||
result.infoBox = jsonContent;
|
||||
console.log('[RPG Parser] ✓ Assigned to Info Box');
|
||||
debugLog('[RPG Parser] ✓ Extracted raw JSON Info Box');
|
||||
} else if (unwrapped.characters || Array.isArray(unwrapped)) {
|
||||
result.characterThoughts = jsonContent;
|
||||
console.log('[RPG Parser] ✓ Assigned to Characters');
|
||||
debugLog('[RPG Parser] ✓ Extracted raw JSON Characters');
|
||||
} else {
|
||||
console.warn('[RPG Parser] ⚠️ Could not categorize object with keys:', Object.keys(parsed));
|
||||
}
|
||||
} else {
|
||||
console.error('[RPG Parser] ✗ Failed to parse raw JSON object', idx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.userStats || result.infoBox || result.characterThoughts) {
|
||||
console.log('[RPG Parser] ✓ Returning raw JSON parse results:', {
|
||||
hasUserStats: !!result.userStats,
|
||||
hasInfoBox: !!result.infoBox,
|
||||
hasCharacters: !!result.characterThoughts
|
||||
});
|
||||
debugLog('[RPG Parser] Returning raw JSON parse results');
|
||||
return result;
|
||||
} else {
|
||||
console.warn('[RPG Parser] ⚠️ No tracker data extracted from', extractedObjects.length, 'objects');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for JSON code blocks (legacy v3 format with ```json fences)
|
||||
// Look for ```json code blocks which indicate JSON format
|
||||
const jsonBlockRegex = /```json\s*\n([\s\S]*?)```/g;
|
||||
const jsonMatches = [...cleanedResponse.matchAll(jsonBlockRegex)];
|
||||
|
||||
if (jsonMatches.length > 0) {
|
||||
console.log('[RPG Parser] ✓ Found', jsonMatches.length, 'JSON code blocks (v3 format with fences)');
|
||||
debugLog('[RPG Parser] ✓ Found JSON code blocks (v3 format), parsing as JSON');
|
||||
|
||||
for (let idx = 0; idx < jsonMatches.length; idx++) {
|
||||
const match = jsonMatches[idx];
|
||||
const jsonContent = match[1].trim();
|
||||
console.log(`[RPG Parser] Parsing JSON block ${idx + 1}:`, jsonContent.substring(0, 100) + '...');
|
||||
|
||||
const parsed = repairJSON(jsonContent);
|
||||
|
||||
if (parsed) {
|
||||
console.log(`[RPG Parser] JSON block ${idx + 1} parsed successfully, keys:`, Object.keys(parsed));
|
||||
|
||||
// Detect tracker type by checking for top-level fields
|
||||
if (parsed.stats || parsed.status || parsed.skills || parsed.inventory || parsed.quests) {
|
||||
result.userStats = jsonContent;
|
||||
console.log('[RPG Parser] ✓ Assigned to User Stats');
|
||||
debugLog('[RPG Parser] ✓ Extracted JSON User Stats');
|
||||
} else if (parsed.date || parsed.location || parsed.weather || parsed.temperature || parsed.time) {
|
||||
result.infoBox = jsonContent;
|
||||
console.log('[RPG Parser] ✓ Assigned to Info Box');
|
||||
debugLog('[RPG Parser] ✓ Extracted JSON Info Box');
|
||||
} else if (parsed.characters || Array.isArray(parsed)) {
|
||||
result.characterThoughts = jsonContent;
|
||||
console.log('[RPG Parser] ✓ Assigned to Characters');
|
||||
debugLog('[RPG Parser] ✓ Extracted JSON Characters');
|
||||
} else {
|
||||
console.warn('[RPG Parser] ⚠️ Could not categorize JSON block with keys:', Object.keys(parsed));
|
||||
}
|
||||
} else {
|
||||
console.error('[RPG Parser] ✗ Failed to parse JSON code block', idx + 1);
|
||||
debugLog('[RPG Parser] ✗ Failed to parse JSON block, will try text fallback');
|
||||
}
|
||||
}
|
||||
|
||||
// If we found at least one valid JSON block, return the result
|
||||
// Mixed formats (some JSON, some text) will still work
|
||||
if (result.userStats || result.infoBox || result.characterThoughts) {
|
||||
console.log('[RPG Parser] ✓ Returning JSON code block parse results:', {
|
||||
hasUserStats: !!result.userStats,
|
||||
hasInfoBox: !!result.infoBox,
|
||||
hasCharacters: !!result.characterThoughts
|
||||
});
|
||||
debugLog('[RPG Parser] Returning JSON parse results');
|
||||
return result;
|
||||
} else {
|
||||
console.warn('[RPG Parser] ⚠️ No tracker data extracted from', jsonMatches.length, 'JSON blocks');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if response uses XML <trackers> tags (hybrid format)
|
||||
const xmlMatch = cleanedResponse.match(/<trackers>([\s\S]*?)<\/trackers>/i);
|
||||
if (xmlMatch) {
|
||||
debugLog('[RPG Parser] ✓ Found XML <trackers> tags, using XML parser');
|
||||
const trackersContent = xmlMatch[1].trim();
|
||||
|
||||
// Extract sections from XML content (sections are not in code blocks)
|
||||
const statsMatch = trackersContent.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
|
||||
if (statsMatch) {
|
||||
result.userStats = stripBrackets(statsMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Stats from XML');
|
||||
}
|
||||
// Try to parse JSON blocks within XML first
|
||||
const xmlJsonMatches = [...trackersContent.matchAll(jsonBlockRegex)];
|
||||
if (xmlJsonMatches.length > 0) {
|
||||
debugLog('[RPG Parser] Found JSON blocks within XML tags');
|
||||
for (const match of xmlJsonMatches) {
|
||||
const jsonContent = match[1].trim();
|
||||
const parsed = repairJSON(jsonContent);
|
||||
|
||||
const infoBoxMatch = trackersContent.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
|
||||
if (infoBoxMatch) {
|
||||
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Info Box from XML');
|
||||
}
|
||||
if (parsed) {
|
||||
if (parsed.type === 'userStats' || parsed.stats) {
|
||||
result.userStats = jsonContent;
|
||||
} else if (parsed.type === 'infoBox' || parsed.date || parsed.location) {
|
||||
result.infoBox = jsonContent;
|
||||
} else if (parsed.type === 'characters' || parsed.characters || Array.isArray(parsed)) {
|
||||
result.characterThoughts = jsonContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to text extraction from XML content (legacy v2 text format)
|
||||
const statsMatch = trackersContent.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
|
||||
if (statsMatch) {
|
||||
result.userStats = stripBrackets(statsMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Stats from XML (text format)');
|
||||
}
|
||||
|
||||
const charactersMatch = trackersContent.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
|
||||
if (charactersMatch) {
|
||||
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Present Characters from XML');
|
||||
const infoBoxMatch = trackersContent.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
|
||||
if (infoBoxMatch) {
|
||||
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Info Box from XML (text format)');
|
||||
}
|
||||
|
||||
const charactersMatch = trackersContent.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
|
||||
if (charactersMatch) {
|
||||
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Present Characters from XML (text format)');
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('[RPG Parser] Parsed from XML:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fallback to markdown code block parsing (old format)
|
||||
// Fallback to markdown code block parsing (old text format or mixed format)
|
||||
debugLog('[RPG Parser] No XML tags found, using code block parser');
|
||||
|
||||
// Extract code blocks
|
||||
@@ -289,7 +501,7 @@ export function parseResponse(responseText) {
|
||||
debugLog('[RPG Parser] =======================================================');
|
||||
|
||||
return result;
|
||||
}
|
||||
} // End parseResponse
|
||||
|
||||
/**
|
||||
* Parses user stats from the text and updates the extensionSettings.
|
||||
@@ -303,6 +515,118 @@ export function parseUserStats(statsText) {
|
||||
debugLog('[RPG Parser] Stats text preview:', statsText.substring(0, 200));
|
||||
|
||||
try {
|
||||
// Check if this is v3 JSON format - try to parse it first
|
||||
let statsData = null;
|
||||
const trimmed = statsText.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
statsData = repairJSON(statsText);
|
||||
if (statsData) {
|
||||
debugLog('[RPG Parser] ✓ Parsed as v3 JSON format');
|
||||
|
||||
// Extract stats from v3 JSON structure
|
||||
if (statsData.stats && Array.isArray(statsData.stats)) {
|
||||
console.log('[RPG Parser] ✓ Extracting stats array, count:', statsData.stats.length);
|
||||
statsData.stats.forEach(stat => {
|
||||
if (stat.id && typeof stat.value !== 'undefined') {
|
||||
extensionSettings.userStats[stat.id] = stat.value;
|
||||
console.log(`[RPG Parser] ✓ Set ${stat.id} = ${stat.value}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extract status
|
||||
if (statsData.status) {
|
||||
console.log('[RPG Parser] ✓ Extracting status:', statsData.status);
|
||||
if (statsData.status.mood) {
|
||||
extensionSettings.userStats.mood = statsData.status.mood;
|
||||
console.log('[RPG Parser] ✓ Set mood =', statsData.status.mood);
|
||||
}
|
||||
if (statsData.status.conditions) {
|
||||
extensionSettings.userStats.conditions = statsData.status.conditions;
|
||||
console.log('[RPG Parser] ✓ Set conditions =', statsData.status.conditions);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract inventory (convert v3 array format to v2 string format)
|
||||
if (statsData.inventory) {
|
||||
const inv = statsData.inventory;
|
||||
|
||||
// Convert arrays of {name, quantity} objects to comma-separated strings
|
||||
const convertItems = (items) => {
|
||||
if (!items || !Array.isArray(items)) return '';
|
||||
return items.map(item => {
|
||||
if (typeof item === 'object' && item.name) {
|
||||
// Include quantity if > 1
|
||||
return item.quantity && item.quantity > 1
|
||||
? `${item.quantity}x ${item.name}`
|
||||
: item.name;
|
||||
}
|
||||
return String(item);
|
||||
}).join(', ');
|
||||
};
|
||||
|
||||
// Convert stored object {location: [items]} to {location: "item1, item2"}
|
||||
const convertStoredInventory = (stored) => {
|
||||
if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return {};
|
||||
const result = {};
|
||||
for (const [location, items] of Object.entries(stored)) {
|
||||
if (Array.isArray(items)) {
|
||||
result[location] = convertItems(items);
|
||||
} else if (typeof items === 'string') {
|
||||
result[location] = items;
|
||||
} else {
|
||||
result[location] = '';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
extensionSettings.userStats.inventory = {
|
||||
onPerson: convertItems(inv.onPerson),
|
||||
clothing: convertItems(inv.clothing),
|
||||
stored: convertStoredInventory(inv.stored),
|
||||
assets: convertItems(inv.assets)
|
||||
};
|
||||
console.log('[RPG Parser] ✓ Converted v3 inventory:', extensionSettings.userStats.inventory);
|
||||
}
|
||||
|
||||
// Extract quests (convert v3 object format to v2 string format)
|
||||
if (statsData.quests) {
|
||||
// Convert quest objects to strings
|
||||
const convertQuest = (quest) => {
|
||||
if (!quest) return '';
|
||||
if (typeof quest === 'string') return quest;
|
||||
if (typeof quest === 'object') {
|
||||
// v3 format: {title, description, status}
|
||||
return quest.title || quest.description || JSON.stringify(quest);
|
||||
}
|
||||
return String(quest);
|
||||
};
|
||||
|
||||
extensionSettings.quests = {
|
||||
main: convertQuest(statsData.quests.main),
|
||||
optional: Array.isArray(statsData.quests.optional)
|
||||
? statsData.quests.optional.map(convertQuest)
|
||||
: []
|
||||
};
|
||||
console.log('[RPG Parser] ✓ Converted v3 quests:', extensionSettings.quests);
|
||||
}
|
||||
|
||||
// Extract skills if present (store as object, not JSON string)
|
||||
if (statsData.skills && Array.isArray(statsData.skills)) {
|
||||
extensionSettings.userStats.skills = statsData.skills;
|
||||
console.log('[RPG Parser] ✓ Set skills:', extensionSettings.userStats.skills);
|
||||
}
|
||||
|
||||
debugLog('[RPG Parser] ✓ Successfully extracted v3 JSON data');
|
||||
saveSettings();
|
||||
return; // Done processing v3 format
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to v2 text format parsing if JSON parsing failed
|
||||
debugLog('[RPG Parser] Falling back to v2 text format parsing');
|
||||
|
||||
// Get custom stat configuration
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
const customStats = trackerConfig?.userStats?.customStats || [];
|
||||
|
||||
@@ -7,6 +7,13 @@ import { getContext } from '../../../../../../extensions.js';
|
||||
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
|
||||
import { selected_group, getGroupMembers, getGroupChat, groups } from '../../../../../../group-chats.js';
|
||||
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
|
||||
import {
|
||||
buildUserStatsJSONInstruction,
|
||||
buildInfoBoxJSONInstruction,
|
||||
buildCharactersJSONInstruction,
|
||||
addLockInstruction
|
||||
} from './jsonPromptHelpers.js';
|
||||
import { applyLocks } from './lockManager.js';
|
||||
|
||||
// Type imports
|
||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||
@@ -16,16 +23,26 @@ import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../co
|
||||
*/
|
||||
export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, and JS segments whenever they enhance visual storytelling (e.g., for in-world screens, posters, books, letters, signs, crests, labels, etc.). Style them to match the setting's theme (e.g., fantasy, sci-fi), keep the text readable, and embed all assets directly (using inline SVGs only with no external scripts, libraries, or fonts). Use these elements freely and naturally within the narrative as characters would encounter them, including animations, 3D effects, pop-ups, dropdowns, websites, and so on. Do not wrap the HTML/CSS/JS in code fences!`;
|
||||
|
||||
/**
|
||||
* Default Dialogue Coloring prompt text
|
||||
*/
|
||||
export const DEFAULT_DIALOGUE_COLORING_PROMPT = `Wrap all character/NPC "dialogues" in unique <font color=######>tags</font>, exemplary: <font color=#abc123>"You're pretty good."</font> Assign a distinct color to each speaker and reuse it whenever they speak again.`;
|
||||
|
||||
/**
|
||||
* Default Spotify music prompt text (customizable by users)
|
||||
*/
|
||||
export const DEFAULT_SPOTIFY_PROMPT = `If appropriate for the current scene's mood and atmosphere, suggest a song that fits the ambiance. Choose music that enhances the emotional tone, setting, or action of the scene.`;
|
||||
export const DEFAULT_SPOTIFY_PROMPT = `If fitting for the current scene's mood and atmosphere, suggest a song that fits the ambiance. Choose music that enhances the emotional tone, setting, or action of the scene.`;
|
||||
|
||||
/**
|
||||
* Spotify format instruction (constant, not editable by users)
|
||||
*/
|
||||
export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spotify:Song Title - Artist Name/>.`;
|
||||
|
||||
/**
|
||||
* Default Narrator Mode prompt text (customizable by users)
|
||||
*/
|
||||
export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`;
|
||||
|
||||
/**
|
||||
* Gets character card information for current chat (handles both single and group chats)
|
||||
* @returns {string} Formatted character information
|
||||
@@ -49,7 +66,10 @@ async function getCharacterCardsInfo() {
|
||||
}
|
||||
|
||||
characterInfo += `</narrator>\n\n`;
|
||||
characterInfo += `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references - instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.\n\n`;
|
||||
|
||||
// Use custom narrator prompt if available, otherwise use default
|
||||
const narratorPrompt = extensionSettings.customNarratorPrompt || DEFAULT_NARRATOR_PROMPT;
|
||||
characterInfo += narratorPrompt + '\n\n';
|
||||
}
|
||||
return characterInfo;
|
||||
}
|
||||
@@ -192,8 +212,11 @@ function buildAttributesString() {
|
||||
return `${attr.name} ${value}`;
|
||||
});
|
||||
|
||||
// Add level at the end
|
||||
attributeParts.push(`LVL ${extensionSettings.level}`);
|
||||
// Add level at the end (if enabled)
|
||||
const showLevel = extensionSettings.trackerConfig?.userStats?.showLevel !== false; // Default to true
|
||||
if (showLevel) {
|
||||
attributeParts.push(`LVL ${extensionSettings.level}`);
|
||||
}
|
||||
|
||||
return attributeParts.join(', ');
|
||||
}
|
||||
@@ -206,26 +229,70 @@ function buildAttributesString() {
|
||||
*/
|
||||
export function generateTrackerExample() {
|
||||
let example = '';
|
||||
const useXmlTags = extensionSettings.saveTrackerHistory;
|
||||
|
||||
// Use COMMITTED data for generation context, not displayed data
|
||||
// Wrap each tracker section in markdown code blocks
|
||||
// Apply locks before sending to AI (for JSON format only)
|
||||
// Build unified JSON structure with proper wrapper keys
|
||||
const parts = [];
|
||||
|
||||
console.log('[RPG Companion] generateTrackerExample - enabled modules:', {
|
||||
showUserStats: extensionSettings.showUserStats,
|
||||
showInfoBox: extensionSettings.showInfoBox,
|
||||
showCharacterThoughts: extensionSettings.showCharacterThoughts
|
||||
});
|
||||
console.log('[RPG Companion] generateTrackerExample - committed data:', {
|
||||
hasUserStats: !!committedTrackerData.userStats,
|
||||
hasInfoBox: !!committedTrackerData.infoBox,
|
||||
hasCharacterThoughts: !!committedTrackerData.characterThoughts
|
||||
});
|
||||
|
||||
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
|
||||
example += '```\n' + committedTrackerData.userStats + '\n```\n\n';
|
||||
// Try to parse as JSON first, otherwise treat as text
|
||||
try {
|
||||
JSON.parse(committedTrackerData.userStats);
|
||||
// It's valid JSON - apply locks
|
||||
const lockedData = applyLocks(committedTrackerData.userStats, 'userStats');
|
||||
parts.push(` "userStats": ${lockedData}`);
|
||||
} catch {
|
||||
// It's text format - no locks applied
|
||||
example += '```\n' + committedTrackerData.userStats + '\n```\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
|
||||
example += '```\n' + committedTrackerData.infoBox + '\n```\n\n';
|
||||
try {
|
||||
JSON.parse(committedTrackerData.infoBox);
|
||||
const lockedData = applyLocks(committedTrackerData.infoBox, 'infoBox');
|
||||
parts.push(` "infoBox": ${lockedData}`);
|
||||
} catch {
|
||||
example += '```\n' + committedTrackerData.infoBox + '\n```\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
||||
example += '```\n' + committedTrackerData.characterThoughts + '\n```';
|
||||
try {
|
||||
JSON.parse(committedTrackerData.characterThoughts);
|
||||
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
||||
parts.push(` "characters": ${lockedData}`);
|
||||
} catch {
|
||||
example += '```\n' + committedTrackerData.characterThoughts + '\n```';
|
||||
}
|
||||
}
|
||||
|
||||
// If we have JSON parts, wrap them in unified structure
|
||||
if (parts.length > 0) {
|
||||
example = '{\n' + parts.join(',\n') + '\n}';
|
||||
}
|
||||
|
||||
console.log('[RPG Companion] generateTrackerExample - result length:', example.length, 'parts:', parts.length);
|
||||
|
||||
return example.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the instruction portion - format specifications and guidelines.
|
||||
* NOW USES JSON FORMAT (v3) instead of text format
|
||||
*
|
||||
* @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
|
||||
@@ -247,187 +314,77 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
const useXmlTags = extensionSettings.saveTrackerHistory;
|
||||
const openTag = useXmlTags ? '<trackers>\n' : '';
|
||||
const closeTag = useXmlTags ? '\n</trackers>' : '';
|
||||
const codeBlockMarker = useXmlTags ? '' : '```';
|
||||
const codeBlockMarker = '';
|
||||
const endCodeBlockMarker = '';
|
||||
|
||||
// Universal instruction header
|
||||
if (useXmlTags) {
|
||||
// Format specification is always hardcoded
|
||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in <trackers></trackers> XML tags. `;
|
||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the JSON format shown below, enclosed in <trackers></trackers> XML tags. `;
|
||||
} else {
|
||||
// Format specification is always hardcoded
|
||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. `;
|
||||
instructions += '\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the JSON format shown below as a single unified JSON object containing all enabled tracker fields. ';
|
||||
}
|
||||
|
||||
// Append custom instruction portion if available (same for both XML and Markdown)
|
||||
// Append custom instruction portion if available
|
||||
const customPrompt = extensionSettings.customTrackerInstructionsPrompt;
|
||||
if (customPrompt) {
|
||||
instructions += customPrompt.replace(/{userName}/g, userName);
|
||||
} else {
|
||||
instructions += `Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. `;
|
||||
instructions += `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 (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).`;
|
||||
instructions += `Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that ${userName} perceives about the current scene and the present characters. For example: "Location" becomes "Forest Clearing", "Mood Emoji" becomes "😊". `;
|
||||
instructions += `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.`;
|
||||
}
|
||||
|
||||
// Add format specifications for each enabled tracker
|
||||
// Add lock instruction
|
||||
instructions += addLockInstruction('');
|
||||
|
||||
// Add format specifications for each enabled tracker using JSON
|
||||
// Wrap all trackers in a unified JSON structure
|
||||
const enabledTrackers = [];
|
||||
if (extensionSettings.showUserStats) {
|
||||
const userStatsConfig = trackerConfig?.userStats;
|
||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
|
||||
instructions += codeBlockMarker + '\n';
|
||||
instructions += `${userName}'s Stats\n`;
|
||||
instructions += '---\n';
|
||||
|
||||
// Add custom stats dynamically
|
||||
for (const stat of enabledStats) {
|
||||
instructions += `- ${stat.name}: X%\n`;
|
||||
}
|
||||
|
||||
// Add status section if enabled
|
||||
if (userStatsConfig?.statusSection?.enabled) {
|
||||
const statusFields = userStatsConfig.statusSection.customFields || [];
|
||||
const statusFieldsText = statusFields.map(f => `${f}`).join(', ');
|
||||
|
||||
if (userStatsConfig.statusSection.showMoodEmoji) {
|
||||
instructions += `Status: [Mood Emoji${statusFieldsText ? ', ' + statusFieldsText : ''}]\n`;
|
||||
} else if (statusFieldsText) {
|
||||
instructions += `Status: [${statusFieldsText}]\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add skills section if enabled
|
||||
if (userStatsConfig?.skillsSection?.enabled) {
|
||||
const skillFields = userStatsConfig.skillsSection.customFields || [];
|
||||
const skillFieldsText = skillFields.map(f => `[${f}]`).join(', ');
|
||||
instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`;
|
||||
}
|
||||
|
||||
// Add inventory format based on feature flag - only if showInventory is enabled
|
||||
if (extensionSettings.showInventory) {
|
||||
if (FEATURE_FLAGS.useNewInventory) {
|
||||
instructions += 'On Person: [Items currently carried/worn, or "None"]\n';
|
||||
instructions += 'Clothing: [Clothing/Armor currently worn, or "None"]\n';
|
||||
instructions += 'Stored - [Location Name]: [Items stored at this location]\n';
|
||||
instructions += '(Add multiple "Stored - [Location]:" lines as needed for different storage locations)\n';
|
||||
instructions += 'Assets: [Vehicles, property, major possessions, or "None"]\n';
|
||||
} else {
|
||||
// Legacy v1 format
|
||||
instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items, or "None")]\\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add quests section
|
||||
instructions += 'Main Quests: [Short title of the currently active main quest (for example, "Save the world"), or "None"]\n';
|
||||
instructions += 'Optional Quests: [Short titles of the currently active optional quests (for example, "Find Zandik\'s book"), or "None"]\n';
|
||||
|
||||
instructions += codeBlockMarker + '\n\n';
|
||||
enabledTrackers.push('userStats');
|
||||
}
|
||||
|
||||
if (extensionSettings.showInfoBox) {
|
||||
const infoBoxConfig = trackerConfig?.infoBox;
|
||||
const widgets = infoBoxConfig?.widgets || {};
|
||||
|
||||
instructions += codeBlockMarker + '\n';
|
||||
instructions += 'Info Box\n';
|
||||
instructions += '---\n';
|
||||
|
||||
// Add only enabled widgets
|
||||
if (widgets.date?.enabled) {
|
||||
instructions += 'Date: [Weekday, Month, Year]\n';
|
||||
}
|
||||
if (widgets.weather?.enabled) {
|
||||
instructions += 'Weather: [Weather Emoji, Forecast]\n';
|
||||
}
|
||||
if (widgets.temperature?.enabled) {
|
||||
const unit = widgets.temperature.unit === 'F' ? '°F' : '°C';
|
||||
instructions += `Temperature: [Temperature in ${unit}]\n`;
|
||||
}
|
||||
if (widgets.time?.enabled) {
|
||||
instructions += 'Time: [Time Start → Time End]\n';
|
||||
}
|
||||
if (widgets.location?.enabled) {
|
||||
instructions += 'Location: [Location]\n';
|
||||
}
|
||||
if (widgets.recentEvents?.enabled) {
|
||||
instructions += 'Recent Events: [Up to three past events leading to the ongoing scene (short descriptors with no details, for example, "last-night date with Mary")]\n';
|
||||
}
|
||||
|
||||
instructions += codeBlockMarker + '\n\n';
|
||||
enabledTrackers.push('infoBox');
|
||||
}
|
||||
if (extensionSettings.showCharacterThoughts) {
|
||||
enabledTrackers.push('characters');
|
||||
}
|
||||
|
||||
if (extensionSettings.showCharacterThoughts) {
|
||||
const presentCharsConfig = trackerConfig?.presentCharacters;
|
||||
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
|
||||
if (enabledTrackers.length > 0) {
|
||||
instructions += '\n\nFORMAT:\n\nProvide EXACTLY ONE JSON code block with ALL tracker sections wrapped in a single object:\n\n```json\n{\n';
|
||||
|
||||
// Check if relationships are enabled
|
||||
const relationshipsEnabled = presentCharsConfig?.relationships?.enabled !== false; // Default to true
|
||||
const relationshipFields = relationshipsEnabled ? (presentCharsConfig?.relationshipFields || []) : [];
|
||||
|
||||
const thoughtsConfig = presentCharsConfig?.thoughts;
|
||||
const characterStats = presentCharsConfig?.characterStats;
|
||||
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
|
||||
instructions += codeBlockMarker + '\n';
|
||||
instructions += 'Present Characters\n';
|
||||
instructions += '---\n';
|
||||
|
||||
// Build relationship placeholders (e.g., "Lover/Friend")
|
||||
const relationshipPlaceholders = relationshipFields
|
||||
.filter(r => r && r.trim())
|
||||
.map(r => `${r}`)
|
||||
.join('/');
|
||||
|
||||
// Build custom field placeholders (e.g., "[Appearance] | [Current Action]")
|
||||
const fieldPlaceholders = enabledFields
|
||||
.map(f => `[${f.name}]`)
|
||||
.join(' | ');
|
||||
|
||||
// Character block format
|
||||
if (extensionSettings.narratorMode) {
|
||||
instructions += `- [Character Name (infer from story context; do not include ${userName}; state "Unavailable" if no other characters are present in the scene)]\n`;
|
||||
} else {
|
||||
instructions += `- [Name (do not include ${userName}; state "Unavailable" if no major characters are present in the scene)]\n`;
|
||||
if (extensionSettings.showUserStats) {
|
||||
instructions += ' "userStats": ';
|
||||
const userStatsJSON = buildUserStatsJSONInstruction();
|
||||
// Add 2 spaces to all lines after the first to properly nest within root object
|
||||
instructions += userStatsJSON.split('\n').map((line, i) => i === 0 ? line : ' ' + line).join('\n');
|
||||
instructions += enabledTrackers.indexOf('userStats') < enabledTrackers.length - 1 ? ',\n' : '\n';
|
||||
}
|
||||
|
||||
// Details line with emoji and custom fields
|
||||
if (fieldPlaceholders) {
|
||||
instructions += `Details: [Present Character's Emoji] | ${fieldPlaceholders}\n`;
|
||||
} else {
|
||||
instructions += `Details: [Present Character's Emoji]\n`;
|
||||
if (extensionSettings.showInfoBox) {
|
||||
instructions += ' "infoBox": ';
|
||||
const infoBoxJSON = buildInfoBoxJSONInstruction();
|
||||
// Add 2 spaces to all lines after the first to properly nest within root object
|
||||
instructions += infoBoxJSON.split('\n').map((line, i) => i === 0 ? line : ' ' + line).join('\n');
|
||||
instructions += enabledTrackers.indexOf('infoBox') < enabledTrackers.length - 1 ? ',\n' : '\n';
|
||||
}
|
||||
|
||||
// Relationship line (only if relationships are enabled)
|
||||
if (relationshipsEnabled && relationshipPlaceholders) {
|
||||
instructions += `Relationship: [(choose one: ${relationshipPlaceholders})]\n`;
|
||||
if (extensionSettings.showCharacterThoughts) {
|
||||
instructions += ' "characters": ';
|
||||
const charactersJSON = buildCharactersJSONInstruction();
|
||||
// Add 2 spaces to all lines after the first to properly nest within root object
|
||||
instructions += charactersJSON.split('\n').map((line, i) => i === 0 ? line : ' ' + line).join('\n');
|
||||
}
|
||||
|
||||
// Stats line (if enabled)
|
||||
if (enabledCharStats.length > 0) {
|
||||
const statPlaceholders = enabledCharStats.map(s => `${s.name}: X%`).join(' | ');
|
||||
instructions += `Stats: ${statPlaceholders}\n`;
|
||||
}
|
||||
|
||||
// Thoughts line (if enabled)
|
||||
if (thoughtsConfig?.enabled) {
|
||||
const thoughtsName = thoughtsConfig.name || 'Thoughts';
|
||||
const thoughtsDescription = thoughtsConfig.description || 'Internal monologue (in first person POV, up to three sentences long)';
|
||||
instructions += `${thoughtsName}: [${thoughtsDescription}]\n`;
|
||||
}
|
||||
|
||||
if (extensionSettings.narratorMode) {
|
||||
instructions += `- … (Repeat the format above for every other character present in the scene, inferred from story context)\n`;
|
||||
} else {
|
||||
instructions += `- … (Repeat the format above for every other present major character)\n`;
|
||||
}
|
||||
|
||||
instructions += codeBlockMarker + '\n\n';
|
||||
instructions += '\n}\n```\n\nDo NOT output multiple separate JSON objects. Everything must be in ONE unified object with the keys shown above.';
|
||||
}
|
||||
|
||||
// Only add continuation instruction if includeContinuation is true
|
||||
if (includeContinuation) {
|
||||
const customPrompt = extensionSettings.customTrackerContinuationPrompt;
|
||||
if (customPrompt) {
|
||||
instructions += customPrompt + '\n\n';
|
||||
instructions += '\n\n' + customPrompt + '\n\n';
|
||||
} else {
|
||||
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 the protagonist's performance, low hygiene influencing their social interactions, environmental factors shaping the scene, a character's emotional state coloring their responses, and so on. Remember, all bracketed placeholders (e.g., [Location], [Mood Emoji]) MUST be replaced with actual content without the square brackets.\n\n`;
|
||||
instructions += `\n\nAfter 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 the protagonist's performance, low hygiene influencing their social interactions, environmental factors shaping the scene, a character's emotional state coloring their responses, and so on. Remember, all bracketed placeholders (e.g., [Location], [Mood Emoji]) MUST be replaced with actual content without the square brackets.\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,6 +439,295 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
return instructions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats tracker data as human-readable text for context injection.
|
||||
* Converts JSON format to a concise, natural language summary.
|
||||
* @param {string} jsonData - JSON formatted tracker data
|
||||
* @param {string} trackerType - Type of tracker ('userStats', 'infoBox', 'characters')
|
||||
* @param {string} userName - User's name for personalization
|
||||
* @returns {string} Formatted text summary
|
||||
*/
|
||||
function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
||||
if (!jsonData) return '';
|
||||
|
||||
try {
|
||||
const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;
|
||||
let formatted = '';
|
||||
|
||||
// Helper to extract value from potentially locked fields and common object formats
|
||||
const getValue = (field) => {
|
||||
if (field === null || field === undefined) return '';
|
||||
|
||||
// If it's a locked object with {value, locked}, extract the value
|
||||
if (field && typeof field === 'object' && !Array.isArray(field) && 'value' in field) {
|
||||
return getValue(field.value); // Recursively handle in case value itself is locked
|
||||
}
|
||||
|
||||
// If it's a regular value, return as string
|
||||
if (typeof field !== 'object') {
|
||||
return String(field);
|
||||
}
|
||||
|
||||
// For arrays of strings, join them
|
||||
if (Array.isArray(field)) {
|
||||
return field.map(item => getValue(item)).filter(Boolean).join(', ');
|
||||
}
|
||||
|
||||
// Handle common object formats
|
||||
if (field && typeof field === 'object') {
|
||||
// Status object: {mood, conditions}
|
||||
if ('mood' in field && 'conditions' in field) {
|
||||
const mood = getValue(field.mood);
|
||||
const conditions = getValue(field.conditions);
|
||||
return `${mood} - ${conditions}`;
|
||||
}
|
||||
|
||||
// Skill/item/quest objects: {name}, {title}, {name, quantity}
|
||||
if ('name' in field) {
|
||||
const name = getValue(field.name);
|
||||
if ('quantity' in field && field.quantity > 1) {
|
||||
return `${name} (x${field.quantity})`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
if ('title' in field) {
|
||||
return getValue(field.title);
|
||||
}
|
||||
|
||||
// Time object: {start, end}
|
||||
if ('start' in field && 'end' in field) {
|
||||
return `${getValue(field.start)} - ${getValue(field.end)}`;
|
||||
}
|
||||
|
||||
// Weather object: {emoji, forecast}
|
||||
if ('emoji' in field && 'forecast' in field) {
|
||||
return `${getValue(field.emoji)} ${getValue(field.forecast)}`;
|
||||
}
|
||||
|
||||
// Generic object fallback: create key-value pairs
|
||||
const keys = Object.keys(field);
|
||||
if (keys.length > 0 && keys.length <= 3) {
|
||||
const values = keys.map(k => {
|
||||
const val = getValue(field[k]);
|
||||
return val ? `${k}: ${val}` : null;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (values.length > 0) {
|
||||
return values.join(', ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
if (trackerType === 'userStats') {
|
||||
formatted += `${userName}'s Stats:\n`;
|
||||
|
||||
// Handle stats array format: [{id, name, value}, ...]
|
||||
if (data.stats && Array.isArray(data.stats)) {
|
||||
for (const stat of data.stats) {
|
||||
if (stat && stat.value !== undefined) {
|
||||
const statName = stat.name || (stat.id ? stat.id.charAt(0).toUpperCase() + stat.id.slice(1) : 'Unknown');
|
||||
formatted += `${statName}: ${stat.value}\n`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: handle flat format {health: 10, mana: 20, ...}
|
||||
const statFieldOrder = ['health', 'mana', 'stamina', 'satiety', 'hygiene', 'energy', 'arousal'];
|
||||
const specialFields = ['status', 'mood', 'skills', 'inventory', 'quests'];
|
||||
|
||||
for (const statName of statFieldOrder) {
|
||||
if (data[statName] !== undefined) {
|
||||
const value = getValue(data[statName]);
|
||||
if (value) {
|
||||
const displayName = statName.charAt(0).toUpperCase() + statName.slice(1);
|
||||
formatted += `${displayName}: ${value}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom numeric stats
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (!statFieldOrder.includes(key) && !specialFields.includes(key) && typeof value === 'number') {
|
||||
const displayName = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
formatted += `${displayName}: ${getValue(value)}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status/mood
|
||||
if (data.status) formatted += `Status: ${getValue(data.status)}\n`;
|
||||
if (data.mood) formatted += `Mood: ${getValue(data.mood)}\n`;
|
||||
|
||||
// Skills - handle both array and object format
|
||||
if (data.skills) {
|
||||
if (Array.isArray(data.skills)) {
|
||||
// Array format: ["Combat", "Magic", "Stealth"]
|
||||
const skillsList = data.skills.map(s => getValue(s)).filter(s => s).join(', ');
|
||||
if (skillsList) formatted += `Skills: ${skillsList}\n`;
|
||||
} else if (typeof data.skills === 'object') {
|
||||
// Object format: {Combat: 50, Magic: 30}
|
||||
const skillsList = Object.entries(data.skills)
|
||||
.map(([name, val]) => {
|
||||
const skillName = getValue(name);
|
||||
const skillVal = getValue(val);
|
||||
return skillVal ? `${skillName}: ${skillVal}` : skillName;
|
||||
})
|
||||
.filter(s => s)
|
||||
.join(', ');
|
||||
if (skillsList) formatted += `Skills: ${skillsList}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Inventory sections
|
||||
if (data.inventory) {
|
||||
const inv = data.inventory;
|
||||
|
||||
if (inv.onPerson && Array.isArray(inv.onPerson) && inv.onPerson.length > 0) {
|
||||
const items = inv.onPerson.map(i => getValue(i)).filter(i => i);
|
||||
if (items.length > 0) formatted += `On Person: ${items.join(', ')}\n`;
|
||||
}
|
||||
|
||||
if (inv.clothing && Array.isArray(inv.clothing) && inv.clothing.length > 0) {
|
||||
const items = inv.clothing.map(i => getValue(i)).filter(i => i);
|
||||
if (items.length > 0) formatted += `Clothing: ${items.join(', ')}\n`;
|
||||
}
|
||||
|
||||
if (inv.stored && typeof inv.stored === 'object' && !Array.isArray(inv.stored)) {
|
||||
for (const [location, items] of Object.entries(inv.stored)) {
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
const itemsList = items.map(i => getValue(i)).filter(i => i);
|
||||
if (itemsList.length > 0) {
|
||||
formatted += `${getValue(location)}: ${itemsList.join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inv.assets && Array.isArray(inv.assets) && inv.assets.length > 0) {
|
||||
const items = inv.assets.map(i => getValue(i)).filter(i => i);
|
||||
if (items.length > 0) formatted += `Assets: ${items.join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Quests
|
||||
if (data.quests) {
|
||||
const quests = data.quests;
|
||||
|
||||
// Main quest - handle string, array, or object with {title}
|
||||
if (quests.main) {
|
||||
if (typeof quests.main === 'string') {
|
||||
const mainQuest = getValue(quests.main);
|
||||
if (mainQuest) formatted += `Main Quest: ${mainQuest}\n`;
|
||||
} else if (Array.isArray(quests.main) && quests.main.length > 0) {
|
||||
const questsList = quests.main.map(q => getValue(q)).filter(q => q);
|
||||
if (questsList.length > 0) formatted += `Main Quests: ${questsList.join(', ')}\n`;
|
||||
} else if (typeof quests.main === 'object') {
|
||||
// Handle {title: "..."} format
|
||||
const mainQuest = getValue(quests.main);
|
||||
if (mainQuest) formatted += `Main Quest: ${mainQuest}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional quests
|
||||
if (quests.optional && Array.isArray(quests.optional) && quests.optional.length > 0) {
|
||||
const questsList = quests.optional.map(q => getValue(q)).filter(q => q);
|
||||
if (questsList.length > 0) formatted += `Optional Quests: ${questsList.join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
} else if (trackerType === 'infoBox') {
|
||||
formatted += `Info Box:\n`;
|
||||
if (data.location) formatted += `Location: ${getValue(data.location)}\n`;
|
||||
if (data.date) formatted += `Date: ${getValue(data.date)}\n`;
|
||||
if (data.time) formatted += `Time: ${getValue(data.time)}\n`;
|
||||
if (data.weather) formatted += `Weather: ${getValue(data.weather)}\n`;
|
||||
if (data.temperature) formatted += `Temperature: ${getValue(data.temperature)}\n`;
|
||||
|
||||
// Custom fields
|
||||
const knownFields = ['location', 'date', 'time', 'weather', 'temperature'];
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (!knownFields.includes(key)) {
|
||||
const val = getValue(value);
|
||||
if (val) {
|
||||
// Convert camelCase to Title Case with spaces (recentEvents -> Recent Events)
|
||||
const displayName = key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase())
|
||||
.trim();
|
||||
formatted += `${displayName}: ${val}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (trackerType === 'characters') {
|
||||
if (Array.isArray(data)) {
|
||||
formatted += `Present Characters:\n`;
|
||||
for (const char of data) {
|
||||
const charName = getValue(char.name) || 'Unknown';
|
||||
formatted += `- ${charName}:\n`;
|
||||
|
||||
// Details section - parse all custom fields
|
||||
if (char.details && typeof char.details === 'object') {
|
||||
for (const [key, value] of Object.entries(char.details)) {
|
||||
const fieldValue = getValue(value);
|
||||
if (fieldValue) {
|
||||
// Convert camelCase/snake_case to Title Case with spaces
|
||||
const fieldName = key
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase())
|
||||
.trim();
|
||||
formatted += ` ${fieldName}: ${fieldValue}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Relationship
|
||||
if (char.relationship) {
|
||||
let relValue;
|
||||
if (typeof char.relationship === 'object' && !Array.isArray(char.relationship) && 'status' in char.relationship) {
|
||||
relValue = getValue(char.relationship.status);
|
||||
} else {
|
||||
relValue = getValue(char.relationship);
|
||||
}
|
||||
if (relValue) formatted += ` Relationship: ${relValue}\n`;
|
||||
}
|
||||
|
||||
// Thoughts
|
||||
if (char.thoughts) {
|
||||
let thoughtValue;
|
||||
if (typeof char.thoughts === 'object' && !Array.isArray(char.thoughts) && 'content' in char.thoughts) {
|
||||
thoughtValue = getValue(char.thoughts.content);
|
||||
} else {
|
||||
thoughtValue = getValue(char.thoughts);
|
||||
}
|
||||
if (thoughtValue) formatted += ` Thoughts: ${thoughtValue}\n`;
|
||||
}
|
||||
|
||||
// Stats
|
||||
if (char.stats && typeof char.stats === 'object' && !Array.isArray(char.stats)) {
|
||||
const statsList = Object.entries(char.stats)
|
||||
.map(([name, val]) => {
|
||||
const statValue = getValue(val);
|
||||
return statValue ? `${name}: ${statValue}` : null;
|
||||
})
|
||||
.filter(s => s)
|
||||
.join(', ');
|
||||
if (statsList) formatted += ` Stats: ${statsList}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formatted;
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to format tracker data for context:', e);
|
||||
console.warn('[RPG Companion] Error details:', e.stack);
|
||||
return ''; // Return empty string on error to avoid breaking context
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formatted contextual summary for SEPARATE mode injection.
|
||||
* Includes the full tracker data in original format (without code fences and separators).
|
||||
@@ -495,41 +741,39 @@ export function generateContextualSummary() {
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
let summary = '';
|
||||
|
||||
// Helper function to clean tracker data (remove code fences and separator lines)
|
||||
const cleanTrackerData = (data) => {
|
||||
if (!data) return '';
|
||||
return data
|
||||
.split('\n')
|
||||
.filter(line => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed &&
|
||||
!trimmed.startsWith('```') &&
|
||||
trimmed !== '---';
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
// Add User Stats tracker data if enabled
|
||||
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
|
||||
const cleanedStats = cleanTrackerData(committedTrackerData.userStats);
|
||||
if (cleanedStats) {
|
||||
summary += cleanedStats + '\n\n';
|
||||
try {
|
||||
const formatted = formatTrackerDataForContext(committedTrackerData.userStats, 'userStats', userName);
|
||||
if (formatted) {
|
||||
summary += formatted + '\n';
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to format userStats for context:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Info Box tracker data if enabled
|
||||
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
|
||||
const cleanedInfoBox = cleanTrackerData(committedTrackerData.infoBox);
|
||||
if (cleanedInfoBox) {
|
||||
summary += cleanedInfoBox + '\n\n';
|
||||
try {
|
||||
const formatted = formatTrackerDataForContext(committedTrackerData.infoBox, 'infoBox', userName);
|
||||
if (formatted) {
|
||||
summary += formatted + '\n';
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to format infoBox for context:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Present Characters tracker data if enabled
|
||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
||||
const cleanedThoughts = cleanTrackerData(committedTrackerData.characterThoughts);
|
||||
if (cleanedThoughts) {
|
||||
summary += cleanedThoughts + '\n\n';
|
||||
try {
|
||||
const formatted = formatTrackerDataForContext(committedTrackerData.characterThoughts, 'characters', userName);
|
||||
if (formatted) {
|
||||
summary += formatted + '\n';
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to format characters for context:', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,47 +812,58 @@ export function generateRPGPromptText() {
|
||||
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`;
|
||||
}
|
||||
// Build unified JSON structure for previous trackers (v3.1 format)
|
||||
const hasAnyPreviousData = committedTrackerData.userStats || committedTrackerData.infoBox || committedTrackerData.characterThoughts;
|
||||
|
||||
// Add current quests to the previous data context
|
||||
if (extensionSettings.quests) {
|
||||
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
|
||||
promptText += `Main Quests: ${extensionSettings.quests.main}\n`;
|
||||
if (hasAnyPreviousData) {
|
||||
const unifiedPrevious = {};
|
||||
|
||||
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
|
||||
try {
|
||||
// Try to parse as JSON - apply locks before adding to previous
|
||||
const lockedData = applyLocks(committedTrackerData.userStats, 'userStats');
|
||||
const parsed = JSON.parse(lockedData);
|
||||
unifiedPrevious.userStats = parsed;
|
||||
} catch {
|
||||
// Old text format - show it separately for backward compat
|
||||
promptText += `${committedTrackerData.userStats}\n\n`;
|
||||
}
|
||||
if (extensionSettings.quests.optional && extensionSettings.quests.optional.length > 0) {
|
||||
const optionalQuests = extensionSettings.quests.optional.filter(q => q && q !== 'None').join(', ');
|
||||
promptText += `Optional Quests: ${optionalQuests || 'None'}\n`;
|
||||
}
|
||||
|
||||
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
|
||||
try {
|
||||
// Try to parse as JSON - apply locks before adding to previous
|
||||
const lockedData = applyLocks(committedTrackerData.infoBox, 'infoBox');
|
||||
const parsed = JSON.parse(lockedData);
|
||||
unifiedPrevious.infoBox = parsed;
|
||||
} catch {
|
||||
// Old text format - show it separately for backward compat
|
||||
if (!unifiedPrevious.userStats) {
|
||||
promptText += `${committedTrackerData.infoBox}\n\n`;
|
||||
}
|
||||
}
|
||||
promptText += `\n`;
|
||||
}
|
||||
|
||||
// Add current skills to the previous data context
|
||||
const skillsSection = extensionSettings.trackerConfig?.userStats?.skillsSection;
|
||||
if (skillsSection?.enabled && skillsSection.customFields && skillsSection.customFields.length > 0) {
|
||||
const skillsList = skillsSection.customFields.join(', ');
|
||||
promptText += `Skills: ${skillsList}\n\n`;
|
||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
||||
try {
|
||||
// Try to parse as JSON - apply locks before adding to previous
|
||||
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
||||
const parsed = JSON.parse(lockedData);
|
||||
unifiedPrevious.characters = parsed;
|
||||
} catch {
|
||||
// Old text format - show it separately for backward compat
|
||||
if (!unifiedPrevious.userStats && !unifiedPrevious.infoBox) {
|
||||
promptText += `${committedTrackerData.characterThoughts}\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`;
|
||||
// If we successfully built a unified structure, display it
|
||||
if (Object.keys(unifiedPrevious).length > 0) {
|
||||
promptText += JSON.stringify(unifiedPrevious, null, 2) + '\n';
|
||||
}
|
||||
} else {
|
||||
promptText += `None - this is the first update.\n`;
|
||||
}
|
||||
|
||||
promptText += `</previous>\n`;
|
||||
@@ -638,12 +893,12 @@ export async function generateSeparateUpdatePrompt() {
|
||||
// Add character card information
|
||||
const characterInfo = await getCharacterCardsInfo();
|
||||
if (characterInfo) {
|
||||
systemMessage += characterInfo + '\n\n';
|
||||
systemMessage += characterInfo;
|
||||
}
|
||||
|
||||
systemMessage += `Here is the description of the protagonist for reference:\n`;
|
||||
systemMessage += `<protagonist>\n{{persona}}\n</protagonist>\n`;
|
||||
systemMessage += `\n\n`;
|
||||
systemMessage += `\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({
|
||||
@@ -665,7 +920,7 @@ export async function generateSeparateUpdatePrompt() {
|
||||
// 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. Remember, all bracketed placeholders (e.g., [Location], [Mood Emoji]) MUST be replaced with actual content without the square brackets.`;
|
||||
instructionMessage += `\nProvide ONLY the requested data in the exact JSON format specified above. Do not include any roleplay response, other text, or commentary. Remember, all placeholders MUST be replaced with actual content. Do NOT wrap the JSON in code fences (\`\`\`json). Output the JSON object directly.`;
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
@@ -678,23 +933,16 @@ export async function generateSeparateUpdatePrompt() {
|
||||
/**
|
||||
* Default custom instruction for avatar prompt generation
|
||||
*/
|
||||
const DEFAULT_AVATAR_CUSTOM_INSTRUCTION = `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons, but your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
|
||||
const DEFAULT_AVATAR_CUSTOM_INSTRUCTION = `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
|
||||
|
||||
Your workflow strictly follows a logical sequence:
|
||||
|
||||
First, **establish the subject**. If the character is from a known Intellectual Property (IP), franchise, anime, game, or movie, **you MUST begin the prompt with their full name and the series title** (e.g., "Nami from One Piece", "Geralt of Rivia from The Witcher"). This is the single most important anchor for the image and must take precedence. If the character is original, clearly describe their core identity, race, and appearance.
|
||||
|
||||
Next, **set the framing**. This is an avatar portrait. Focus strictly on the character's face and upper shoulders (bust shot or close-up). Ensure the face is the central focal point.
|
||||
|
||||
Then, **integrate the setting**. Describe the character *within* their current environment as provided in the context, but keep it as a background element. Incorporate the lighting, weather, and atmosphere to influence the character's appearance (e.g., shadows on the face, wet hair from rain).
|
||||
|
||||
Next, **detail the facial specifics**. Describe the character's current expression, eye contact, and mood in high detail based on the scene context and their personality. Mention visible clothing only at the neckline/shoulders.
|
||||
|
||||
Finally, **infuse with aesthetics**. Define the artistic style, medium (e.g., digital art, oil painting), and visual tone (e.g., cinematic lighting, ethereal atmosphere).
|
||||
|
||||
Your final description must be objective and concrete, and the use of metaphors and emotional rhetoric is strictly prohibited. It must also not contain meta tags or drawing instructions such as "8K" or "masterpiece".
|
||||
|
||||
Output only the final, modified prompt; do not output anything else.`;
|
||||
Your workflow strictly follows a logical sequence:
|
||||
First, establish the subject. If the character is from a known Intellectual Property (IP), franchise, anime, game, or movie, you MUST begin the prompt with their full name and the series title (e.g., "Nami from One Piece", "Geralt of Rivia from The Witcher"). This is the single most important anchor for the image and must take precedence. If the character is original, clearly describe their core identity, race, and appearance.
|
||||
Next, set the framing. This is an avatar portrait. Focus strictly on the character's face and upper shoulders (a bust shot or close-up). Ensure the face is the central focal point.
|
||||
Then, integrate the setting. Describe the character within their current environment as provided in the context, but keep it as a background element. Incorporate the lighting, weather, and atmosphere to influence the character's appearance (e.g., shadows on the face, wet hair from rain).
|
||||
Next, detail the facial specifics. Describe the character's current expression, eye contact, and mood in great detail based on the scene context and their personality. Mention visible clothing only at the neckline/shoulders.
|
||||
Finally, infuse with aesthetics. Define the artistic style, medium (e.g., digital art, oil painting), and visual tone (e.g., cinematic lighting, ethereal atmosphere).
|
||||
Your final description must be objective and concrete, and the use of metaphors and emotional rhetoric is strictly prohibited. It must also not contain meta tags or drawing instructions such as "8K" or "masterpiece".
|
||||
Output only the final, modified prompt; do not output anything else.`;
|
||||
|
||||
/**
|
||||
* Generates the prompt for LLM-based avatar prompt generation
|
||||
|
||||
Reference in New Issue
Block a user