fix: several issues

This commit is contained in:
Subarashimo
2025-12-03 22:34:50 +01:00
parent 0f7fdfcef1
commit c24515db7e
18 changed files with 766 additions and 1034 deletions
+103 -351
View File
@@ -6,7 +6,7 @@
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 { extensionSettings, committedTrackerData } from '../../core/state.js';
import { generateSchemaExample } from '../../types/trackerData.js';
// Type imports
@@ -18,12 +18,6 @@ import { generateSchemaExample } from '../../types/trackerData.js';
*/
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 (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)
@@ -158,12 +152,12 @@ function buildAttributesString() {
// Get enabled attributes from config
const rpgAttributes = userStatsConfig?.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 }
{ id: 'str', name: 'STR', description: '', enabled: true },
{ id: 'dex', name: 'DEX', description: '', enabled: true },
{ id: 'con', name: 'CON', description: '', enabled: true },
{ id: 'int', name: 'INT', description: '', enabled: true },
{ id: 'wis', name: 'WIS', description: '', enabled: true },
{ id: 'cha', name: 'CHA', description: '', enabled: true }
];
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
@@ -180,327 +174,6 @@ function buildAttributesString() {
return attributeParts.join(', ');
}
/**
* @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.
*
* @returns {string} Formatted example text with tracker data in code blocks
*/
export function generateTrackerExample() {
let example = '';
// Use COMMITTED data for generation context, not displayed data
// Wrap each tracker section in markdown code blocks
// Build a combined stats/inventory/quests block if any are enabled
let statsBlock = '';
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
statsBlock += committedTrackerData.userStats;
}
// Add inventory example if enabled (and not already in userStats) - case-insensitive check
if (extensionSettings.showInventory && extensionSettings.userStats?.inventory) {
const inventorySummary = buildInventorySummary(extensionSettings.userStats.inventory);
if (inventorySummary && inventorySummary !== 'None') {
// Only add if not already present in userStats (case-insensitive)
const statsBlockLower = statsBlock.toLowerCase();
if (!statsBlockLower.includes('on person:') && !statsBlockLower.includes('inventory:')) {
if (statsBlock) statsBlock += '\n';
statsBlock += inventorySummary;
}
}
}
// Add quests example if enabled - case-insensitive check
if (extensionSettings.showQuests && extensionSettings.quests) {
let questsText = '';
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
questsText += `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(', ');
if (optionalQuests) {
questsText += `Optional Quests: ${optionalQuests}`;
}
}
// Only add if not already present in userStats (case-insensitive)
const statsBlockLower = statsBlock.toLowerCase();
if (questsText && !statsBlockLower.includes('main quest') && !statsBlockLower.includes('optional quest')) {
if (statsBlock) statsBlock += '\n';
statsBlock += questsText;
}
}
if (statsBlock) {
example += '```\n' + statsBlock.trim() + '\n```\n\n';
}
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
example += '```\n' + committedTrackerData.infoBox + '\n```\n\n';
}
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
example += '```\n' + committedTrackerData.characterThoughts + '\n```';
}
return example.trim();
}
/**
* @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)
* @param {boolean} includeContinuation - Whether to include "After updating the trackers, continue..." instruction
* @param {boolean} includeAttributes - Whether to include RPG attributes (false for separate tracker generation)
* @returns {string} Formatted instruction text for the AI
*/
export function generateTrackerInstructions(includeHtmlPrompt = true, includeContinuation = true, includeAttributes = true) {
const userName = getContext().name1;
const classicStats = extensionSettings.classicStats;
const trackerConfig = extensionSettings.trackerConfig;
let instructions = '';
// Check if any trackers are enabled (including inventory, skills and quests as independent sections)
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox ||
extensionSettings.showCharacterThoughts || extensionSettings.showSkills ||
extensionSettings.showInventory || extensionSettings.showQuests;
// Only add tracker instructions if at least one tracker is enabled
if (hasAnyTrackers) {
// Universal instruction header - use custom prompt if set, otherwise use default
const trackerPrompt = (extensionSettings.customTrackerPrompt || DEFAULT_TRACKER_PROMPT).replace(/\{\{user\}\}/g, userName);
instructions += `\n${trackerPrompt}\n`;
// Check if we need a combined stats/inventory/quests code block
const hasStatsBlock = extensionSettings.showUserStats || extensionSettings.showInventory || extensionSettings.showQuests;
if (hasStatsBlock) {
const userStatsConfig = trackerConfig?.userStats;
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
instructions += '```\n';
// Add user stats section if enabled
if (extensionSettings.showUserStats) {
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 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`;
}
}
// Add inventory format - independent of showUserStats
if (extensionSettings.showInventory) {
if (extensionSettings.useSimplifiedInventory) {
// Simplified single-line inventory format
instructions += 'Inventory: [Items currently carried/worn/owned, or "None"]\n';
} else if (FEATURE_FLAGS.useNewInventory) {
// Full v2 categorized inventory format
instructions += 'On Person: [Items currently carried/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 - independent of showUserStats
if (extensionSettings.showQuests) {
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 += '```\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 || {};
instructions += '```\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 += '```\n\n';
}
if (extensionSettings.showCharacterThoughts) {
const presentCharsConfig = trackerConfig?.presentCharacters;
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
const relationshipFields = presentCharsConfig?.relationshipFields || [];
const thoughtsConfig = presentCharsConfig?.thoughts;
const characterStats = presentCharsConfig?.characterStats;
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
instructions += '```\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
instructions += `- [Name (do not include ${userName}; state "Unavailable" if no major characters are present in the scene)]\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`;
}
// Relationship line (only if relationships are enabled)
if (relationshipPlaceholders) {
instructions += `Relationship: [(choose one: ${relationshipPlaceholders})]\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`;
}
instructions += `- … (Repeat the format above for every other present major character)\n`;
instructions += '```\n\n';
}
// Only add continuation instruction if includeContinuation is true
if (includeContinuation) {
instructions += `After updating the trackers, continue directly from where the last message in the chat history left off. Ensure the trackers you provide naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting 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`;
}
// Include attributes based on settings (only if includeAttributes is true)
if (includeAttributes) {
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
if (shouldSendAttributes) {
const attributesString = buildAttributesString();
instructions += `${userName}'s attributes: ${attributesString}\n`;
// Add dice roll context if there was one
if (extensionSettings.lastDiceRoll) {
const roll = extensionSettings.lastDiceRoll;
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
} else {
instructions += `\n`;
}
}
}
}
// Append HTML prompt if enabled AND includeHtmlPrompt is true
if (extensionSettings.enableHtmlPrompt && includeHtmlPrompt) {
// Add newlines only if we had tracker instructions
if (hasAnyTrackers) {
instructions += ``;
} else {
instructions += `\n`;
}
// Use custom HTML prompt if set, otherwise use default
const htmlPrompt = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
instructions += htmlPrompt;
}
return instructions;
}
/**
* Generates JSON-based tracker instructions.
* Creates a prompt asking the LLM to output structured JSON data.
@@ -581,7 +254,7 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
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 (widgets.recentEvents?.enabled) infoParts.push(' "recentEvents": ["Event 1", "Event 2"]');
if (infoParts.length > 0) {
sections.push(' "infoBox": {\n' + infoParts.join(',\n') + '\n }');
@@ -591,10 +264,12 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
// Characters section
if (showCharacters) {
const charConfig = trackerConfig?.presentCharacters || {};
let charExample = ' {\n "name": "Character Name"';
let charExample = ' {\n "name": "Character Name",\n "emoji": "🧑"';
if (charConfig.relationshipFields?.length > 0) {
charExample += `,\n "relationship": "${charConfig.relationshipFields[0]}"`;
// Show allowed relationship values as explanation
const allowedRelationships = charConfig.relationshipFields.join(' | ');
charExample += `,\n "relationship": "(${allowedRelationships})"`;
}
const enabledFields = charConfig.customFields?.filter(f => f.enabled) || [];
@@ -603,6 +278,14 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
charExample += `,\n "fields": {\n${fieldsJson}\n }`;
}
// Character stats (Health, Arousal, etc.)
const charStatsConfig = charConfig.characterStats;
const enabledCharStats = charStatsConfig?.enabled && charStatsConfig?.customStats?.filter(s => s?.enabled && s?.name) || [];
if (enabledCharStats.length > 0) {
const statsJson = enabledCharStats.map(s => ` "${s.name}": 75`).join(',\n');
charExample += `,\n "stats": {\n${statsJson}\n }`;
}
if (charConfig.thoughts?.enabled) {
charExample += ',\n "thoughts": "Character\'s inner thoughts in first person..."';
}
@@ -640,14 +323,21 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
// Skills section
if (showSkills) {
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
if (skillCategories.length > 0) {
// Filter to only enabled categories and handle both old (string) and new (object) formats
const enabledCategories = skillCategories.filter(cat => {
if (typeof cat === 'string') return true;
return cat.enabled !== false;
});
if (enabledCategories.length > 0) {
let skillsSection = ' "skills": {\n';
const categoryExamples = skillCategories.map(cat => {
const categoryExamples = enabledCategories.map(cat => {
const catName = typeof cat === 'string' ? cat : cat.name;
let skillExample = '{ "name": "Ability Name", "description": "What this ability does" }';
if (enableItemSkillLinks) {
skillExample = '{ "name": "Ability", "description": "Description", "grantedBy": "Item Name" }';
}
return ` "${cat}": [${skillExample}]`;
return ` "${catName}": [${skillExample}]`;
});
skillsSection += categoryExamples.join(',\n');
skillsSection += '\n }';
@@ -675,10 +365,41 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
instructions += '- Empty arrays [] for sections with no items\n';
instructions += '- null for main quest if none active\n';
// Add stat descriptions if any have descriptions
if (showStats) {
const customStats = trackerConfig?.userStats?.customStats || [];
const statsWithDesc = customStats.filter(s => s?.enabled && s?.description);
if (statsWithDesc.length > 0) {
instructions += '- Stat meanings:\n';
statsWithDesc.forEach(stat => {
instructions += ` • "${stat.name}": ${stat.description}\n`;
});
}
}
if (showSkills) {
const skillsLabel = trackerConfig?.userStats?.skillsSection?.label || 'Skills';
if (skillsLabel !== 'Skills') {
instructions += `- The "skills" section represents "${skillsLabel}" in this context\n`;
}
// Add skill category descriptions if any have descriptions
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
const categoriesWithDesc = skillCategories.filter(cat =>
typeof cat === 'object' && cat.description && cat.enabled !== false
);
if (categoriesWithDesc.length > 0) {
instructions += `- ${skillsLabel} categories:\n`;
categoriesWithDesc.forEach(cat => {
instructions += ` • "${cat.name}": ${cat.description}\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 += '- Items can grant skills: add {"grantsSkill": "Skill Name"} to the item object\n';
instructions += '- When a skill comes from an item, add {"grantedBy": "Item Name"} to that skill object\n';
instructions += '- If an item is removed/lost, also remove any skill it granted\n';
}
instructions += '\n';
@@ -696,6 +417,16 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
if (shouldSendAttributes) {
const attributesString = buildAttributesString();
instructions += `${userName}'s attributes: ${attributesString}\n`;
// Add attribute descriptions if any have descriptions
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [];
const attrsWithDesc = rpgAttributes.filter(a => a?.enabled && a?.description);
if (attrsWithDesc.length > 0) {
instructions += 'Attribute meanings:\n';
attrsWithDesc.forEach(attr => {
instructions += `${attr.name}: ${attr.description}\n`;
});
}
if (extensionSettings.lastDiceRoll) {
const roll = extensionSettings.lastDiceRoll;
@@ -862,17 +593,38 @@ export function generateRPGPromptText() {
previousState.infoBox = extensionSettings.infoBoxData;
}
// Characters
// Characters - format to match schema
if (extensionSettings.showCharacterThoughts && extensionSettings.charactersData?.length > 0) {
previousState.characters = extensionSettings.charactersData;
// Ensure characters match the expected schema format
previousState.characters = extensionSettings.charactersData.map(char => {
const formatted = { name: char.name };
if (char.relationship) formatted.relationship = char.relationship;
if (char.emoji) formatted.emoji = char.emoji;
if (char.fields && Object.keys(char.fields).length > 0) formatted.fields = char.fields;
if (char.stats && Object.keys(char.stats).length > 0) formatted.stats = char.stats;
if (char.thoughts) formatted.thoughts = char.thoughts;
return formatted;
});
}
// 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;
// Inventory - format to match schema (use "items" for simplified mode)
if (extensionSettings.showInventory && extensionSettings.inventoryV3) {
const inv = extensionSettings.inventoryV3;
if (extensionSettings.useSimplifiedInventory) {
// Simplified mode uses "items" key
const items = inv.simplified || inv.onPerson || [];
if (items.length > 0) {
previousState.inventory = { items };
}
} else {
// Full categorized mode
if (inv.onPerson?.length > 0 || Object.keys(inv.stored || {}).length > 0 || inv.assets?.length > 0) {
previousState.inventory = {
onPerson: inv.onPerson || [],
stored: inv.stored || {},
assets: inv.assets || []
};
}
}
}