feat: json format, et al.
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
} from '../../core/state.js';
|
||||
import { saveChatData } from '../../core/persistence.js';
|
||||
import { generateSeparateUpdatePrompt } from './promptBuilder.js';
|
||||
import { parseResponse, parseUserStats } from './parser.js';
|
||||
import { parseResponse, parseUserStats, parseSkills, tryParseJSONResponse } from './parser.js';
|
||||
import { renderUserStats } from '../rendering/userStats.js';
|
||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||
import { renderThoughts } from '../rendering/thoughts.js';
|
||||
@@ -133,18 +133,35 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
|
||||
if (response) {
|
||||
// console.log('[RPG Companion] Raw AI response:', response);
|
||||
const parsedData = parseResponse(response);
|
||||
// console.log('[RPG Companion] Parsed data:', parsedData);
|
||||
// console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null');
|
||||
|
||||
// Try JSON parsing first if structured data mode is enabled
|
||||
const jsonParsed = tryParseJSONResponse(response);
|
||||
|
||||
if (jsonParsed) {
|
||||
// JSON parsing succeeded - render all sections
|
||||
console.log('[RPG Companion] JSON parsing successful');
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
if (typeof renderSkills === 'function') renderSkills();
|
||||
saveChatData();
|
||||
} else {
|
||||
// JSON parsing failed - try legacy text-based parsing as fallback
|
||||
console.warn('[RPG Companion] JSON parsing failed, attempting legacy text parsing...');
|
||||
const parsedData = parseResponse(response);
|
||||
// console.log('[RPG Companion] Parsed data:', parsedData);
|
||||
// console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null');
|
||||
|
||||
// DON'T update lastGeneratedData here - it should only reflect the data
|
||||
// from the assistant message the user replied to, not auto-generated updates
|
||||
// This ensures swipes/regenerations use consistent source data
|
||||
// DON'T update lastGeneratedData here - it should only reflect the data
|
||||
// from the assistant message the user replied to, not auto-generated updates
|
||||
// This ensures swipes/regenerations use consistent source data
|
||||
|
||||
// Store RPG data for the last assistant message (separate mode)
|
||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||||
// console.log('[RPG Companion] Last message is_user:', lastMessage ? lastMessage.is_user : 'no message');
|
||||
if (lastMessage && !lastMessage.is_user) {
|
||||
// Store RPG data for the last assistant message (separate mode)
|
||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||||
// console.log('[RPG Companion] Last message is_user:', lastMessage ? lastMessage.is_user : 'no message');
|
||||
if (lastMessage && !lastMessage.is_user) {
|
||||
if (!lastMessage.extra) {
|
||||
lastMessage.extra = {};
|
||||
}
|
||||
@@ -166,6 +183,9 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
lastGeneratedData.userStats = parsedData.userStats;
|
||||
parseUserStats(parsedData.userStats);
|
||||
}
|
||||
if (parsedData.skills) {
|
||||
parseSkills(parsedData.skills);
|
||||
}
|
||||
if (parsedData.infoBox) {
|
||||
lastGeneratedData.infoBox = parsedData.infoBox;
|
||||
}
|
||||
@@ -212,8 +232,9 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
renderQuests();
|
||||
}
|
||||
|
||||
// Save to chat metadata
|
||||
saveChatData();
|
||||
// Save to chat metadata
|
||||
saveChatData();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,12 +16,21 @@ import {
|
||||
import { evaluateSuppression } from './suppression.js';
|
||||
import { parseUserStats } from './parser.js';
|
||||
import {
|
||||
generateTrackerExample,
|
||||
generateTrackerInstructions,
|
||||
generateJSONTrackerInstructions,
|
||||
generateContextualSummary,
|
||||
DEFAULT_HTML_PROMPT
|
||||
} from './promptBuilder.js';
|
||||
|
||||
/**
|
||||
* Gets tracker instructions (always uses JSON format)
|
||||
* @param {boolean} includeHtmlPrompt
|
||||
* @param {boolean} includeContinuation
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTrackerInstructions(includeHtmlPrompt, includeContinuation) {
|
||||
return generateJSONTrackerInstructions(includeHtmlPrompt, includeContinuation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for generation start.
|
||||
* Manages tracker data commitment and prompt injection based on generation mode.
|
||||
@@ -167,9 +176,9 @@ export function onGenerationStarted(type, data) {
|
||||
|
||||
if (extensionSettings.generationMode === 'together') {
|
||||
// console.log('[RPG Companion] In together mode, generating prompts...');
|
||||
const example = generateTrackerExample();
|
||||
const example = ''; // JSON format includes schema in instructions, no separate example needed
|
||||
// Don't include HTML prompt in instructions - inject it separately to avoid duplication on swipes
|
||||
const instructions = generateTrackerInstructions(false, true);
|
||||
const instructions = getTrackerInstructions(false, true);
|
||||
|
||||
// Clear separate mode context injection - we don't use contextual summary in together mode
|
||||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* Parser Module
|
||||
* Handles parsing of AI responses to extract tracker data
|
||||
* Supports both legacy text format and new JSON format
|
||||
*/
|
||||
|
||||
import { extensionSettings, FEATURE_FLAGS, addDebugLog } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||
import { extractInventory } from './inventoryParser.js';
|
||||
import { validateTrackerData, mergeTrackerData } from '../../types/trackerData.js';
|
||||
import { handleItemRemoved } from '../rendering/skills.js';
|
||||
|
||||
/**
|
||||
* Helper to separate emoji from text in a string
|
||||
@@ -133,19 +136,296 @@ function debugLog(message, data = null) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts JSON from a code block (handles ```json ... ``` format)
|
||||
* @param {string} text - Text that may contain JSON code blocks
|
||||
* @returns {Object|null} Parsed JSON object or null
|
||||
*/
|
||||
function extractJSONFromCodeBlock(text) {
|
||||
if (!text) return null;
|
||||
|
||||
// Match ```json ... ``` or ``` ... ``` blocks
|
||||
const jsonBlockRegex = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||
const matches = [...text.matchAll(jsonBlockRegex)];
|
||||
|
||||
for (const match of matches) {
|
||||
const content = match[1].trim();
|
||||
// Check if content looks like JSON (starts with { or [)
|
||||
if (content.startsWith('{') || content.startsWith('[')) {
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
debugLog('[RPG Parser] JSON parse failed:', e.message);
|
||||
// Try to fix common JSON issues
|
||||
const fixed = tryFixJSON(content);
|
||||
if (fixed) return fixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to fix common JSON formatting issues
|
||||
* @param {string} jsonStr - Potentially malformed JSON string
|
||||
* @returns {Object|null} Fixed JSON object or null
|
||||
*/
|
||||
function tryFixJSON(jsonStr) {
|
||||
try {
|
||||
// Remove trailing commas
|
||||
let fixed = jsonStr.replace(/,(\s*[}\]])/g, '$1');
|
||||
// Fix unquoted keys
|
||||
fixed = fixed.replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3');
|
||||
return JSON.parse(fixed);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses JSON tracker data and applies it to extension settings
|
||||
* @param {Object} jsonData - Parsed JSON tracker data
|
||||
* @returns {boolean} Whether parsing was successful
|
||||
*/
|
||||
export function parseJSONTrackerData(jsonData) {
|
||||
debugLog('[RPG Parser] ==================== JSON PARSING ====================');
|
||||
|
||||
const validation = validateTrackerData(jsonData);
|
||||
if (!validation.valid) {
|
||||
debugLog('[RPG Parser] JSON validation failed:', validation.errors);
|
||||
return false;
|
||||
}
|
||||
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
|
||||
// Parse stats
|
||||
if (jsonData.stats) {
|
||||
debugLog('[RPG Parser] Parsing stats:', Object.keys(jsonData.stats));
|
||||
const customStats = trackerConfig?.userStats?.customStats || [];
|
||||
|
||||
for (const [statName, value] of Object.entries(jsonData.stats)) {
|
||||
// Find matching stat in config
|
||||
const statConfig = customStats.find(s =>
|
||||
s.name.toLowerCase() === statName.toLowerCase()
|
||||
);
|
||||
if (statConfig && typeof value === 'number') {
|
||||
// Store in userStats using the stat id
|
||||
extensionSettings.userStats[statConfig.id] = Math.max(0, Math.min(100, value));
|
||||
debugLog(`[RPG Parser] Stat ${statConfig.name}: ${value}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse status
|
||||
if (jsonData.status) {
|
||||
if (jsonData.status.mood) {
|
||||
extensionSettings.userStats.mood = jsonData.status.mood;
|
||||
debugLog('[RPG Parser] Mood:', jsonData.status.mood);
|
||||
}
|
||||
if (jsonData.status.fields) {
|
||||
// Store custom status fields
|
||||
extensionSettings.userStats.conditions = Object.values(jsonData.status.fields).join(', ') || 'None';
|
||||
debugLog('[RPG Parser] Status fields:', jsonData.status.fields);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse infoBox - normalize values and filter out null
|
||||
if (jsonData.infoBox) {
|
||||
const infoBox = {};
|
||||
// Only copy non-null values
|
||||
for (const [key, val] of Object.entries(jsonData.infoBox)) {
|
||||
if (val !== null && val !== undefined && val !== 'null') {
|
||||
infoBox[key] = val;
|
||||
}
|
||||
}
|
||||
// Normalize recentEvents - LLM sometimes returns string instead of array
|
||||
if (infoBox.recentEvents && typeof infoBox.recentEvents === 'string') {
|
||||
infoBox.recentEvents = [infoBox.recentEvents];
|
||||
} else if (!infoBox.recentEvents || !Array.isArray(infoBox.recentEvents)) {
|
||||
infoBox.recentEvents = [];
|
||||
}
|
||||
// Filter out null/empty events from array
|
||||
infoBox.recentEvents = infoBox.recentEvents.filter(e => e && e !== 'null');
|
||||
extensionSettings.infoBoxData = infoBox;
|
||||
debugLog('[RPG Parser] InfoBox:', Object.keys(infoBox));
|
||||
}
|
||||
|
||||
// Parse characters - store for UI rendering
|
||||
if (jsonData.characters && Array.isArray(jsonData.characters)) {
|
||||
extensionSettings.charactersData = jsonData.characters;
|
||||
debugLog('[RPG Parser] Characters:', jsonData.characters.length);
|
||||
}
|
||||
|
||||
// Parse inventory (structured format)
|
||||
// Handle LLM variations: empty objects {} should become empty arrays []
|
||||
if (jsonData.inventory) {
|
||||
const normalizeArray = (val) => {
|
||||
if (Array.isArray(val)) return val;
|
||||
if (val && typeof val === 'object' && Object.keys(val).length === 0) return [];
|
||||
return [];
|
||||
};
|
||||
|
||||
// Get all item names from current inventory BEFORE updating
|
||||
const getItemNamesFromInventory = (inv) => {
|
||||
const names = new Set();
|
||||
if (!inv) return names;
|
||||
// From onPerson array
|
||||
if (Array.isArray(inv.onPerson)) {
|
||||
inv.onPerson.forEach(item => {
|
||||
const name = typeof item === 'string' ? item : item?.name;
|
||||
if (name) names.add(name.toLowerCase());
|
||||
});
|
||||
}
|
||||
// From stored locations
|
||||
if (inv.stored && typeof inv.stored === 'object') {
|
||||
Object.values(inv.stored).forEach(items => {
|
||||
if (Array.isArray(items)) {
|
||||
items.forEach(item => {
|
||||
const name = typeof item === 'string' ? item : item?.name;
|
||||
if (name) names.add(name.toLowerCase());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// From assets
|
||||
if (Array.isArray(inv.assets)) {
|
||||
inv.assets.forEach(item => {
|
||||
const name = typeof item === 'string' ? item : item?.name;
|
||||
if (name) names.add(name.toLowerCase());
|
||||
});
|
||||
}
|
||||
return names;
|
||||
};
|
||||
|
||||
const previousItemNames = getItemNamesFromInventory(extensionSettings.inventoryV3);
|
||||
|
||||
extensionSettings.inventoryV3 = {
|
||||
onPerson: normalizeArray(jsonData.inventory.onPerson || jsonData.inventory.items),
|
||||
stored: jsonData.inventory.stored && typeof jsonData.inventory.stored === 'object'
|
||||
? jsonData.inventory.stored : {},
|
||||
assets: normalizeArray(jsonData.inventory.assets)
|
||||
};
|
||||
debugLog('[RPG Parser] Inventory parsed - onPerson:', extensionSettings.inventoryV3.onPerson.length);
|
||||
|
||||
// Detect removed items and handle skill links
|
||||
const newItemNames = getItemNamesFromInventory(extensionSettings.inventoryV3);
|
||||
previousItemNames.forEach(itemName => {
|
||||
if (!newItemNames.has(itemName)) {
|
||||
debugLog('[RPG Parser] Item removed by LLM:', itemName);
|
||||
// Handle item removal - will unlink or delete skills based on settings
|
||||
handleItemRemoved(itemName);
|
||||
}
|
||||
});
|
||||
|
||||
// Also update legacy inventory for backwards compatibility
|
||||
const itemsToString = (items) => {
|
||||
if (!items || items.length === 0) return 'None';
|
||||
return items.map(i => i.name).join(', ');
|
||||
};
|
||||
extensionSettings.userStats.inventory = {
|
||||
version: 2,
|
||||
onPerson: itemsToString(extensionSettings.inventoryV3.onPerson),
|
||||
stored: Object.fromEntries(
|
||||
Object.entries(extensionSettings.inventoryV3.stored).map(([k, v]) => [k, itemsToString(v)])
|
||||
),
|
||||
assets: itemsToString(extensionSettings.inventoryV3.assets)
|
||||
};
|
||||
}
|
||||
|
||||
// Parse skills (structured format) - handle array/object/string variations
|
||||
if (jsonData.skills && typeof jsonData.skills === 'object') {
|
||||
// Normalize skills - each category should be an array
|
||||
const normalizedSkills = {};
|
||||
for (const [category, abilities] of Object.entries(jsonData.skills)) {
|
||||
if (Array.isArray(abilities)) {
|
||||
normalizedSkills[category] = abilities;
|
||||
} else if (typeof abilities === 'string') {
|
||||
// LLM returned string instead of array - split by comma
|
||||
normalizedSkills[category] = abilities.split(',').map(a => ({ name: a.trim(), description: '' })).filter(a => a.name);
|
||||
} else {
|
||||
normalizedSkills[category] = [];
|
||||
}
|
||||
}
|
||||
extensionSettings.skillsV2 = normalizedSkills;
|
||||
debugLog('[RPG Parser] Skills parsed:', Object.keys(normalizedSkills));
|
||||
|
||||
// Update legacy skills data for backwards compatibility
|
||||
for (const [category, abilities] of Object.entries(normalizedSkills)) {
|
||||
if (!extensionSettings.skillsData) extensionSettings.skillsData = {};
|
||||
const names = abilities.map(a => typeof a === 'string' ? a : (a?.name || '')).filter(n => n);
|
||||
extensionSettings.skillsData[category] = names.join(', ') || 'None';
|
||||
}
|
||||
}
|
||||
|
||||
// Parse quests - handle different formats
|
||||
if (jsonData.quests) {
|
||||
// Normalize main quest - could be string, object with name, or null
|
||||
let mainName = 'None';
|
||||
let mainDesc = '';
|
||||
if (jsonData.quests.main) {
|
||||
if (typeof jsonData.quests.main === 'string') {
|
||||
mainName = jsonData.quests.main;
|
||||
} else if (jsonData.quests.main.name) {
|
||||
mainName = jsonData.quests.main.name;
|
||||
mainDesc = jsonData.quests.main.description || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize optional quests - could be array of strings or objects
|
||||
const optionalQuests = Array.isArray(jsonData.quests.optional) ? jsonData.quests.optional : [];
|
||||
const optionalNames = optionalQuests.map(q => typeof q === 'string' ? q : (q?.name || '')).filter(n => n);
|
||||
const optionalDescs = optionalQuests.map(q => typeof q === 'string' ? '' : (q?.description || ''));
|
||||
|
||||
extensionSettings.quests = {
|
||||
main: mainName,
|
||||
mainDescription: mainDesc,
|
||||
optional: optionalNames,
|
||||
optionalDescriptions: optionalDescs
|
||||
};
|
||||
// Store structured quests too
|
||||
extensionSettings.questsV2 = jsonData.quests;
|
||||
debugLog('[RPG Parser] Quests - main:', mainName);
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
|
||||
debugLog('[RPG Parser] JSON parsing complete');
|
||||
debugLog('[RPG Parser] =======================================================');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for parsing responses - tries JSON first, falls back to text
|
||||
* @param {string} responseText - The raw AI response
|
||||
* @returns {boolean} Whether JSON parsing was successful
|
||||
*/
|
||||
export function tryParseJSONResponse(responseText) {
|
||||
const jsonData = extractJSONFromCodeBlock(responseText);
|
||||
if (jsonData) {
|
||||
return parseJSONTrackerData(jsonData);
|
||||
}
|
||||
|
||||
debugLog('[RPG Parser] No valid JSON found, falling back to text parsing');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the model response to extract the different data sections.
|
||||
* Extracts tracker data from markdown code blocks in the AI response.
|
||||
* Handles both separate code blocks and combined code blocks gracefully.
|
||||
*
|
||||
* @param {string} responseText - The raw AI response text
|
||||
* @returns {{userStats: string|null, infoBox: string|null, characterThoughts: string|null}} Parsed tracker data
|
||||
* @returns {{userStats: string|null, infoBox: string|null, characterThoughts: string|null, skills: string|null}} Parsed tracker data
|
||||
*/
|
||||
export function parseResponse(responseText) {
|
||||
const result = {
|
||||
userStats: null,
|
||||
infoBox: null,
|
||||
characterThoughts: null
|
||||
characterThoughts: null,
|
||||
skills: null
|
||||
};
|
||||
|
||||
// DEBUG: Log full response for troubleshooting
|
||||
@@ -213,7 +493,12 @@ export function parseResponse(responseText) {
|
||||
(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i)) ||
|
||||
// Fallback: inventory-only or quests-only blocks (no stats header)
|
||||
(content.match(/^(On Person:|Inventory:|Main Quests?:|Optional Quests:)/im) &&
|
||||
!content.match(/Info Box/i) && !content.match(/Present Characters/i));
|
||||
!content.match(/Info Box/i) && !content.match(/Present Characters/i) && !content.match(/Skills\s*\n\s*---/i));
|
||||
|
||||
// Match Skills section (separate from stats when showSkills is enabled)
|
||||
const isSkills =
|
||||
content.match(/Skills\s*\n\s*---/i) &&
|
||||
!content.match(/Stats\s*\n\s*---/i); // Make sure it's not a combined block
|
||||
|
||||
// Match Info Box section - flexible patterns
|
||||
const isInfoBox =
|
||||
@@ -234,6 +519,9 @@ export function parseResponse(responseText) {
|
||||
if (isStats && !result.userStats) {
|
||||
result.userStats = stripBrackets(content);
|
||||
debugLog('[RPG Parser] ✓ Matched: Stats section');
|
||||
} else if (isSkills && !result.skills) {
|
||||
result.skills = stripBrackets(content);
|
||||
debugLog('[RPG Parser] ✓ Matched: Skills section');
|
||||
} else if (isInfoBox && !result.infoBox) {
|
||||
result.infoBox = stripBrackets(content);
|
||||
debugLog('[RPG Parser] ✓ Matched: Info Box section');
|
||||
@@ -249,12 +537,14 @@ export function parseResponse(responseText) {
|
||||
debugLog('[RPG Parser] - Has info keywords?', !!(content.match(/Date:/i) && content.match(/Location:/i)));
|
||||
debugLog('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(/Present Characters\s*\n\s*---/i));
|
||||
debugLog('[RPG Parser] - Has new format ("- Name" + "Details:")?', !!(content.match(/^-\s+\w+/m) && content.match(/Details:/i)));
|
||||
debugLog('[RPG Parser] - Has "Skills\\n---"?', !!content.match(/Skills\s*\n\s*---/i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('[RPG Parser] ==================== PARSE RESULTS ====================');
|
||||
debugLog('[RPG Parser] Found Stats:', !!result.userStats);
|
||||
debugLog('[RPG Parser] Found Skills:', !!result.skills);
|
||||
debugLog('[RPG Parser] Found Info Box:', !!result.infoBox);
|
||||
debugLog('[RPG Parser] Found Characters:', !!result.characterThoughts);
|
||||
debugLog('[RPG Parser] =======================================================');
|
||||
@@ -425,6 +715,125 @@ export function parseUserStats(statsText) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses skills from the separate skills code block.
|
||||
* Used when showSkills is enabled (skills appear in their own section).
|
||||
* Format expected:
|
||||
* Skills
|
||||
* ---
|
||||
* Combat: Sword Fighting, Shield Block, Parry
|
||||
* Stealth: Lockpicking (via Lockpicks), Sneaking, Pickpocketing
|
||||
* Magic: None
|
||||
*
|
||||
* Each skill category contains a comma-separated list of abilities.
|
||||
* Abilities can be linked to items using "(via ItemName)" format.
|
||||
*
|
||||
* @param {string} skillsText - The raw skills text from AI response
|
||||
*/
|
||||
export function parseSkills(skillsText) {
|
||||
debugLog('[RPG Parser] ==================== PARSING SKILLS ====================');
|
||||
debugLog('[RPG Parser] Skills text length:', skillsText?.length + ' chars');
|
||||
|
||||
if (!skillsText || typeof skillsText !== 'string') {
|
||||
debugLog('[RPG Parser] No skills text to parse');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize data structures if needed
|
||||
if (!extensionSettings.skillsData) {
|
||||
extensionSettings.skillsData = {};
|
||||
}
|
||||
if (!extensionSettings.skillAbilityLinks) {
|
||||
extensionSettings.skillAbilityLinks = {};
|
||||
}
|
||||
|
||||
// Get configured skill categories
|
||||
const configuredCategories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
|
||||
const lines = skillsText.split('\n');
|
||||
const newSkillAbilityLinks = {};
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip header lines and notes
|
||||
if (line.match(/^Skills\s*$/i) || line.match(/^---/) || !line.trim() || line.match(/^\(Note:/i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse skill category line: "CategoryName: ability1, ability2 (via Item), ability3"
|
||||
const categoryMatch = line.match(/^(.+?):\s*(.*)$/);
|
||||
if (categoryMatch) {
|
||||
const categoryName = categoryMatch[1].trim();
|
||||
const abilitiesText = categoryMatch[2].trim();
|
||||
|
||||
// Check if this is a configured category (case-insensitive match)
|
||||
const matchedCategory = configuredCategories.find(c =>
|
||||
c.toLowerCase() === categoryName.toLowerCase()
|
||||
);
|
||||
|
||||
if (!matchedCategory) {
|
||||
debugLog(`[RPG Parser] Skipping unknown skill category: ${categoryName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse abilities (comma-separated)
|
||||
if (!abilitiesText || abilitiesText.toLowerCase() === 'none') {
|
||||
extensionSettings.skillsData[matchedCategory] = 'None';
|
||||
debugLog(`[RPG Parser] Skill category ${matchedCategory}: None`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split by comma and parse each ability
|
||||
const abilities = [];
|
||||
const rawAbilities = abilitiesText.split(',').map(a => a.trim()).filter(a => a);
|
||||
|
||||
for (const rawAbility of rawAbilities) {
|
||||
// Check for "(ItemName)" pattern - ability granted by item
|
||||
// Supports both "AbilityName (ItemName)" and legacy "AbilityName (via ItemName)"
|
||||
const itemMatch = rawAbility.match(/^(.+?)\s*\((?:via\s+)?(.+?)\)$/i);
|
||||
|
||||
if (itemMatch) {
|
||||
const abilityName = itemMatch[1].trim();
|
||||
const itemName = itemMatch[2].trim();
|
||||
abilities.push(abilityName);
|
||||
|
||||
// Store the link
|
||||
if (extensionSettings.enableItemSkillLinks) {
|
||||
const linkKey = `${matchedCategory}::${abilityName}`;
|
||||
newSkillAbilityLinks[linkKey] = itemName;
|
||||
debugLog(`[RPG Parser] Linked: ${abilityName} <- ${itemName}`);
|
||||
}
|
||||
} else {
|
||||
abilities.push(rawAbility);
|
||||
}
|
||||
}
|
||||
|
||||
// Store abilities for this category
|
||||
const abilitiesString = abilities.length > 0 ? abilities.join(', ') : 'None';
|
||||
extensionSettings.skillsData[matchedCategory] = abilitiesString;
|
||||
debugLog(`[RPG Parser] Skill category ${matchedCategory}: ${abilitiesString}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update skill-ability links if item linking is enabled
|
||||
if (extensionSettings.enableItemSkillLinks && Object.keys(newSkillAbilityLinks).length > 0) {
|
||||
// Merge new links with existing ones
|
||||
extensionSettings.skillAbilityLinks = {
|
||||
...extensionSettings.skillAbilityLinks,
|
||||
...newSkillAbilityLinks
|
||||
};
|
||||
debugLog('[RPG Parser] Skill-ability links updated:', Object.keys(newSkillAbilityLinks).length + ' new links');
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
debugLog('[RPG Parser] Skills saved successfully');
|
||||
debugLog('[RPG Parser] =======================================================');
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Error parsing skills:', error);
|
||||
debugLog('[RPG Parser] ERROR:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Extract code blocks from text
|
||||
* @param {string} text - Text containing markdown code blocks
|
||||
|
||||
@@ -7,9 +7,11 @@ import { getContext } from '../../../../../../extensions.js';
|
||||
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
|
||||
import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js';
|
||||
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
|
||||
import { generateSchemaExample } from '../../types/trackerData.js';
|
||||
|
||||
// Type imports
|
||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||
/** @typedef {import('../../types/trackerData.js').TrackerData} TrackerData */
|
||||
|
||||
/**
|
||||
* Default HTML prompt text
|
||||
@@ -17,11 +19,17 @@ 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 tracker instruction prompt text
|
||||
* Default tracker instruction prompt text (legacy text format)
|
||||
* Use {{user}} as placeholder for the user's name (will be replaced at runtime)
|
||||
*/
|
||||
export const DEFAULT_TRACKER_PROMPT = `At 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. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that {{user}} 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 😊. 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).`;
|
||||
|
||||
/**
|
||||
* Default JSON tracker instruction prompt text
|
||||
* Use {{user}} as placeholder for the user's name (will be replaced at runtime)
|
||||
*/
|
||||
export const DEFAULT_JSON_TRACKER_PROMPT = `At the start of every reply, output a JSON object inside a markdown code fence (with \`\`\`json). This tracks {{user}}'s stats, inventory, skills, and scene information. Follow the exact schema shown below. Use concrete values - no placeholders or brackets. Update stats realistically based on actions and time (0% change for minutes, 1-5% normally, 5%+ only for major events). Items and skills have "name" and "description" fields. Items can grant skills via "grantsSkill", and skills show their source via "grantedBy".`;
|
||||
|
||||
/**
|
||||
* Gets character card information for current chat (handles both single and group chats)
|
||||
* @returns {string} Formatted character information
|
||||
@@ -173,6 +181,9 @@ function buildAttributesString() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use generateJSONTrackerInstructions instead. This legacy text format
|
||||
* is kept for backwards compatibility with older LLM responses.
|
||||
*
|
||||
* Generates an example block showing current tracker states in markdown code blocks.
|
||||
* Uses COMMITTED data (not displayed data) for generation context.
|
||||
*
|
||||
@@ -240,6 +251,9 @@ export function generateTrackerExample() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use generateJSONTrackerInstructions instead. This legacy text format
|
||||
* is kept for backwards compatibility with older LLM responses.
|
||||
*
|
||||
* Generates the instruction portion - format specifications and guidelines.
|
||||
*
|
||||
* @param {boolean} includeHtmlPrompt - Whether to include the HTML prompt (true for main generation, false for separate tracker generation)
|
||||
@@ -253,10 +267,10 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
let instructions = '';
|
||||
|
||||
// Check if any trackers are enabled (including inventory and quests as independent sections)
|
||||
// Check if any trackers are enabled (including inventory, skills and quests as independent sections)
|
||||
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox ||
|
||||
extensionSettings.showCharacterThoughts || extensionSettings.showInventory ||
|
||||
extensionSettings.showQuests;
|
||||
extensionSettings.showCharacterThoughts || extensionSettings.showSkills ||
|
||||
extensionSettings.showInventory || extensionSettings.showQuests;
|
||||
|
||||
// Only add tracker instructions if at least one tracker is enabled
|
||||
if (hasAnyTrackers) {
|
||||
@@ -295,8 +309,9 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
}
|
||||
}
|
||||
|
||||
// Add skills section if enabled
|
||||
if (userStatsConfig?.skillsSection?.enabled) {
|
||||
// Add skills section if enabled in config AND NOT shown as separate section
|
||||
// When showSkills is true, skills are in their own tab and have their own code block
|
||||
if (userStatsConfig?.skillsSection?.enabled && !extensionSettings.showSkills) {
|
||||
const skillFields = userStatsConfig.skillsSection.customFields || [];
|
||||
const skillFieldsText = skillFields.map(f => `[${f}]`).join(', ');
|
||||
instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`;
|
||||
@@ -329,6 +344,33 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
instructions += '```\n\n';
|
||||
}
|
||||
|
||||
// Add separate skills section when showSkills is enabled
|
||||
if (extensionSettings.showSkills) {
|
||||
const skillsConfig = trackerConfig?.userStats?.skillsSection;
|
||||
const skillFields = skillsConfig?.customFields || [];
|
||||
|
||||
if (skillFields.length > 0) {
|
||||
instructions += '```\n';
|
||||
instructions += 'Skills\n';
|
||||
instructions += '---\n';
|
||||
|
||||
// Each skill category contains a list of abilities
|
||||
for (const skillName of skillFields) {
|
||||
if (extensionSettings.enableItemSkillLinks) {
|
||||
instructions += `${skillName}: [Abilities in this category, e.g. "Sword Fighting (Iron Sword), Parry" or "None"]\n`;
|
||||
} else {
|
||||
instructions += `${skillName}: [Abilities in this category, e.g. "Lockpicking, Sneaking" or "None"]\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionSettings.enableItemSkillLinks) {
|
||||
instructions += '\n(Abilities from items use parentheses: "Skill (Item)". Remove if item is removed or unequipped.)\n';
|
||||
}
|
||||
|
||||
instructions += '```\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionSettings.showInfoBox) {
|
||||
const infoBoxConfig = trackerConfig?.infoBox;
|
||||
const widgets = infoBoxConfig?.widgets || {};
|
||||
@@ -459,6 +501,220 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
return instructions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JSON-based tracker instructions.
|
||||
* Creates a prompt asking the LLM to output structured JSON data.
|
||||
*
|
||||
* @param {boolean} includeHtmlPrompt - Whether to include the HTML prompt
|
||||
* @param {boolean} includeContinuation - Whether to include continuation instruction
|
||||
* @param {boolean} includeAttributes - Whether to include RPG attributes
|
||||
* @returns {string} Formatted JSON instruction text for the AI
|
||||
*/
|
||||
export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includeContinuation = true, includeAttributes = true) {
|
||||
const userName = getContext().name1;
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
let instructions = '';
|
||||
|
||||
// Check which sections are enabled
|
||||
const showStats = extensionSettings.showUserStats;
|
||||
const showInfoBox = extensionSettings.showInfoBox;
|
||||
const showCharacters = extensionSettings.showCharacterThoughts;
|
||||
const showInventory = extensionSettings.showInventory;
|
||||
const showSkills = extensionSettings.showSkills;
|
||||
const showQuests = extensionSettings.showQuests;
|
||||
const enableItemSkillLinks = extensionSettings.enableItemSkillLinks;
|
||||
|
||||
const hasAnyTrackers = showStats || showInfoBox || showCharacters || showInventory || showSkills || showQuests;
|
||||
|
||||
if (!hasAnyTrackers) {
|
||||
return instructions;
|
||||
}
|
||||
|
||||
// JSON instruction header
|
||||
const jsonPrompt = (extensionSettings.customTrackerPrompt || DEFAULT_JSON_TRACKER_PROMPT).replace(/\{\{user\}\}/g, userName);
|
||||
instructions += `\n${jsonPrompt}\n\n`;
|
||||
|
||||
// Build the JSON schema example based on enabled sections
|
||||
instructions += '```json\n';
|
||||
instructions += '{\n';
|
||||
|
||||
let sections = [];
|
||||
|
||||
// Stats section
|
||||
if (showStats) {
|
||||
const enabledStats = trackerConfig?.userStats?.customStats?.filter(s => s?.enabled && s?.name) || [];
|
||||
if (enabledStats.length > 0) {
|
||||
let statsJson = ' "stats": {\n';
|
||||
statsJson += enabledStats.map(s => ` "${s.name}": 75`).join(',\n');
|
||||
statsJson += '\n }';
|
||||
sections.push(statsJson);
|
||||
}
|
||||
|
||||
// Status section
|
||||
const statusConfig = trackerConfig?.userStats?.statusSection;
|
||||
if (statusConfig?.enabled) {
|
||||
let statusJson = ' "status": {\n';
|
||||
const statusParts = [];
|
||||
if (statusConfig.showMoodEmoji) {
|
||||
statusParts.push(' "mood": "😊"');
|
||||
}
|
||||
const customFields = statusConfig.customFields || [];
|
||||
if (customFields.length > 0) {
|
||||
const fieldsJson = customFields.map(f => ` "${f}": "[${f} description]"`).join(',\n');
|
||||
statusParts.push(` "fields": {\n${fieldsJson}\n }`);
|
||||
}
|
||||
statusJson += statusParts.join(',\n');
|
||||
statusJson += '\n }';
|
||||
sections.push(statusJson);
|
||||
}
|
||||
}
|
||||
|
||||
// Info Box section
|
||||
if (showInfoBox) {
|
||||
const widgets = trackerConfig?.infoBox?.widgets || {};
|
||||
const infoParts = [];
|
||||
if (widgets.date?.enabled) infoParts.push(' "date": "Monday, March 15, 1242"');
|
||||
if (widgets.time?.enabled) infoParts.push(' "time": "14:00 → 15:30"');
|
||||
if (widgets.weather?.enabled) infoParts.push(' "weather": "☀️ Sunny"');
|
||||
if (widgets.temperature?.enabled) {
|
||||
const unit = widgets.temperature.unit === 'F' ? '°F' : '°C';
|
||||
infoParts.push(` "temperature": "22${unit}"`);
|
||||
}
|
||||
if (widgets.location?.enabled) infoParts.push(' "location": "Forest Clearing"');
|
||||
if (widgets.recentEvents?.enabled) infoParts.push(' "recentEvents": "Brief summary of recent events"');
|
||||
|
||||
if (infoParts.length > 0) {
|
||||
sections.push(' "infoBox": {\n' + infoParts.join(',\n') + '\n }');
|
||||
}
|
||||
}
|
||||
|
||||
// Characters section
|
||||
if (showCharacters) {
|
||||
const charConfig = trackerConfig?.presentCharacters || {};
|
||||
let charExample = ' {\n "name": "Character Name"';
|
||||
|
||||
if (charConfig.relationshipFields?.length > 0) {
|
||||
charExample += `,\n "relationship": "${charConfig.relationshipFields[0]}"`;
|
||||
}
|
||||
|
||||
const enabledFields = charConfig.customFields?.filter(f => f.enabled) || [];
|
||||
if (enabledFields.length > 0) {
|
||||
const fieldsJson = enabledFields.map(f => ` "${f.name}": "[${f.description || f.name}]"`).join(',\n');
|
||||
charExample += `,\n "fields": {\n${fieldsJson}\n }`;
|
||||
}
|
||||
|
||||
if (charConfig.thoughts?.enabled) {
|
||||
charExample += ',\n "thoughts": "Character\'s inner thoughts in first person..."';
|
||||
}
|
||||
|
||||
charExample += '\n }';
|
||||
sections.push(' "characters": [\n' + charExample + '\n ]');
|
||||
}
|
||||
|
||||
// Inventory section
|
||||
if (showInventory) {
|
||||
let invSection = ' "inventory": {\n';
|
||||
|
||||
if (extensionSettings.useSimplifiedInventory) {
|
||||
// Simplified: single list
|
||||
let itemExample = '{ "name": "Item Name", "description": "What it is" }';
|
||||
if (enableItemSkillLinks) {
|
||||
itemExample = '{ "name": "Iron Sword", "description": "A sturdy blade", "grantsSkill": "Sword Fighting" }';
|
||||
}
|
||||
invSection += ` "items": [${itemExample}]\n`;
|
||||
} else {
|
||||
// Full categorized inventory
|
||||
let itemExample = '{ "name": "Item", "description": "Description" }';
|
||||
if (enableItemSkillLinks) {
|
||||
itemExample = '{ "name": "Iron Sword", "description": "A sturdy blade", "grantsSkill": "Sword Fighting" }';
|
||||
}
|
||||
invSection += ` "onPerson": [${itemExample}],\n`;
|
||||
invSection += ' "stored": { "Location Name": [{ "name": "Stored Item", "description": "Description" }] },\n';
|
||||
invSection += ' "assets": [{ "name": "Property/Vehicle", "description": "Description" }]\n';
|
||||
}
|
||||
|
||||
invSection += ' }';
|
||||
sections.push(invSection);
|
||||
}
|
||||
|
||||
// Skills section
|
||||
if (showSkills) {
|
||||
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
if (skillCategories.length > 0) {
|
||||
let skillsSection = ' "skills": {\n';
|
||||
const categoryExamples = skillCategories.map(cat => {
|
||||
let skillExample = '{ "name": "Ability Name", "description": "What this ability does" }';
|
||||
if (enableItemSkillLinks) {
|
||||
skillExample = '{ "name": "Ability", "description": "Description", "grantedBy": "Item Name" }';
|
||||
}
|
||||
return ` "${cat}": [${skillExample}]`;
|
||||
});
|
||||
skillsSection += categoryExamples.join(',\n');
|
||||
skillsSection += '\n }';
|
||||
sections.push(skillsSection);
|
||||
}
|
||||
}
|
||||
|
||||
// Quests section
|
||||
if (showQuests) {
|
||||
let questsSection = ' "quests": {\n';
|
||||
questsSection += ' "main": { "name": "Main Quest Title", "description": "Primary objective" },\n';
|
||||
questsSection += ' "optional": [{ "name": "Side Quest", "description": "Optional objective" }]\n';
|
||||
questsSection += ' }';
|
||||
sections.push(questsSection);
|
||||
}
|
||||
|
||||
instructions += sections.join(',\n');
|
||||
instructions += '\n}\n```\n\n';
|
||||
|
||||
// Add notes about the format
|
||||
instructions += 'Important:\n';
|
||||
instructions += '- Output ONLY valid JSON inside the code fence\n';
|
||||
instructions += '- Use actual values, not placeholders like [Location]\n';
|
||||
instructions += '- Stats are percentages (0-100)\n';
|
||||
instructions += '- Empty arrays [] for sections with no items\n';
|
||||
instructions += '- null for main quest if none active\n';
|
||||
|
||||
if (enableItemSkillLinks) {
|
||||
instructions += '- Items can grant skills: add "grantsSkill": "Skill Name" to the item\n';
|
||||
instructions += '- Skills from items: add "grantedBy": "Item Name" to the skill\n';
|
||||
instructions += '- If an item is removed/lost, remove its linked skill too\n';
|
||||
}
|
||||
|
||||
instructions += '\n';
|
||||
|
||||
// Continuation instruction
|
||||
if (includeContinuation) {
|
||||
instructions += `After the JSON block, continue the story naturally from where the last message left off. The tracker data should reflect and influence the narrative - fatigue affects performance, mood colors dialogue, etc.\n\n`;
|
||||
}
|
||||
|
||||
// Attributes
|
||||
if (includeAttributes) {
|
||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
||||
|
||||
if (shouldSendAttributes) {
|
||||
const attributesString = buildAttributesString();
|
||||
instructions += `${userName}'s attributes: ${attributesString}\n`;
|
||||
|
||||
if (extensionSettings.lastDiceRoll) {
|
||||
const roll = extensionSettings.lastDiceRoll;
|
||||
instructions += `${userName} rolled ${roll.total} on ${roll.formula}. Determine success/failure based on attributes.\n\n`;
|
||||
} else {
|
||||
instructions += '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HTML prompt
|
||||
if (extensionSettings.enableHtmlPrompt && includeHtmlPrompt) {
|
||||
const htmlPrompt = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
|
||||
instructions += htmlPrompt;
|
||||
}
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formatted contextual summary for SEPARATE mode injection.
|
||||
* Includes the full tracker data in original format (without code fences and separators).
|
||||
@@ -561,75 +817,88 @@ export function generateContextualSummary() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the RPG tracking prompt text (for backward compatibility with separate mode).
|
||||
* Uses COMMITTED data (not displayed data) for generation context.
|
||||
* Generates the RPG tracking prompt text for separate mode.
|
||||
* Shows previous data in JSON format and requests JSON response.
|
||||
*
|
||||
* @returns {string} Full prompt text for separate tracker generation
|
||||
*/
|
||||
export function generateRPGPromptText() {
|
||||
// Use COMMITTED data for generation context, not displayed data
|
||||
const userName = getContext().name1;
|
||||
|
||||
let promptText = '';
|
||||
|
||||
promptText += `Here are the previous trackers in the roleplay that you should consider when responding:\n`;
|
||||
promptText += `Here are the previous trackers in JSON format that you should consider when responding:\n`;
|
||||
promptText += `<previous>\n`;
|
||||
|
||||
// Build previous state as JSON
|
||||
const previousState = {};
|
||||
|
||||
// Stats
|
||||
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`;
|
||||
const customStats = extensionSettings.trackerConfig?.userStats?.customStats?.filter(s => s?.enabled) || [];
|
||||
if (customStats.length > 0) {
|
||||
previousState.stats = {};
|
||||
for (const stat of customStats) {
|
||||
previousState.stats[stat.name] = extensionSettings.userStats[stat.id] || 100;
|
||||
}
|
||||
}
|
||||
|
||||
// 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`;
|
||||
|
||||
// Status
|
||||
const statusConfig = extensionSettings.trackerConfig?.userStats?.statusSection;
|
||||
if (statusConfig?.enabled) {
|
||||
previousState.status = {
|
||||
mood: extensionSettings.userStats.mood || '😐',
|
||||
fields: {}
|
||||
};
|
||||
const customFields = statusConfig.customFields || [];
|
||||
for (const field of customFields) {
|
||||
previousState.status.fields[field] = extensionSettings.userStats.conditions || 'None';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add current inventory to the previous data context - independent of showUserStats
|
||||
if (extensionSettings.showInventory && extensionSettings.userStats?.inventory) {
|
||||
const inventorySummary = buildInventorySummary(extensionSettings.userStats.inventory);
|
||||
if (inventorySummary && inventorySummary !== 'None') {
|
||||
promptText += `Last Inventory:\n${inventorySummary}\n\n`;
|
||||
|
||||
// InfoBox
|
||||
if (extensionSettings.showInfoBox && extensionSettings.infoBoxData) {
|
||||
previousState.infoBox = extensionSettings.infoBoxData;
|
||||
}
|
||||
|
||||
// Characters
|
||||
if (extensionSettings.showCharacterThoughts && extensionSettings.charactersData?.length > 0) {
|
||||
previousState.characters = extensionSettings.charactersData;
|
||||
}
|
||||
|
||||
// Inventory
|
||||
if (extensionSettings.showInventory) {
|
||||
if (extensionSettings.inventoryV3 && (extensionSettings.inventoryV3.onPerson?.length > 0 ||
|
||||
Object.keys(extensionSettings.inventoryV3.stored || {}).length > 0 ||
|
||||
extensionSettings.inventoryV3.assets?.length > 0)) {
|
||||
previousState.inventory = extensionSettings.inventoryV3;
|
||||
}
|
||||
}
|
||||
|
||||
// Add current quests to the previous data context - independent of showUserStats
|
||||
if (extensionSettings.showQuests && extensionSettings.quests) {
|
||||
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
|
||||
promptText += `Main Quests: ${extensionSettings.quests.main}\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`;
|
||||
}
|
||||
promptText += `\n`;
|
||||
|
||||
// Skills
|
||||
if (extensionSettings.showSkills && extensionSettings.skillsV2) {
|
||||
previousState.skills = extensionSettings.skillsV2;
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
// Quests
|
||||
if (extensionSettings.showQuests && extensionSettings.questsV2) {
|
||||
previousState.quests = extensionSettings.questsV2;
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
// Output as JSON if we have any data, otherwise indicate first update
|
||||
if (Object.keys(previousState).length > 0) {
|
||||
promptText += '```json\n';
|
||||
promptText += JSON.stringify(previousState, null, 2);
|
||||
promptText += '\n```\n';
|
||||
} else {
|
||||
promptText += 'None - this is the first update.\n';
|
||||
}
|
||||
|
||||
promptText += `</previous>\n`;
|
||||
|
||||
// Don't include HTML prompt, continuation instruction, or attributes for separate tracker generation
|
||||
promptText += generateTrackerInstructions(false, false, false);
|
||||
// Add JSON format instructions
|
||||
promptText += generateJSONTrackerInstructions(false, false, false);
|
||||
|
||||
return promptText;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user