merge: resolve conflicts with upstream/main

Merged upstream/main into feat/v2-widget-dashboard-system branch.

Key conflict resolutions:
- index.js: Added renderQuests() to Dashboard v2 fallback rendering
- state.js: Combined memoryMessagesToProcess with Dashboard v2 config
- apiClient.js: Combined refreshDashboard() and renderQuests() calls
- style.css: Kept Dashboard v2 mobile refresh button styles

New features from upstream:
- Quest tracking system (renderQuests, quests.js)
- Memory recollection system
- Lorebook limiter feature
- Various parser and prompt builder improvements
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-30 08:26:19 +11:00
21 changed files with 3050 additions and 483 deletions
+15 -7
View File
@@ -18,6 +18,11 @@ import { saveChatData } from '../../core/persistence.js';
import { generateSeparateUpdatePrompt } from './promptBuilder.js';
import { parseResponse, parseUserStats } from './parser.js';
import { refreshDashboard } from '../dashboard/dashboardIntegration.js';
import { renderUserStats } from '../rendering/userStats.js';
import { renderInfoBox } from '../rendering/infoBox.js';
import { renderThoughts } from '../rendering/thoughts.js';
import { renderInventory } from '../rendering/inventory.js';
import { renderQuests } from '../rendering/quests.js';
// Store the original preset name to restore after tracker generation
let originalPresetName = null;
@@ -175,15 +180,16 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
// });
// If there's no committed data yet (first time) or only has placeholder data, commit immediately
const hasNoRealData = !committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts;
const hasOnlyPlaceholderData = (
(!committedTrackerData.userStats || committedTrackerData.userStats === '') &&
(!committedTrackerData.infoBox || committedTrackerData.infoBox === 'Info Box\n---\n' || committedTrackerData.infoBox === '') &&
(!committedTrackerData.characterThoughts || committedTrackerData.characterThoughts === 'Present Characters\n---\n' || committedTrackerData.characterThoughts === '')
// Only auto-commit on TRULY first generation (no committed data exists at all)
// This prevents auto-commit after refresh when we have saved committed data
const hasAnyCommittedContent = (
(committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') ||
(committedTrackerData.infoBox && committedTrackerData.infoBox.trim() !== '' && committedTrackerData.infoBox !== 'Info Box\n---\n') ||
(committedTrackerData.characterThoughts && committedTrackerData.characterThoughts.trim() !== '' && committedTrackerData.characterThoughts !== 'Present Characters\n---\n')
);
if (hasNoRealData || hasOnlyPlaceholderData) {
// Only commit if we have NO committed content at all (truly first time ever)
if (!hasAnyCommittedContent) {
committedTrackerData.userStats = parsedData.userStats;
committedTrackerData.infoBox = parsedData.infoBox;
committedTrackerData.characterThoughts = parsedData.characterThoughts;
@@ -195,6 +201,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
// Refresh dashboard widgets (v2 dashboard)
refreshDashboard();
@@ -207,6 +214,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
// Refresh dashboard widgets (v2 dashboard)
refreshDashboard();
+124 -14
View File
@@ -7,6 +7,70 @@ import { extensionSettings, FEATURE_FLAGS, addDebugLog } from '../../core/state.
import { saveSettings } from '../../core/persistence.js';
import { extractInventory } from './inventoryParser.js';
/**
* Helper to separate emoji from text in a string
* Handles cases where there's no comma or space after emoji
* @param {string} str - String potentially containing emoji followed by text
* @returns {{emoji: string, text: string}} Separated emoji and text
*/
function separateEmojiFromText(str) {
if (!str) return { emoji: '', text: '' };
str = str.trim();
// Regex to match emoji at the start (handles most emoji including compound ones)
// This matches emoji sequences including skin tones, gender modifiers, etc.
const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
const emojiMatch = str.match(emojiRegex);
if (emojiMatch) {
const emoji = emojiMatch[0];
let text = str.substring(emoji.length).trim();
// Remove leading comma or space if present
text = text.replace(/^[,\s]+/, '');
return { emoji, text };
}
// No emoji found - check if there's a comma separator anyway
const commaParts = str.split(',');
if (commaParts.length >= 2) {
return {
emoji: commaParts[0].trim(),
text: commaParts.slice(1).join(',').trim()
};
}
// No clear separation - return original as text
return { emoji: '', text: str };
}
/**
* Helper to strip enclosing brackets from text
* Removes [], {}, and () from the entire text if it's wrapped
* @param {string} text - Text that may be wrapped in brackets
* @returns {string} Text with brackets removed
*/
function stripBrackets(text) {
if (!text) return text;
// Remove leading and trailing whitespace first
text = text.trim();
// Check if the entire text is wrapped in brackets and remove them
// This handles cases where models wrap entire sections in brackets
while (
(text.startsWith('[') && text.endsWith(']')) ||
(text.startsWith('{') && text.endsWith('}')) ||
(text.startsWith('(') && text.endsWith(')'))
) {
text = text.substring(1, text.length - 1).trim();
}
return text;
}
/**
* Helper to log to both console and debug logs array
*/
@@ -37,9 +101,15 @@ export function parseResponse(responseText) {
debugLog('[RPG Parser] Response length:', responseText.length + ' chars');
debugLog('[RPG Parser] First 500 chars:', responseText.substring(0, 500));
// Remove content inside thinking tags first (model's internal reasoning)
// This prevents parsing code blocks from the model's thinking process
let cleanedResponse = responseText.replace(/<think>[\s\S]*?<\/think>/gi, '');
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars');
// Extract code blocks
const codeBlockRegex = /```([^`]+)```/g;
const matches = [...responseText.matchAll(codeBlockRegex)];
const matches = [...cleanedResponse.matchAll(codeBlockRegex)];
debugLog('[RPG Parser] Found', matches.length + ' code blocks');
@@ -63,21 +133,21 @@ export function parseResponse(responseText) {
// Extract User Stats section
const statsMatch = content.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
if (statsMatch && !result.userStats) {
result.userStats = statsMatch[0].trim();
result.userStats = stripBrackets(statsMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Stats from combined block');
}
// Extract Info Box section
const infoBoxMatch = content.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
if (infoBoxMatch && !result.infoBox) {
result.infoBox = infoBoxMatch[0].trim();
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Info Box from combined block');
}
// Extract Present Characters section
const charactersMatch = content.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
if (charactersMatch && !result.characterThoughts) {
result.characterThoughts = charactersMatch[0].trim();
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Present Characters from combined block');
}
} else {
@@ -107,13 +177,13 @@ export function parseResponse(responseText) {
(content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭")));
if (isStats && !result.userStats) {
result.userStats = content;
result.userStats = stripBrackets(content);
debugLog('[RPG Parser] ✓ Matched: Stats section');
} else if (isInfoBox && !result.infoBox) {
result.infoBox = content;
result.infoBox = stripBrackets(content);
debugLog('[RPG Parser] ✓ Matched: Info Box section');
} else if (isCharacters && !result.characterThoughts) {
result.characterThoughts = content;
result.characterThoughts = stripBrackets(content);
debugLog('[RPG Parser] ✓ Matched: Present Characters section');
debugLog('[RPG Parser] Full content:', content);
} else {
@@ -169,18 +239,36 @@ export function parseUserStats(statsText) {
// Format 2: Status: [Emoji], [Conditions]
// Format 3: [Emoji]: [Conditions] (legacy)
// Format 4: Mood: [Emoji] - [Conditions]
// Format 5: Status: [Emoji Conditions] (no separator - FIXED)
let moodMatch = null;
// Try new format: Status: emoji, conditions
const statusMatch = statsText.match(/Status:\s*(.+?),\s*(.+)/i);
// Try new format: Status: emoji, conditions OR Status: emojiConditions
const statusMatch = statsText.match(/Status:\s*(.+)/i);
if (statusMatch) {
moodMatch = [null, statusMatch[1].trim(), statusMatch[2].trim()];
const statusContent = statusMatch[1].trim();
const { emoji, text } = separateEmojiFromText(statusContent);
if (emoji && text) {
moodMatch = [null, emoji, text];
} else if (statusContent.includes(',')) {
// Fallback to comma split if emoji detection failed
const parts = statusContent.split(',').map(p => p.trim());
moodMatch = [null, parts[0], parts.slice(1).join(', ')];
}
}
// Try alternative: Mood: emoji, conditions
else {
const moodAltMatch = statsText.match(/Mood:\s*(.+?)[,\-]\s*(.+)/i);
// Try alternative: Mood: emoji, conditions OR Mood: emojiConditions
if (!moodMatch) {
const moodAltMatch = statsText.match(/Mood:\s*(.+)/i);
if (moodAltMatch) {
moodMatch = [null, moodAltMatch[1].trim(), moodAltMatch[2].trim()];
const moodContent = moodAltMatch[1].trim();
const { emoji, text } = separateEmojiFromText(moodContent);
if (emoji && text) {
moodMatch = [null, emoji, text];
} else if (moodContent.includes(',') || moodContent.includes('-')) {
// Fallback to comma/dash split if emoji detection failed
const parts = moodContent.split(/[,\-]/).map(p => p.trim());
moodMatch = [null, parts[0], parts.slice(1).join(', ')];
}
}
}
@@ -245,6 +333,28 @@ export function parseUserStats(statsText) {
extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions
}
// Extract quests
const mainQuestMatch = statsText.match(/Main Quests?:\s*(.+)/i);
if (mainQuestMatch) {
extensionSettings.quests.main = mainQuestMatch[1].trim();
debugLog('[RPG Parser] Main quests extracted:', mainQuestMatch[1].trim());
}
const optionalQuestsMatch = statsText.match(/Optional Quests:\s*(.+)/i);
if (optionalQuestsMatch) {
const questsText = optionalQuestsMatch[1].trim();
if (questsText && questsText !== 'None') {
// Split by comma and clean up
extensionSettings.quests.optional = questsText
.split(',')
.map(q => q.trim())
.filter(q => q && q !== 'None');
} else {
extensionSettings.quests.optional = [];
}
debugLog('[RPG Parser] Optional quests extracted:', extensionSettings.quests.optional);
}
debugLog('[RPG Parser] Final userStats after parsing:', {
health: extensionSettings.userStats.health,
satiety: extensionSettings.userStats.satiety,
+58 -10
View File
@@ -104,14 +104,23 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
// Add format specifications for each enabled tracker
if (extensionSettings.showUserStats) {
// Get custom stat names with fallback defaults
const statNames = extensionSettings.statNames || {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
};
instructions += '```\n';
instructions += `${userName}'s Stats\n`;
instructions += '---\n';
instructions += '- Health: X%\n';
instructions += '- Satiety: X%\n';
instructions += '- Energy: X%\n';
instructions += '- Hygiene: X%\n';
instructions += '- Arousal: X%\n';
instructions += `- ${statNames.health}: X%\n`;
instructions += `- ${statNames.satiety}: X%\n`;
instructions += `- ${statNames.energy}: X%\n`;
instructions += `- ${statNames.hygiene}: X%\n`;
instructions += `- ${statNames.arousal}: X%\n`;
instructions += 'Status: [Mood Emoji, Conditions (up to three traits)]\n';
// Add inventory format based on feature flag
@@ -122,9 +131,13 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
instructions += 'Assets: [Vehicles, property, major possessions, or "None"]\n';
} else {
// Legacy v1 format
instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items/none)]\n';
instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items, or "None")]\\n';
}
// Add quests section
instructions += 'Main Quests: [Short title of the currently active main quest (for example, "Save the world"), or "None"]\n';
instructions += 'Optional Quests: [Short titles of the currently active optional quests (for example, "Find Zandik\'s book"), or "None"]\n';
instructions += '```\n\n';
}
@@ -137,6 +150,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
instructions += 'Temperature: [Temperature in °C]\n';
instructions += 'Time: [Time Start → Time End]\n';
instructions += 'Location: [Location]\n';
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';
}
@@ -208,9 +222,23 @@ export function generateContextualSummary() {
if (stats.inventory) {
const inventorySummary = buildInventorySummary(stats.inventory);
if (inventorySummary && inventorySummary !== 'None') {
summary += `Inventory:\n${inventorySummary}\n`;
summary += `${inventorySummary}\n`;
}
}
// Add quests summary
if (extensionSettings.quests) {
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
summary += `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) {
summary += `Optional Quests: ${optionalQuests}\n`;
}
}
}
// Include classic stats (attributes) and dice roll only if there was a dice roll
if (extensionSettings.lastDiceRoll) {
const classicStats = extensionSettings.classicStats;
@@ -224,7 +252,7 @@ export function generateContextualSummary() {
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
// Parse info box data - support both new and legacy formats
const lines = committedTrackerData.infoBox.split('\n');
let date = '', weather = '', temp = '', time = '', location = '';
let date = '', weather = '', temp = '', time = '', location = '', recentEvents = '';
// console.log('[RPG Companion] 🔍 Parsing Info Box lines:', lines);
@@ -242,6 +270,8 @@ export function generateContextualSummary() {
time = line.replace('Time:', '').trim();
} else if (line.startsWith('Location:')) {
location = line.replace('Location:', '').trim();
} else if (line.startsWith('Recent Events:')) {
recentEvents = line.replace('Recent Events:', '').trim();
}
// Legacy format with emojis (for backward compatibility)
else if (line.includes('🗓️:')) {
@@ -264,7 +294,7 @@ export function generateContextualSummary() {
// console.log('[RPG Companion] 🔍 Parsed values - date:', date, 'weather:', weather, 'temp:', temp, 'time:', time, 'location:', location);
if (date || weather || temp || time || location) {
if (date || weather || temp || time || location || recentEvents) {
summary += `Information:\n`;
summary += `Scene: `;
if (date) summary += `${date}`;
@@ -272,7 +302,9 @@ export function generateContextualSummary() {
if (time) summary += ` | ${time}`;
if (weather) summary += ` | ${weather}`;
if (temp) summary += ` | ${temp}`;
summary += `\n\n`;
summary += `\n`;
if (recentEvents) summary += `Recent Events: ${recentEvents}\n`;
summary += `\n`;
}
}
@@ -317,6 +349,22 @@ export function generateRPGPromptText() {
} else {
promptText += `Last ${userName}'s Stats:\nNone - this is the first update.\n\n`;
}
// Add current quests to the previous data context
if (extensionSettings.quests) {
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
promptText += `Main Quests: ${extensionSettings.quests.main}\n`;
} else {
promptText += `Main Quests: None\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`;
} else {
promptText += `Optional Quests: None\n`;
}
promptText += `\n`;
}
}
if (extensionSettings.showInfoBox) {