Major update: Full tracker customization system

Features:
- Complete tracker configuration UI with add/remove functionality
- User Stats: Custom stats, status fields, skills section
- Info Box: Configurable widgets (date, weather, temp, time, location, events)
- Present Characters: Custom fields, relationships, character stats, thoughts
- Character-specific stats with color interpolation
- New multi-line format for cleaner AI generation and parsing
- Auto-cleanup of placeholder brackets in AI responses
- Relationship badges with emoji mapping
- Advanced inventory v2 system with multi-location storage
- Responsive mobile support with horizontal scrolling
- Removed legacy format support for cleaner codebase
- Fixed context injection for together mode (no duplication)
- Updated README with new features and configuration guide
This commit is contained in:
Spicy_Marinara
2025-11-01 20:19:35 +01:00
parent 87cfcb6946
commit 897c0278fb
15 changed files with 2705 additions and 546 deletions
+179 -119
View File
@@ -270,151 +270,211 @@ export function renderInfoBox() {
// location: data.location
// });
// Get tracker configuration
const config = extensionSettings.trackerConfig?.infoBox;
// Build visual dashboard HTML
// Wrap all content in a scrollable container
let html = '<div class="rpg-info-content">';
// Row 1: Date, Weather, Temperature, Time widgets
html += '<div class="rpg-dashboard rpg-dashboard-row-1">';
const row1Widgets = [];
// Calendar widget - always show (editable even if empty)
// Display abbreviated version but allow editing full value
const monthShort = data.month ? data.month.substring(0, 3).toUpperCase() : 'MON';
const weekdayShort = data.weekday ? data.weekday.substring(0, 3).toUpperCase() : 'DAY';
const yearDisplay = data.year || 'YEAR';
html += `
<div class="rpg-dashboard-widget rpg-calendar-widget">
<div class="rpg-calendar-top rpg-editable" contenteditable="true" data-field="month" data-full-value="${data.month || ''}" title="Click to edit">${monthShort}</div>
<div class="rpg-calendar-day rpg-editable" contenteditable="true" data-field="weekday" data-full-value="${data.weekday || ''}" title="Click to edit">${weekdayShort}</div>
<div class="rpg-calendar-year rpg-editable" contenteditable="true" data-field="year" data-full-value="${data.year || ''}" title="Click to edit">${yearDisplay}</div>
</div>
`;
// Calendar widget - show if enabled
if (config?.widgets?.date?.enabled) {
// Apply date format conversion
let monthDisplay = data.month || 'MON';
let weekdayDisplay = data.weekday || 'DAY';
let yearDisplay = data.year || 'YEAR';
// Weather widget - always show (editable even if empty)
const weatherEmoji = data.weatherEmoji || '🌤️';
const weatherForecast = data.weatherForecast || 'Weather';
html += `
<div class="rpg-dashboard-widget rpg-weather-widget">
<div class="rpg-weather-icon rpg-editable" contenteditable="true" data-field="weatherEmoji" title="Click to edit emoji">${weatherEmoji}</div>
<div class="rpg-weather-forecast rpg-editable" contenteditable="true" data-field="weatherForecast" title="Click to edit">${weatherForecast}</div>
</div>
`;
// Apply format based on config
const dateFormat = config.widgets.date.format || 'dd/mm/yy';
if (dateFormat === 'dd/mm/yy') {
monthDisplay = monthDisplay.substring(0, 3).toUpperCase();
weekdayDisplay = weekdayDisplay.substring(0, 3).toUpperCase();
} else if (dateFormat === 'mm/dd/yy') {
// For US format, show month first, day second
monthDisplay = monthDisplay.substring(0, 3).toUpperCase();
weekdayDisplay = weekdayDisplay.substring(0, 3).toUpperCase();
} else if (dateFormat === 'yyyy-mm-dd') {
// ISO format - show full names
monthDisplay = monthDisplay;
weekdayDisplay = weekdayDisplay;
}
// Temperature widget - always show (editable even if empty)
const tempDisplay = data.temperature || '20°C';
const tempValue = data.tempValue || 20;
const tempPercent = Math.min(100, Math.max(0, ((tempValue + 20) / 60) * 100));
const tempColor = tempValue < 10 ? '#4a90e2' : tempValue < 25 ? '#67c23a' : '#e94560';
html += `
<div class="rpg-dashboard-widget rpg-temp-widget">
<div class="rpg-thermometer">
<div class="rpg-thermometer-bulb"></div>
<div class="rpg-thermometer-tube">
<div class="rpg-thermometer-fill" style="height: ${tempPercent}%; background: ${tempColor}"></div>
</div>
row1Widgets.push(`
<div class="rpg-dashboard-widget rpg-calendar-widget">
<div class="rpg-calendar-top rpg-editable" contenteditable="true" data-field="month" data-full-value="${data.month || ''}" title="Click to edit">${monthDisplay}</div>
<div class="rpg-calendar-day rpg-editable" contenteditable="true" data-field="weekday" data-full-value="${data.weekday || ''}" title="Click to edit">${weekdayDisplay}</div>
<div class="rpg-calendar-year rpg-editable" contenteditable="true" data-field="year" data-full-value="${data.year || ''}" title="Click to edit">${yearDisplay}</div>
</div>
<div class="rpg-temp-value rpg-editable" contenteditable="true" data-field="temperature" title="Click to edit">${tempDisplay}</div>
</div>
`;
// Time widget - always show (editable even if empty)
// Display the end time (second time in range) if available, otherwise start time
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
// Parse time for clock hands
const timeMatch = timeDisplay.match(/(\d+):(\d+)/);
let hourAngle = 0;
let minuteAngle = 0;
if (timeMatch) {
const hours = parseInt(timeMatch[1]);
const minutes = parseInt(timeMatch[2]);
hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
minuteAngle = minutes * 6; // 6° per minute
`);
}
html += `
<div class="rpg-dashboard-widget rpg-clock-widget">
<div class="rpg-clock">
<div class="rpg-clock-face">
<div class="rpg-clock-hour" style="transform: rotate(${hourAngle}deg)"></div>
<div class="rpg-clock-minute" style="transform: rotate(${minuteAngle}deg)"></div>
<div class="rpg-clock-center"></div>
// Weather widget - show if enabled
if (config?.widgets?.weather?.enabled) {
const weatherEmoji = data.weatherEmoji || '🌤️';
const weatherForecast = data.weatherForecast || 'Weather';
row1Widgets.push(`
<div class="rpg-dashboard-widget rpg-weather-widget">
<div class="rpg-weather-icon rpg-editable" contenteditable="true" data-field="weatherEmoji" title="Click to edit emoji">${weatherEmoji}</div>
<div class="rpg-weather-forecast rpg-editable" contenteditable="true" data-field="weatherForecast" title="Click to edit">${weatherForecast}</div>
</div>
`);
}
// Temperature widget - show if enabled
if (config?.widgets?.temperature?.enabled) {
let tempDisplay = data.temperature || '20°C';
let tempValue = data.tempValue || 20;
// Apply temperature unit conversion
const preferredUnit = config.widgets.temperature.unit || 'celsius';
if (data.temperature) {
// Detect current unit in the data
const isCelsius = tempDisplay.includes('°C');
const isFahrenheit = tempDisplay.includes('°F');
if (preferredUnit === 'fahrenheit' && isCelsius) {
// Convert C to F
const fahrenheit = Math.round((tempValue * 9/5) + 32);
tempDisplay = `${fahrenheit}°F`;
tempValue = fahrenheit;
} else if (preferredUnit === 'celsius' && isFahrenheit) {
// Convert F to C
const celsius = Math.round((tempValue - 32) * 5/9);
tempDisplay = `${celsius}°C`;
tempValue = celsius;
}
} else {
// No data yet, use default for preferred unit
tempDisplay = preferredUnit === 'fahrenheit' ? '68°F' : '20°C';
tempValue = preferredUnit === 'fahrenheit' ? 68 : 20;
}
const tempPercent = Math.min(100, Math.max(0, ((tempValue + 20) / 60) * 100));
const tempColor = tempValue < 10 ? '#4a90e2' : tempValue < 25 ? '#67c23a' : '#e94560';
row1Widgets.push(`
<div class="rpg-dashboard-widget rpg-temp-widget">
<div class="rpg-thermometer">
<div class="rpg-thermometer-bulb"></div>
<div class="rpg-thermometer-tube">
<div class="rpg-thermometer-fill" style="height: ${tempPercent}%; background: ${tempColor}"></div>
</div>
</div>
<div class="rpg-temp-value rpg-editable" contenteditable="true" data-field="temperature" title="Click to edit">${tempDisplay}</div>
</div>
`);
}
// Time widget - show if enabled
if (config?.widgets?.time?.enabled) {
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
// Parse time for clock hands
const timeMatch = timeDisplay.match(/(\d+):(\d+)/);
let hourAngle = 0;
let minuteAngle = 0;
if (timeMatch) {
const hours = parseInt(timeMatch[1]);
const minutes = parseInt(timeMatch[2]);
hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
minuteAngle = minutes * 6; // 6° per minute
}
row1Widgets.push(`
<div class="rpg-dashboard-widget rpg-clock-widget">
<div class="rpg-clock">
<div class="rpg-clock-face">
<div class="rpg-clock-hour" style="transform: rotate(${hourAngle}deg)"></div>
<div class="rpg-clock-minute" style="transform: rotate(${minuteAngle}deg)"></div>
<div class="rpg-clock-center"></div>
</div>
</div>
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeStart" title="Click to edit">${timeDisplay}</div>
</div>
`);
}
// Only create row 1 if there are widgets to show
if (row1Widgets.length > 0) {
html += '<div class="rpg-dashboard rpg-dashboard-row-1">';
html += row1Widgets.join('');
html += '</div>';
}
// Row 2: Location widget (full width) - show if enabled
if (config?.widgets?.location?.enabled) {
const locationDisplay = data.location || 'Location';
html += `
<div class="rpg-dashboard rpg-dashboard-row-2">
<div class="rpg-dashboard-widget rpg-location-widget">
<div class="rpg-map-bg">
<div class="rpg-map-marker">📍</div>
</div>
<div class="rpg-location-text rpg-editable" contenteditable="true" data-field="location" title="Click to edit">${locationDisplay}</div>
</div>
</div>
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeStart" title="Click to edit">${timeDisplay}</div>
</div>
`;
`;
}
html += '</div>';
// Row 2: Location widget (full width) - always show (editable even if empty)
const locationDisplay = data.location || 'Location';
html += `
<div class="rpg-dashboard rpg-dashboard-row-2">
<div class="rpg-dashboard-widget rpg-location-widget">
<div class="rpg-map-bg">
<div class="rpg-map-marker">📍</div>
</div>
<div class="rpg-location-text rpg-editable" contenteditable="true" data-field="location" title="Click to edit">${locationDisplay}</div>
</div>
</div>
`;
// Row 3: Recent Events widget (notebook style) - dynamically show 1-3 events
// Parse Recent Events from infoBox string
let recentEvents = [];
if (committedTrackerData.infoBox) {
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
if (recentEventsLine) {
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
if (eventsString) {
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
// Row 3: Recent Events widget (notebook style) - show if enabled
if (config?.widgets?.recentEvents?.enabled) {
// Parse Recent Events from infoBox string
let recentEvents = [];
if (committedTrackerData.infoBox) {
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
if (recentEventsLine) {
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
if (eventsString) {
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
}
}
}
}
const validEvents = recentEvents.filter(e => e && e.trim() && e !== 'Event 1' && e !== 'Event 2' && e !== 'Event 3');
const validEvents = recentEvents.filter(e => e && e.trim() && e !== 'Event 1' && e !== 'Event 2' && e !== 'Event 3');
// If no valid events, show at least one placeholder
if (validEvents.length === 0) {
validEvents.push('Click to add event');
}
// If no valid events, show at least one placeholder
if (validEvents.length === 0) {
validEvents.push('Click to add event');
}
html += `
<div class="rpg-dashboard rpg-dashboard-row-3">
<div class="rpg-dashboard-widget rpg-events-widget">
<div class="rpg-notebook-header">
<div class="rpg-notebook-ring"></div>
<div class="rpg-notebook-ring"></div>
<div class="rpg-notebook-ring"></div>
</div>
<div class="rpg-notebook-title">Recent Events</div>
<div class="rpg-notebook-lines">
`;
// Dynamically generate event lines (max 3)
for (let i = 0; i < Math.min(validEvents.length, 3); i++) {
html += `
<div class="rpg-notebook-line">
<span class="rpg-bullet">•</span>
<span class="rpg-event-text rpg-editable" contenteditable="true" data-field="event${i + 1}" title="Click to edit">${validEvents[i]}</span>
<div class="rpg-dashboard rpg-dashboard-row-3">
<div class="rpg-dashboard-widget rpg-events-widget">
<div class="rpg-notebook-header">
<div class="rpg-notebook-ring"></div>
<div class="rpg-notebook-ring"></div>
<div class="rpg-notebook-ring"></div>
</div>
<div class="rpg-notebook-title">Recent Events</div>
<div class="rpg-notebook-lines">
`;
}
// If we have less than 3 events, add empty placeholders with + icon
for (let i = validEvents.length; i < 3; i++) {
// Dynamically generate event lines (max 3)
for (let i = 0; i < Math.min(validEvents.length, 3); i++) {
html += `
<div class="rpg-notebook-line">
<span class="rpg-bullet">•</span>
<span class="rpg-event-text rpg-editable" contenteditable="true" data-field="event${i + 1}" title="Click to edit">${validEvents[i]}</span>
</div>
`;
}
// If we have less than 3 events, add empty placeholders with + icon
for (let i = validEvents.length; i < 3; i++) {
html += `
<div class="rpg-notebook-line rpg-event-add">
<span class="rpg-bullet">+</span>
<span class="rpg-event-text rpg-editable rpg-event-placeholder" contenteditable="true" data-field="event${i + 1}" title="Click to add event">Add event...</span>
</div>
`;
}
html += `
<div class="rpg-notebook-line rpg-event-add">
<span class="rpg-bullet">+</span>
<span class="rpg-event-text rpg-editable rpg-event-placeholder" contenteditable="true" data-field="event${i + 1}" title="Click to add event">Add event...</span>
</div>
`;
}
html += `
</div>
</div>
</div>
`;
`;
}
// Close the scrollable content wrapper
html += '</div>';
+222 -106
View File
@@ -27,6 +27,40 @@ function debugLog(message, data = null) {
}
}
/**
* Interpolates color based on percentage value between low and high colors
* @param {number} percentage - Value from 0-100
* @param {string} lowColor - Hex color for low values (e.g., '#ff0000')
* @param {string} highColor - Hex color for high values (e.g., '#00ff00')
* @returns {string} Interpolated hex color
*/
function getStatColor(percentage, lowColor, highColor) {
// Clamp percentage to 0-100
const percent = Math.max(0, Math.min(100, percentage)) / 100;
// Parse hex colors
const parsehex = (hex) => {
const clean = hex.replace('#', '');
return {
r: parseInt(clean.substring(0, 2), 16),
g: parseInt(clean.substring(2, 4), 16),
b: parseInt(clean.substring(4, 6), 16)
};
};
const low = parsehex(lowColor);
const high = parsehex(highColor);
// Interpolate each channel
const r = Math.round(low.r + (high.r - low.r) * percent);
const g = Math.round(low.g + (high.g - low.g) * percent);
const b = Math.round(low.b + (high.b - low.b) * percent);
// Convert back to hex
const toHex = (n) => n.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* Fuzzy name matching that handles:
* - Exact matches: "Sabrina" === "Sabrina"
@@ -76,11 +110,21 @@ export function renderThoughts() {
$thoughtsContainer.addClass('rpg-content-updating');
}
// Get tracker configuration
const config = extensionSettings.trackerConfig?.presentCharacters;
const enabledFields = config?.customFields?.filter(f => f && f.enabled && f.name) || [];
const characterStatsConfig = config?.characterStats;
const enabledCharStats = characterStatsConfig?.enabled && characterStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
const relationshipFields = config?.relationshipFields || [];
const hasRelationshipEnabled = relationshipFields.length > 0;
// Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh)
const characterThoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts || '';
debugLog('[RPG Thoughts] Raw characterThoughts data:', characterThoughtsData);
debugLog('[RPG Thoughts] Data length:', characterThoughtsData.length + ' chars');
debugLog('[RPG Thoughts] Enabled custom fields:', enabledFields.map(f => f.name));
debugLog('[RPG Thoughts] Enabled character stats:', enabledCharStats.map(s => s.name));
const lines = characterThoughtsData.split('\n');
const presentCharacters = [];
@@ -88,88 +132,96 @@ export function renderThoughts() {
debugLog('[RPG Thoughts] Split into lines count:', lines.length);
debugLog('[RPG Thoughts] Lines:', lines);
// Parse format: [Emoji]: [Name, Status, Demeanor] | [Relationship] | [Thoughts]
// Also supports 4-part format: [Emoji]: [Name, Status] | [Demeanor] | [Relationship] | [Thoughts]
// Parse new multi-line format:
// - [Name]
// Details: [Emoji] | [Field1] | [Field2] | ...
// Relationship: [Relationship]
// Stats: Stat1: X% | Stat2: X% | ...
// Thoughts: [Description]
let lineNumber = 0;
let currentCharacter = null;
for (const line of lines) {
lineNumber++;
// Skip empty lines, headers, dividers, and code fences
if (line.trim() &&
!line.includes('Present Characters') &&
!line.includes('---') &&
!line.trim().startsWith('```')) {
if (!line.trim() ||
line.includes('Present Characters') ||
line.includes('---') ||
line.trim().startsWith('```') ||
line.trim() === '- …' ||
line.includes('(Repeat the format')) {
continue;
}
debugLog(`[RPG Thoughts] Processing line ${lineNumber}:`, line);
debugLog(`[RPG Thoughts] Processing line ${lineNumber}:`, line);
// Match the new format with pipes
const parts = line.split('|').map(p => p.trim());
debugLog(`[RPG Thoughts] Split into ${parts.length} parts:`, parts);
// Check if this is a character name line (starts with "- ")
if (line.trim().startsWith('- ')) {
const name = line.trim().substring(2).trim();
// Require at least 3 parts (Emoji:Name | Relationship | Thoughts)
// This matches updateChatThoughts() and the current prompt format
if (parts.length >= 3) {
// First part: [Emoji]: [Name, Status, Demeanor]
const firstPart = parts[0].trim();
const emojiMatch = firstPart.match(/^(.+?):\s*(.+)$/);
if (emojiMatch) {
const emoji = emojiMatch[1].trim();
const info = emojiMatch[2].trim();
debugLog(`[RPG Thoughts] Emoji match found - emoji: "${emoji}", info: "${info}"`);
// Handle both 3-part and 4-part formats
let relationship, thoughts, traits;
if (parts.length === 3) {
// 3-part format: Emoji:Name,traits | Relationship | Thoughts
relationship = parts[1].trim();
thoughts = parts[2].trim();
const infoParts = info.split(',').map(p => p.trim());
traits = infoParts.slice(1).join(', ');
debugLog('[RPG Thoughts] Parsed as 3-part format');
} else {
// 4-part format: Emoji:Name,traits | Demeanor | Relationship | Thoughts
// Add the demeanor to traits and use last two parts for relationship/thoughts
const demeanor = parts[1].trim();
relationship = parts[2].trim();
thoughts = parts[3].trim();
const infoParts = info.split(',').map(p => p.trim());
const baseTraits = infoParts.slice(1).join(', ');
traits = baseTraits ? `${baseTraits}, ${demeanor}` : demeanor;
debugLog('[RPG Thoughts] Parsed as 4-part format');
}
// Parse name from info (first part before comma)
const infoParts = info.split(',').map(p => p.trim());
const name = infoParts[0] || '';
debugLog(`[RPG Thoughts] Extracted - name: "${name}", traits: "${traits}", relationship: "${relationship}", thoughts: "${thoughts}"`);
if (name && name.toLowerCase() !== 'unavailable') {
presentCharacters.push({ emoji, name, traits, relationship, thoughts });
debugLog(`[RPG Thoughts] ✓ Added character: ${name}`);
} else {
debugLog(`[RPG Thoughts] ✗ Rejected character - name: "${name}" (unavailable or empty)`);
}
} else {
debugLog('[RPG Thoughts] ✗ No emoji match found in first part');
}
if (name && name.toLowerCase() !== 'unavailable') {
currentCharacter = { name };
presentCharacters.push(currentCharacter);
debugLog(`[RPG Thoughts] ✓ Started new character: ${name}`);
} else {
debugLog(`[RPG Thoughts] ✗ Not enough parts (${parts.length} < 3, need at least Emoji:Name | Relationship | Thoughts)`);
currentCharacter = null;
debugLog(`[RPG Thoughts] ✗ Rejected character - name: "${name}" (unavailable or empty)`);
}
}
// Check if this is a Details line
else if (line.trim().startsWith('Details:') && currentCharacter) {
const detailsContent = line.substring(line.indexOf(':') + 1).trim();
const parts = detailsContent.split('|').map(p => p.trim());
// First part is the emoji
if (parts.length > 0) {
currentCharacter.emoji = parts[0];
debugLog(`[RPG Thoughts] Parsed emoji: ${parts[0]}`);
}
// Remaining parts are custom fields
for (let i = 0; i < enabledFields.length && i + 1 < parts.length; i++) {
const fieldName = enabledFields[i].name;
currentCharacter[fieldName] = parts[i + 1];
debugLog(`[RPG Thoughts] Parsed field ${fieldName}: ${parts[i + 1]}`);
}
}
// Check if this is a Relationship line
else if (line.trim().startsWith('Relationship:') && currentCharacter) {
const relationship = line.substring(line.indexOf(':') + 1).trim();
currentCharacter.Relationship = relationship;
debugLog(`[RPG Thoughts] Parsed relationship: ${relationship}`);
}
// Check if this is a Stats line
else if (line.trim().startsWith('Stats:') && currentCharacter && enabledCharStats.length > 0) {
const statsContent = line.substring(line.indexOf(':') + 1).trim();
const statParts = statsContent.split('|').map(p => p.trim());
for (const statPart of statParts) {
const statMatch = statPart.match(/^(.+?):\s*(\d+)%$/);
if (statMatch) {
const statName = statMatch[1].trim();
const statValue = parseInt(statMatch[2]);
currentCharacter[statName] = statValue;
debugLog(`[RPG Thoughts] Parsed stat: ${statName} = ${statValue}%`);
}
}
}
// Check if this is a Thoughts line (handled separately for thought bubbles)
else if (line.trim().match(/^[A-Z][a-z]+:/) && currentCharacter) {
// This could be Thoughts, Feelings, etc. - skip for now, handled in thought bubble rendering
debugLog(`[RPG Thoughts] Skipping thoughts/feelings line (handled in bubble rendering)`);
}
}
// Relationship status to emoji mapping
// Relationship status to emoji mapping (for backward compatibility with old "relationship" field)
const relationshipEmojis = {
'Enemy': '⚔️',
'Neutral': '⚖️',
'Friend': '⭐',
'Lover': '❤️'
};
debugLog('[RPG Thoughts] ==================== PARSING COMPLETE ====================');
debugLog('[RPG Thoughts] Total characters parsed:', presentCharacters.length);
debugLog('[RPG Thoughts] Characters array:', presentCharacters);
@@ -183,8 +235,7 @@ export function renderThoughts() {
// If no characters parsed, show a placeholder editable card
if (presentCharacters.length === 0) {
debugLog('[RPG Thoughts] ⚠ No characters parsed - showing placeholder card');
// Get default character portrait (try to use the current character if in 1-on-1 chat)
// Use a base64-encoded SVG placeholder as fallback to avoid 400 errors
// Get default character portrait
let defaultPortrait = FALLBACK_AVATAR_DATA_URI;
let defaultName = 'Character';
@@ -210,7 +261,17 @@ export function renderThoughts() {
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="emoji" title="Click to edit emoji">😊</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="name" title="Click to edit name">${defaultName}</span>
</div>
<div class="rpg-character-traits rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="traits" title="Click to edit traits">Traits</div>
`;
// Add custom fields dynamically
for (const field of enabledFields) {
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
html += `
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="${field.name}" title="Click to edit ${field.name}"></div>
`;
}
html += `
</div>
</div>
`;
@@ -286,8 +347,17 @@ export function renderThoughts() {
debugLog(`[RPG Thoughts] Final avatar for ${char.name}:`, characterPortrait.substring(0, 50) + '...');
// Get relationship emoji
const relationshipEmoji = relationshipEmojis[char.relationship] || '⚖️';
// Get relationship badge - only if relationships are enabled in config
let relationshipBadge = '⚖️'; // Default
let relationshipFieldName = 'Relationship';
if (hasRelationshipEnabled) {
// In the new format, relationship is always stored in char.Relationship
if (char.Relationship) {
// Try to map text to emoji
relationshipBadge = relationshipEmojis[char.Relationship] || char.Relationship;
}
}
debugLog(`[RPG Thoughts] Building HTML card for ${char.name}...`);
@@ -295,14 +365,45 @@ export function renderThoughts() {
<div class="rpg-character-card" data-character-name="${char.name}">
<div class="rpg-character-avatar">
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipEmoji}</div>
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
</div>
<div class="rpg-character-info">
<div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
<div class="rpg-character-content">
<div class="rpg-character-info">
<div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
</div>
`;
// Render custom fields dynamically
for (const field of enabledFields) {
const fieldValue = char[field.name] || '';
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
html += `
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</div>
`;
}
html += `
</div>
<div class="rpg-character-traits rpg-editable" contenteditable="true" data-character="${char.name}" data-field="traits" title="Click to edit traits">${char.traits}</div>
`;
// Render character stats if enabled (outside rpg-character-info)
if (enabledCharStats.length > 0) {
html += `<div class="rpg-character-stats"><div class="rpg-character-stats-inner">`;
for (const stat of enabledCharStats) {
const statValue = char[stat.name] || 0;
const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
html += `
<div class="rpg-character-stat">
<span class="rpg-stat-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" title="Click to edit ${stat.name}">${stat.name}: <span style="color: ${statColor}">${statValue}%</span></span>
</div>
`;
}
html += `</div></div>`;
}
html += `
</div>
</div>
`;
@@ -523,50 +624,65 @@ export function updateChatThoughts() {
// Parse the Present Characters data to get thoughts
const lines = lastGeneratedData.characterThoughts.split('\n');
const thoughtsArray = []; // Array of {name, emoji, thought}
const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts;
const thoughtsLabel = thoughtsConfig?.name || 'Thoughts';
// console.log('[RPG Companion] Parsing thoughts from lines:', lines);
for (const line of lines) {
if (line.trim() &&
!line.includes('Present Characters') &&
!line.includes('---') &&
!line.trim().startsWith('```')) {
// Parse new format to build character map and thoughts
let currentCharName = null;
let currentCharEmoji = null;
const parts = line.split('|').map(p => p.trim());
// console.log('[RPG Companion] Line parts:', parts);
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Handle both 3-part and 4-part formats
if (parts.length >= 3) {
const firstPart = parts[0].trim();
const emojiMatch = firstPart.match(/^(.+?):\s*(.+)$/);
if (!line ||
line.includes('Present Characters') ||
line.includes('---') ||
line.startsWith('```') ||
line.trim() === '- …' ||
line.includes('(Repeat the format')) {
continue;
}
if (emojiMatch) {
const emoji = emojiMatch[1].trim();
const info = emojiMatch[2].trim();
// Check if this is a character name line (starts with "- ")
if (line.startsWith('- ')) {
const name = line.substring(2).trim();
if (name && name.toLowerCase() !== 'unavailable') {
currentCharName = name;
currentCharEmoji = null; // Reset emoji for new character
} else {
currentCharName = null;
currentCharEmoji = null;
}
}
// Check if this is a Details line (contains the emoji)
else if (line.startsWith('Details:') && currentCharName) {
const detailsContent = line.substring(line.indexOf(':') + 1).trim();
const parts = detailsContent.split('|').map(p => p.trim());
let thoughts;
if (parts.length === 3) {
// 3-part format: Emoji:Name,traits | Relationship | Thoughts
thoughts = parts[2].trim();
} else if (parts.length >= 4) {
// 4-part format: Emoji:Name,traits | Demeanor | Relationship | Thoughts
thoughts = parts[3].trim();
}
// First part is the emoji
if (parts.length > 0) {
currentCharEmoji = parts[0];
}
}
// Check if this is a Thoughts line
else if (line.startsWith(thoughtsLabel + ':') && currentCharName && currentCharEmoji) {
const thoughtContent = line.substring(thoughtsLabel.length + 1).trim();
const infoParts = info.split(',').map(p => p.trim());
const name = infoParts[0] || '';
// console.log('[RPG Companion] Parsed thought - Name:', name, 'Thought:', thoughts);
if (name && thoughts && name.toLowerCase() !== 'unavailable') {
thoughtsArray.push({ name: name.toLowerCase(), emoji, thought: thoughts });
// console.log('[RPG Companion] Added to thoughtsArray:', name.toLowerCase());
}
}
// The thought content is just the text (no emoji prefix in new format)
if (thoughtContent) {
thoughtsArray.push({
name: currentCharName.toLowerCase(),
emoji: currentCharEmoji,
thought: thoughtContent
});
}
}
}
debugLog('[RPG Thoughts] Parsed thoughts:', thoughtsArray);
// If no thoughts parsed, return
if (thoughtsArray.length === 0) {
// console.log('[RPG Companion] No thoughts parsed, returning');
+115 -75
View File
@@ -26,16 +26,45 @@ import { buildInventorySummary } from '../generation/promptBuilder.js';
*/
export function buildUserStatsText() {
const stats = extensionSettings.userStats;
const statNames = extensionSettings.statNames || {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
const config = extensionSettings.trackerConfig?.userStats || {
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'satiety', name: 'Satiety', enabled: true },
{ id: 'energy', name: 'Energy', enabled: true },
{ id: 'hygiene', name: 'Hygiene', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
],
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
skillsSection: { enabled: false, label: 'Skills' }
};
const inventorySummary = buildInventorySummary(stats.inventory);
return `${statNames.health}: ${stats.health}%\n${statNames.satiety}: ${stats.satiety}%\n${statNames.energy}: ${stats.energy}%\n${statNames.hygiene}: ${stats.hygiene}%\n${statNames.arousal}: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\n${inventorySummary}`;
let text = '';
// Add enabled custom stats
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
for (const stat of enabledStats) {
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
text += `${stat.name}: ${value}%\n`;
}
// Add status section if enabled
if (config.statusSection.enabled) {
if (config.statusSection.showMoodEmoji) {
text += `${stats.mood}: `;
}
text += `${stats.conditions || 'None'}\n`;
}
// Add inventory summary
const inventorySummary = buildInventorySummary(stats.inventory);
text += inventorySummary;
// Add skills if enabled
if (config.skillsSection.enabled && stats.skills) {
text += `\n${config.skillsSection.label}: ${stats.skills}`;
}
return text.trim();
}
/**
@@ -49,12 +78,17 @@ export function renderUserStats() {
}
const stats = extensionSettings.userStats;
const statNames = extensionSettings.statNames || {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
const config = extensionSettings.trackerConfig?.userStats || {
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'satiety', name: 'Satiety', enabled: true },
{ id: 'energy', name: 'Energy', enabled: true },
{ id: 'hygiene', name: 'Hygiene', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
],
showRPGAttributes: true,
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
skillsSection: { enabled: false, label: 'Skills' }
};
const userName = getContext().name1;
@@ -63,12 +97,9 @@ export function renderUserStats() {
lastGeneratedData.userStats = buildUserStatsText();
}
// Get user portrait - handle both default-user and custom persona folders
// Use a base64-encoded SVG placeholder as fallback to avoid 400 errors
// Get user portrait
let userPortrait = FALLBACK_AVATAR_DATA_URI;
if (user_avatar) {
// Try to get the thumbnail using our safe helper
const thumbnailUrl = getSafeThumbnailUrl('persona', user_avatar);
if (thumbnailUrl) {
userPortrait = thumbnailUrl;
@@ -78,64 +109,71 @@ export function renderUserStats() {
// Create gradient from low to high color
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
const html = `
<div class="rpg-stats-content">
<div class="rpg-stats-left">
<div class="rpg-user-info-row">
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
<span class="rpg-user-name">${userName}</span>
<span style="opacity: 0.5;">|</span>
<span class="rpg-level-label">LVL</span>
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${extensionSettings.level}</span>
</div>
<div class="rpg-stats-grid">
<div class="rpg-stat-row">
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="health" title="Click to edit stat name">${statNames.health}:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.health}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="health" title="Click to edit">${stats.health}%</span>
</div>
<div class="rpg-stat-row">
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="satiety" title="Click to edit stat name">${statNames.satiety}:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.satiety}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="satiety" title="Click to edit">${stats.satiety}%</span>
</div>
<div class="rpg-stat-row">
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="energy" title="Click to edit stat name">${statNames.energy}:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.energy}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="energy" title="Click to edit">${stats.energy}%</span>
</div>
<div class="rpg-stat-row">
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="hygiene" title="Click to edit stat name">${statNames.hygiene}:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.hygiene}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="hygiene" title="Click to edit">${stats.hygiene}%</span>
</div>
<div class="rpg-stat-row">
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="arousal" title="Click to edit stat name">${statNames.arousal}:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.arousal}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="arousal" title="Click to edit">${stats.arousal}%</span>
</div>
</div>
<div class="rpg-mood">
<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>
<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${stats.conditions}</div>
let html = '<div class="rpg-stats-content"><div class="rpg-stats-left">';
// User info row
html += `
<div class="rpg-user-info-row">
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
<span class="rpg-user-name">${userName}</span>
<span style="opacity: 0.5;">|</span>
<span class="rpg-level-label">LVL</span>
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${extensionSettings.level}</span>
</div>
`;
// Dynamic stats grid - only show enabled stats
html += '<div class="rpg-stats-grid">';
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
for (const stat of enabledStats) {
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
html += `
<div class="rpg-stat-row">
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="${stat.id}" title="Click to edit stat name">${stat.name}:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - value}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="${stat.id}" title="Click to edit">${value}%</span>
</div>
`;
}
html += '</div>';
// Status section (conditionally rendered)
if (config.statusSection.enabled) {
html += '<div class="rpg-mood">';
if (config.statusSection.showMoodEmoji) {
html += `<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>`;
}
// Render custom status fields
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
// For now, use first field as "conditions" for backward compatibility
const conditionsValue = stats.conditions || 'None';
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${conditionsValue}</div>`;
}
html += '</div>';
}
// Skills section (conditionally rendered)
if (config.skillsSection.enabled) {
const skillsValue = stats.skills || 'None';
html += `
<div class="rpg-skills-section">
<span class="rpg-skills-label">${config.skillsSection.label}:</span>
<div class="rpg-skills-value rpg-editable" contenteditable="true" data-field="skills" title="Click to edit skills">${skillsValue}</div>
</div>
`;
}
html += '</div>'; // Close rpg-stats-left
// RPG Attributes section (conditionally rendered)
if (config.showRPGAttributes) {
html += `
<div class="rpg-stats-right">
<div class="rpg-classic-stats">
<div class="rpg-classic-stats-grid">
@@ -190,8 +228,10 @@ export function renderUserStats() {
</div>
</div>
</div>
</div>
`;
`;
}
html += '</div>'; // Close rpg-stats-content
$userStatsContainer.html(html);