chore: final cleanup

This commit is contained in:
Subarashimo
2025-12-05 18:10:21 +01:00
parent 38328de1bf
commit 7e47dbfd7c
29 changed files with 338 additions and 2168 deletions
+26 -150
View File
@@ -1,36 +1,11 @@
import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js'; import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js';
import { eventSource, event_types, substituteParams, chat, generateRaw, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js'; import { event_types, saveSettingsDebounced, getRequestHeaders } from '../../../../script.js';
import { selected_group, getGroupMembers } from '../../../group-chats.js';
import { power_user } from '../../../power-user.js';
// Core modules // Core modules
import { extensionName, extensionFolderPath } from './src/core/config.js'; import { extensionName, extensionFolderPath } from './src/core/config.js';
import { i18n } from './src/core/i18n.js'; import { i18n } from './src/core/i18n.js';
import { import {
extensionSettings, extensionSettings,
lastGeneratedData,
committedTrackerData,
lastActionWasSwipe,
isGenerating,
isPlotProgression,
pendingDiceRoll,
FALLBACK_AVATAR_DATA_URI,
$panelContainer,
$userStatsContainer,
$infoBoxContainer,
$thoughtsContainer,
$inventoryContainer,
$questsContainer,
setExtensionSettings,
updateExtensionSettings,
setLastGeneratedData,
updateLastGeneratedData,
setCommittedTrackerData,
updateCommittedTrackerData,
setLastActionWasSwipe,
setIsGenerating,
setIsPlotProgression,
setPendingDiceRoll,
setPanelContainer, setPanelContainer,
setUserStatsContainer, setUserStatsContainer,
setInfoBoxContainer, setInfoBoxContainer,
@@ -39,28 +14,19 @@ import {
setInventoryContainer, setInventoryContainer,
setQuestsContainer setQuestsContainer
} from './src/core/state.js'; } from './src/core/state.js';
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js'; import { loadSettings, saveSettings, loadChatData } from './src/core/persistence.js';
import { registerAllEvents } from './src/core/events.js'; import { registerAllEvents } from './src/core/events.js';
// Generation & Parsing modules // Generation & Parsing modules
import {
generateContextualSummary,
generateRPGPromptText,
generateSeparateUpdatePrompt
} from './src/systems/generation/promptBuilder.js';
import { parseResponse, parseUserStats, parseSkills } from './src/systems/generation/parser.js';
import { updateRPGData } from './src/systems/generation/apiClient.js'; import { updateRPGData } from './src/systems/generation/apiClient.js';
import { onGenerationStarted } from './src/systems/generation/injector.js'; import { onGenerationStarted } from './src/systems/generation/injector.js';
// Rendering modules // Rendering modules
import { getSafeThumbnailUrl } from './src/utils/avatars.js';
import { renderUserStats } from './src/systems/rendering/userStats.js'; import { renderUserStats } from './src/systems/rendering/userStats.js';
import { renderInfoBox, updateInfoBoxField } from './src/systems/rendering/infoBox.js'; import { renderInfoBox } from './src/systems/rendering/infoBox.js';
import { import {
renderThoughts, renderThoughts,
updateCharacterField, updateChatThoughts
updateChatThoughts,
createThoughtPanel
} from './src/systems/rendering/thoughts.js'; } from './src/systems/rendering/thoughts.js';
import { renderInventory } from './src/systems/rendering/inventory.js'; import { renderInventory } from './src/systems/rendering/inventory.js';
import { renderQuests } from './src/systems/rendering/quests.js'; import { renderQuests } from './src/systems/rendering/quests.js';
@@ -75,12 +41,9 @@ import {
applyCustomTheme, applyCustomTheme,
toggleCustomColors, toggleCustomColors,
toggleAnimations, toggleAnimations,
updateSettingsPopupTheme, updateSettingsPopupTheme
applyCustomThemeToSettingsPopup
} from './src/systems/ui/theme.js'; } from './src/systems/ui/theme.js';
import { import {
DiceModal,
SettingsModal,
setupDiceRoller, setupDiceRoller,
setupSettingsPopup, setupSettingsPopup,
updateDiceDisplay, updateDiceDisplay,
@@ -92,7 +55,6 @@ import {
} from './src/systems/ui/trackerEditor.js'; } from './src/systems/ui/trackerEditor.js';
import { import {
togglePlotButtons, togglePlotButtons,
updateCollapseToggleIcon,
setupCollapseToggle, setupCollapseToggle,
updatePanelVisibility, updatePanelVisibility,
updateSectionVisibility, updateSectionVisibility,
@@ -101,9 +63,6 @@ import {
} from './src/systems/ui/layout.js'; } from './src/systems/ui/layout.js';
import { import {
setupMobileToggle, setupMobileToggle,
constrainFabToViewport,
setupMobileTabs,
removeMobileTabs,
setupMobileKeyboardHandling, setupMobileKeyboardHandling,
setupContentEditableScrolling, setupContentEditableScrolling,
updateMobileTabLabels updateMobileTabLabels
@@ -123,7 +82,6 @@ import { DEFAULT_HTML_PROMPT, DEFAULT_JSON_TRACKER_PROMPT, DEFAULT_MESSAGE_INTER
// Integration modules // Integration modules
import { import {
commitTrackerData,
onMessageSent, onMessageSent,
onMessageReceived, onMessageReceived,
onCharacterChanged, onCharacterChanged,
@@ -132,27 +90,6 @@ import {
clearExtensionPrompts clearExtensionPrompts
} from './src/systems/integration/sillytavern.js'; } from './src/systems/integration/sillytavern.js';
// Old state variable declarations removed - now imported from core modules
// (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js)
// Utility functions removed - now imported from src/utils/avatars.js
// (getSafeThumbnailUrl)
// Persistence functions removed - now imported from src/core/persistence.js
// (loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData)
// Theme functions removed - now imported from src/systems/ui/theme.js
// (applyTheme, applyCustomTheme, toggleCustomColors, toggleAnimations,
// updateSettingsPopupTheme, applyCustomThemeToSettingsPopup)
// Layout functions removed - now imported from src/systems/ui/layout.js
// (togglePlotButtons, updateCollapseToggleIcon, setupCollapseToggle,
// updatePanelVisibility, updateSectionVisibility, applyPanelPosition)
// Note: closeMobilePanelWithAnimation is only used internally by mobile.js
// Mobile UI functions removed - now imported from src/systems/ui/mobile.js
// (setupMobileToggle, constrainFabToViewport, setupMobileTabs, removeMobileTabs,
// setupMobileKeyboardHandling, setupContentEditableScrolling)
/** /**
* Updates UI elements that are dynamically generated and not covered by data-i18n-key. * Updates UI elements that are dynamically generated and not covered by data-i18n-key.
@@ -351,20 +288,17 @@ async function initUI() {
setInventoryContainer($('#rpg-inventory')); setInventoryContainer($('#rpg-inventory'));
setQuestsContainer($('#rpg-quests')); setQuestsContainer($('#rpg-quests'));
// Re-apply translations to the entire body to catch all new elements from the template i18n.applyTranslations(document.body);
i18n.applyTranslations(document.body);
// Set up event listeners (enable/disable is handled in Extensions tab) $('#rpg-toggle-auto-update').on('change', function() {
$('#rpg-toggle-auto-update').on('change', function() {
extensionSettings.autoUpdate = $(this).prop('checked'); extensionSettings.autoUpdate = $(this).prop('checked');
saveSettings(); saveSettings();
}); });
$('#rpg-position-select').on('change', function() { $('#rpg-position-select').on('change', function() {
extensionSettings.panelPosition = String($(this).val()); extensionSettings.panelPosition = String($(this).val());
saveSettings(); saveSettings();
applyPanelPosition(); applyPanelPosition();
// Recreate thought bubbles to update their position
updateChatThoughts(); updateChatThoughts();
}); });
@@ -417,11 +351,10 @@ async function initUI() {
updateSectionVisibility(); updateSectionVisibility();
}); });
$('#rpg-toggle-inventory').on('change', function() { $('#rpg-toggle-inventory').on('change', function() {
extensionSettings.showInventory = $(this).prop('checked'); extensionSettings.showInventory = $(this).prop('checked');
saveSettings(); saveSettings();
updateSectionVisibility(); updateSectionVisibility();
// Re-setup desktop tabs to show/hide inventory tab
if (window.innerWidth > 1000) { if (window.innerWidth > 1000) {
removeDesktopTabs(); removeDesktopTabs();
setupDesktopTabs(); setupDesktopTabs();
@@ -431,15 +364,14 @@ async function initUI() {
$('#rpg-toggle-simplified-inventory').on('change', function() { $('#rpg-toggle-simplified-inventory').on('change', function() {
extensionSettings.useSimplifiedInventory = $(this).prop('checked'); extensionSettings.useSimplifiedInventory = $(this).prop('checked');
saveSettings(); saveSettings();
renderInventory(); // Re-render inventory with new mode renderInventory();
}); });
$('#rpg-toggle-quests').on('change', function() { $('#rpg-toggle-quests').on('change', function() {
extensionSettings.showQuests = $(this).prop('checked'); extensionSettings.showQuests = $(this).prop('checked');
saveSettings(); saveSettings();
updateSectionVisibility(); updateSectionVisibility();
renderQuests(); // Re-render quests renderQuests();
// Re-setup desktop tabs to show/hide quests tab
if (window.innerWidth > 1000) { if (window.innerWidth > 1000) {
removeDesktopTabs(); removeDesktopTabs();
setupDesktopTabs(); setupDesktopTabs();
@@ -450,8 +382,7 @@ async function initUI() {
extensionSettings.showSkills = $(this).prop('checked'); extensionSettings.showSkills = $(this).prop('checked');
saveSettings(); saveSettings();
updateSectionVisibility(); updateSectionVisibility();
renderSkills(); // Render skills section renderSkills();
// Re-setup desktop tabs to show/hide skills tab
if (window.innerWidth > 1000) { if (window.innerWidth > 1000) {
removeDesktopTabs(); removeDesktopTabs();
setupDesktopTabs(); setupDesktopTabs();
@@ -461,7 +392,6 @@ async function initUI() {
$('#rpg-toggle-item-skill-links').on('change', function() { $('#rpg-toggle-item-skill-links').on('change', function() {
extensionSettings.enableItemSkillLinks = $(this).prop('checked'); extensionSettings.enableItemSkillLinks = $(this).prop('checked');
saveSettings(); saveSettings();
// Re-render skills to show/hide link badges
renderSkills(); renderSkills();
}); });
@@ -472,7 +402,6 @@ async function initUI() {
$('#rpg-toggle-thoughts-in-chat').on('change', function() { $('#rpg-toggle-thoughts-in-chat').on('change', function() {
extensionSettings.showThoughtsInChat = $(this).prop('checked'); extensionSettings.showThoughtsInChat = $(this).prop('checked');
// console.log('[RPG Companion] Toggle showThoughtsInChat changed to:', extensionSettings.showThoughtsInChat);
saveSettings(); saveSettings();
updateChatThoughts(); updateChatThoughts();
}); });
@@ -480,17 +409,14 @@ async function initUI() {
$('#rpg-toggle-always-show-bubble').on('change', function() { $('#rpg-toggle-always-show-bubble').on('change', function() {
extensionSettings.alwaysShowThoughtBubble = $(this).prop('checked'); extensionSettings.alwaysShowThoughtBubble = $(this).prop('checked');
saveSettings(); saveSettings();
// Force immediate save to ensure setting is persisted before any other code runs
const context = getContext(); const context = getContext();
const extension_settings = context.extension_settings || context.extensionSettings; const extension_settings = context.extension_settings || context.extensionSettings;
extension_settings[extensionName] = extensionSettings; extension_settings[extensionName] = extensionSettings;
// Re-render thoughts to apply the setting
updateChatThoughts(); updateChatThoughts();
}); });
$('#rpg-toggle-html-prompt').on('change', function() { $('#rpg-toggle-html-prompt').on('change', function() {
extensionSettings.enableHtmlPrompt = $(this).prop('checked'); extensionSettings.enableHtmlPrompt = $(this).prop('checked');
// console.log('[RPG Companion] Toggle enableHtmlPrompt changed to:', extensionSettings.enableHtmlPrompt);
saveSettings(); saveSettings();
}); });
@@ -544,7 +470,6 @@ async function initUI() {
$('#rpg-toggle-plot-buttons').on('change', function() { $('#rpg-toggle-plot-buttons').on('change', function() {
extensionSettings.enablePlotButtons = $(this).prop('checked'); extensionSettings.enablePlotButtons = $(this).prop('checked');
// console.log('[RPG Companion] Toggle enablePlotButtons changed to:', extensionSettings.enablePlotButtons);
saveSettings(); saveSettings();
togglePlotButtons(); togglePlotButtons();
}); });
@@ -557,7 +482,6 @@ async function initUI() {
$('#rpg-manual-update').on('click', async function() { $('#rpg-manual-update').on('click', async function() {
if (!extensionSettings.enabled) { if (!extensionSettings.enabled) {
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
return; return;
} }
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
@@ -566,23 +490,22 @@ async function initUI() {
$('#rpg-stat-bar-color-low').on('change', function() { $('#rpg-stat-bar-color-low').on('change', function() {
extensionSettings.statBarColorLow = String($(this).val()); extensionSettings.statBarColorLow = String($(this).val());
saveSettings(); saveSettings();
renderUserStats(); // Re-render with new colors renderUserStats();
}); });
$('#rpg-stat-bar-color-high').on('change', function() { $('#rpg-stat-bar-color-high').on('change', function() {
extensionSettings.statBarColorHigh = String($(this).val()); extensionSettings.statBarColorHigh = String($(this).val());
saveSettings(); saveSettings();
renderUserStats(); // Re-render with new colors renderUserStats();
}); });
// Theme selection
$('#rpg-theme-select').on('change', function() { $('#rpg-theme-select').on('change', function() {
extensionSettings.theme = String($(this).val()); extensionSettings.theme = String($(this).val());
saveSettings(); saveSettings();
applyTheme(); applyTheme();
toggleCustomColors(); toggleCustomColors();
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts(); // Recreate thought bubbles with new theme updateChatThoughts();
}); });
// Custom color pickers // Custom color pickers
@@ -591,8 +514,8 @@ async function initUI() {
saveSettings(); saveSettings();
if (extensionSettings.theme === 'custom') { if (extensionSettings.theme === 'custom') {
applyCustomTheme(); applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts(); // Update thought bubbles updateChatThoughts();
} }
}); });
@@ -601,8 +524,8 @@ async function initUI() {
saveSettings(); saveSettings();
if (extensionSettings.theme === 'custom') { if (extensionSettings.theme === 'custom') {
applyCustomTheme(); applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts(); // Update thought bubbles updateChatThoughts();
} }
}); });
@@ -611,8 +534,8 @@ async function initUI() {
saveSettings(); saveSettings();
if (extensionSettings.theme === 'custom') { if (extensionSettings.theme === 'custom') {
applyCustomTheme(); applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts(); // Update thought bubbles updateChatThoughts();
} }
}); });
@@ -621,12 +544,11 @@ async function initUI() {
saveSettings(); saveSettings();
if (extensionSettings.theme === 'custom') { if (extensionSettings.theme === 'custom') {
applyCustomTheme(); applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts(); // Update thought bubbles updateChatThoughts();
} }
}); });
// Initialize UI state (enable/disable is in Extensions tab)
$('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate); $('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate);
$('#rpg-position-select').val(extensionSettings.panelPosition); $('#rpg-position-select').val(extensionSettings.panelPosition);
$('#rpg-update-depth').val(extensionSettings.updateDepth); $('#rpg-update-depth').val(extensionSettings.updateDepth);
@@ -647,18 +569,11 @@ async function initUI() {
$('#rpg-toggle-message-interception').prop('checked', extensionSettings.enableMessageInterception); $('#rpg-toggle-message-interception').prop('checked', extensionSettings.enableMessageInterception);
updateInterceptionToggleVisibility(); updateInterceptionToggleVisibility();
// Set default HTML prompt as actual text if no custom prompt exists
$('#rpg-custom-html-prompt').val(extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT); $('#rpg-custom-html-prompt').val(extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT);
// Set default tracker prompt as actual text if no custom prompt exists
$('#rpg-custom-tracker-prompt').val(extensionSettings.customTrackerPrompt || DEFAULT_JSON_TRACKER_PROMPT); $('#rpg-custom-tracker-prompt').val(extensionSettings.customTrackerPrompt || DEFAULT_JSON_TRACKER_PROMPT);
// Set default message interception prompt as actual text if no custom prompt exists
$('#rpg-custom-message-interception-prompt').val( $('#rpg-custom-message-interception-prompt').val(
extensionSettings.customMessageInterceptionPrompt || DEFAULT_MESSAGE_INTERCEPTION_PROMPT extensionSettings.customMessageInterceptionPrompt || DEFAULT_MESSAGE_INTERCEPTION_PROMPT
); );
// Message interception depth
$('#rpg-message-interception-depth').val( $('#rpg-message-interception-depth').val(
extensionSettings.messageInterceptionContextDepth || extensionSettings.updateDepth || 4 extensionSettings.messageInterceptionContextDepth || extensionSettings.updateDepth || 4
); );
@@ -683,18 +598,14 @@ async function initUI() {
toggleCustomColors(); toggleCustomColors();
toggleAnimations(); toggleAnimations();
// Setup mobile toggle button
setupMobileToggle(); setupMobileToggle();
// Setup desktop tabs (only on desktop viewport)
if (window.innerWidth > 1000) { if (window.innerWidth > 1000) {
setupDesktopTabs(); setupDesktopTabs();
} }
// Setup collapse/expand toggle button
setupCollapseToggle(); setupCollapseToggle();
// Render initial data if available
renderUserStats(); renderUserStats();
renderInfoBox(); renderInfoBox();
renderThoughts(); renderThoughts();
@@ -712,11 +623,7 @@ async function initUI() {
setupMobileKeyboardHandling(); setupMobileKeyboardHandling();
setupContentEditableScrolling(); setupContentEditableScrolling();
initInventoryEventListeners(); initInventoryEventListeners();
// Setup Memory Recollection button in World Info
setupMemoryRecollectionButton(); setupMemoryRecollectionButton();
// Initialize Lorebook Limiter
initLorebookLimiter(); initLorebookLimiter();
} }
@@ -724,13 +631,6 @@ async function initUI() {
// Rendering functions removed - now imported from src/systems/rendering/*
// (renderUserStats, renderInfoBox, renderThoughts, updateInfoBoxField,
// updateCharacterField, updateChatThoughts, createThoughtPanel)
// Event handlers removed - now imported from src/systems/integration/sillytavern.js
// (commitTrackerData, onMessageSent, onMessageReceived, onCharacterChanged,
// onMessageSwiped, updatePersonaAvatar, clearExtensionPrompts)
/** /**
* Ensures the "RPG Companion Trackers" preset exists in the user's OpenAI Settings. * Ensures the "RPG Companion Trackers" preset exists in the user's OpenAI Settings.
@@ -803,79 +703,57 @@ jQuery(async () => {
try { try {
console.log('[RPG Companion] Starting initialization...'); console.log('[RPG Companion] Starting initialization...');
// Load settings with validation
try { try {
loadSettings(); loadSettings();
} catch (error) { } catch (error) {
console.error('[RPG Companion] Settings load failed, continuing with defaults:', error); console.error('[RPG Companion] Settings load failed, continuing with defaults:', error);
} }
// Initialize i18n early for the settings panel
await i18n.init(); await i18n.init();
// Set up a central listener for language changes to update dynamic UI parts
i18n.addEventListener('languageChanged', updateDynamicLabels); i18n.addEventListener('languageChanged', updateDynamicLabels);
// Add extension settings to Extensions tab
try { try {
await addExtensionSettings(); await addExtensionSettings();
} catch (error) { } catch (error) {
console.error('[RPG Companion] Failed to add extension settings tab:', error); console.error('[RPG Companion] Failed to add extension settings tab:', error);
// Don't throw - extension can still work without settings tab
} }
// Initialize UI
try { try {
await initUI(); await initUI();
} catch (error) { } catch (error) {
console.error('[RPG Companion] UI initialization failed:', error); console.error('[RPG Companion] UI initialization failed:', error);
throw error; // This is critical - can't continue without UI throw error;
} }
// Load chat-specific data for current chat
try { try {
loadChatData(); loadChatData();
} catch (error) { } catch (error) {
console.error('[RPG Companion] Chat data load failed, using defaults:', error); console.error('[RPG Companion] Chat data load failed, using defaults:', error);
} }
// Import the HTML cleaning regex if needed
try { try {
await ensureHtmlCleaningRegex(st_extension_settings, saveSettingsDebounced); await ensureHtmlCleaningRegex(st_extension_settings, saveSettingsDebounced);
} catch (error) { } catch (error) {
console.error('[RPG Companion] HTML regex import failed:', error); console.error('[RPG Companion] HTML regex import failed:', error);
// Non-critical - continue without it
} }
// Import the RPG Companion Trackers preset if needed
try { try {
await ensureTrackerPresetExists(); await ensureTrackerPresetExists();
} catch (error) { } catch (error) {
console.error('[RPG Companion] Preset import failed:', error); console.error('[RPG Companion] Preset import failed:', error);
// Non-critical - users can manually import if needed
} }
// Detect conflicting regex scripts from old manual formatters
try { try {
const conflicts = detectConflictingRegexScripts(st_extension_settings); const conflicts = detectConflictingRegexScripts(st_extension_settings);
if (conflicts.length > 0) { if (conflicts.length > 0) {
console.log('[RPG Companion] ⚠️ Detected old manual formatting regex scripts that may conflict:'); console.log('[RPG Companion] ⚠️ Detected old manual formatting regex scripts that may conflict:');
conflicts.forEach(name => console.log(` - ${name}`)); conflicts.forEach(name => console.log(` - ${name}`));
console.log('[RPG Companion] Consider disabling these regexes as the extension now handles formatting automatically.'); console.log('[RPG Companion] Consider disabling these regexes as the extension now handles formatting automatically.');
// Show user-friendly warning (non-blocking)
// toastr.warning(
// `Found ${conflicts.length} old RPG formatting regex script(s). These may conflict with the extension. Check console for details.`,
// 'RPG Companion Warning',
// { timeOut: 8000 }
// );
} }
} catch (error) { } catch (error) {
console.error('[RPG Companion] Conflict detection failed:', error); console.error('[RPG Companion] Conflict detection failed:', error);
// Non-critical - continue anyway
} }
// Register all event listeners
try { try {
registerAllEvents({ registerAllEvents({
[event_types.MESSAGE_SENT]: onMessageSent, [event_types.MESSAGE_SENT]: onMessageSent,
@@ -888,15 +766,13 @@ jQuery(async () => {
}); });
} catch (error) { } catch (error) {
console.error('[RPG Companion] Event registration failed:', error); console.error('[RPG Companion] Event registration failed:', error);
throw error; // This is critical - can't continue without events throw error;
} }
console.log('[RPG Companion] ✅ Extension loaded successfully'); console.log('[RPG Companion] ✅ Extension loaded successfully');
} catch (error) { } catch (error) {
console.error('[RPG Companion] ❌ Critical initialization failure:', error); console.error('[RPG Companion] ❌ Critical initialization failure:', error);
console.error('[RPG Companion] Error details:', error.message, error.stack); console.error('[RPG Companion] Error details:', error.message, error.stack);
// Show user-friendly error message
toastr.error( toastr.error(
'RPG Companion failed to initialize. Check console for details. Please try refreshing the page or resetting extension settings.', 'RPG Companion failed to initialize. Check console for details. Please try refreshing the page or resetting extension settings.',
'RPG Companion Error', 'RPG Companion Error',
+2 -1
View File
@@ -1,5 +1,6 @@
//- No-op in case this is running outside of SillyTavern //- No-op in case this is running outside of SillyTavern
const { extension_settings } = typeof self.SillyTavern !== 'undefined' ? self.SillyTavern.getContext() : { extension_settings: {} }; // eslint-disable-next-line no-unused-vars
const { extension_settings: _extension_settings } = typeof self.SillyTavern !== 'undefined' ? self.SillyTavern.getContext() : { extension_settings: {} };
class Internationalization { class Internationalization {
constructor() { constructor() {
+76 -23
View File
@@ -9,11 +9,9 @@ import {
extensionSettings, extensionSettings,
lastGeneratedData, lastGeneratedData,
committedTrackerData, committedTrackerData,
setExtensionSettings,
updateExtensionSettings, updateExtensionSettings,
setLastGeneratedData, setLastGeneratedData,
setCommittedTrackerData, setCommittedTrackerData
FEATURE_FLAGS
} from './state.js'; } from './state.js';
import { migrateInventory } from '../utils/migration.js'; import { migrateInventory } from '../utils/migration.js';
import { validateStoredInventory, cleanItemString } from '../utils/security.js'; import { validateStoredInventory, cleanItemString } from '../utils/security.js';
@@ -78,19 +76,14 @@ export function loadSettings() {
} }
updateExtensionSettings(savedSettings); updateExtensionSettings(savedSettings);
// console.log('[RPG Companion] Settings loaded:', extensionSettings);
} else {
// console.log('[RPG Companion] No saved settings found, using defaults');
} }
// Migrate inventory if feature flag enabled // Migrate inventory from v1 (string) to v2 (object) format if needed
if (FEATURE_FLAGS.useNewInventory) { const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
const migrationResult = migrateInventory(extensionSettings.userStats.inventory); if (migrationResult.migrated) {
if (migrationResult.migrated) { console.log(`[RPG Companion] Inventory migrated from ${migrationResult.source} to v2 format`);
console.log(`[RPG Companion] Inventory migrated from ${migrationResult.source} to v2 format`); extensionSettings.userStats.inventory = migrationResult.inventory;
extensionSettings.userStats.inventory = migrationResult.inventory; saveSettings(); // Persist migrated inventory
saveSettings(); // Persist migrated inventory
}
} }
// Migrate to trackerConfig if it doesn't exist // Migrate to trackerConfig if it doesn't exist
@@ -104,6 +97,11 @@ export function loadSettings() {
if (migrateStatsAndSkillsFormat()) { if (migrateStatsAndSkillsFormat()) {
saveSettings(); // Persist migration saveSettings(); // Persist migration
} }
// Migrate quests from legacy format to structured format
if (migrateQuestsFormat()) {
saveSettings(); // Persist migration
}
} catch (error) { } catch (error) {
console.error('[RPG Companion] Error loading settings:', error); console.error('[RPG Companion] Error loading settings:', error);
console.error('[RPG Companion] Error details:', error.message, error.stack); console.error('[RPG Companion] Error details:', error.message, error.stack);
@@ -186,8 +184,6 @@ export function updateMessageSwipeData() {
infoBox: lastGeneratedData.infoBox, infoBox: lastGeneratedData.infoBox,
characterThoughts: lastGeneratedData.characterThoughts characterThoughts: lastGeneratedData.characterThoughts
}; };
// console.log('[RPG Companion] Updated message swipe data after user edit');
break; break;
} }
} }
@@ -323,8 +319,13 @@ export function loadChatData() {
extensionSettings.questsV2 = savedData.questsV2; extensionSettings.questsV2 = savedData.questsV2;
} }
// Migrate inventory in chat data if feature flag enabled // Migrate quests from legacy format to structured format if needed
if (FEATURE_FLAGS.useNewInventory && extensionSettings.userStats.inventory) { if (migrateQuestsFormat()) {
saveChatData(); // Persist migrated quests to chat metadata
}
// Migrate inventory from v1 (string) to v2 (object) format if needed
if (extensionSettings.userStats.inventory) {
const migrationResult = migrateInventory(extensionSettings.userStats.inventory); const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
if (migrationResult.migrated) { if (migrationResult.migrated) {
console.log(`[RPG Companion] Chat inventory migrated from ${migrationResult.source} to v2 format`); console.log(`[RPG Companion] Chat inventory migrated from ${migrationResult.source} to v2 format`);
@@ -333,16 +334,12 @@ export function loadChatData() {
} }
} }
// Validate inventory structure (Bug #3 fix)
validateInventoryStructure(extensionSettings.userStats.inventory, 'chat'); validateInventoryStructure(extensionSettings.userStats.inventory, 'chat');
// console.log('[RPG Companion] Loaded chat data:', savedData);
} }
/** /**
* Validates and repairs inventory structure to prevent corruption. * Validates and repairs inventory structure to prevent corruption.
* Ensures all v2 fields exist and are the correct type. * Ensures all v2 fields exist and are the correct type.
* Fixes Bug #3: Location disappears when switching tabs
* *
* @param {Object} inventory - Inventory object to validate * @param {Object} inventory - Inventory object to validate
* @param {string} source - Source of load ('settings' or 'chat') for logging * @param {string} source - Source of load ('settings' or 'chat') for logging
@@ -385,7 +382,6 @@ function validateInventoryStructure(inventory, source) {
} }
} }
// Validate stored field (CRITICAL for Bug #3)
if (!inventory.stored || typeof inventory.stored !== 'object' || Array.isArray(inventory.stored)) { if (!inventory.stored || typeof inventory.stored !== 'object' || Array.isArray(inventory.stored)) {
console.error(`[RPG Companion] Corrupted stored inventory from ${source}, resetting to empty object`); console.error(`[RPG Companion] Corrupted stored inventory from ${source}, resetting to empty object`);
inventory.stored = {}; inventory.stored = {};
@@ -690,3 +686,60 @@ function migrateStatsAndSkillsFormat() {
return migrated; return migrated;
} }
/**
* Migrates quests from legacy format to structured format (questsV2).
* Legacy format: quests.main (string), quests.optional (string array)
* New format: questsV2.main ({name, description}), questsV2.optional (array of {name, description})
* @returns {boolean} true if any migration was performed
*/
function migrateQuestsFormat() {
let migrated = false;
// Initialize questsV2 if it doesn't exist
if (!extensionSettings.questsV2) {
extensionSettings.questsV2 = {
main: null,
optional: []
};
}
// Migrate main quest if it exists in legacy format but not in new format
// Check if legacy format has data AND new format is empty/null
if (extensionSettings.quests?.main &&
extensionSettings.quests.main !== 'None' &&
extensionSettings.quests.main !== '' &&
(!extensionSettings.questsV2.main || !extensionSettings.questsV2.main.name)) {
extensionSettings.questsV2.main = {
name: extensionSettings.quests.main,
description: extensionSettings.quests?.mainDescription || ''
};
migrated = true;
console.log('[RPG Companion] Migrated main quest to structured format:', extensionSettings.quests.main);
}
// Migrate optional quests if they exist in legacy format but not in new format
// Check if legacy format has data AND new format is empty
if (extensionSettings.quests?.optional &&
Array.isArray(extensionSettings.quests.optional) &&
extensionSettings.quests.optional.length > 0 &&
(!extensionSettings.questsV2.optional || extensionSettings.questsV2.optional.length === 0)) {
const descriptions = extensionSettings.quests?.optionalDescriptions || [];
extensionSettings.questsV2.optional = extensionSettings.quests.optional
.filter(title => title && title !== 'None' && title !== '')
.map((title, i) => ({
name: title,
description: descriptions[i] || ''
}));
if (extensionSettings.questsV2.optional.length > 0) {
migrated = true;
console.log('[RPG Companion] Migrated optional quests to structured format:', extensionSettings.questsV2.optional.length, 'quests');
}
}
if (migrated) {
console.log('[RPG Companion] Quests format migration complete');
}
return migrated;
}
-14
View File
@@ -103,13 +103,6 @@ export let extensionSettings = {
itemSkillLinks: {}, itemSkillLinks: {},
skillAbilityLinks: {}, skillAbilityLinks: {},
skillsData: {}, skillsData: {},
statNames: {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
},
// Tracker customization configuration // Tracker customization configuration
trackerConfig: { trackerConfig: {
userStats: { userStats: {
@@ -278,13 +271,6 @@ export function addDebugLog(message, data = null) {
} }
} }
/**
* Feature flags for gradual rollout of new features
*/
export const FEATURE_FLAGS = {
useNewInventory: true // Enable v2 inventory system with categorized storage
};
/** /**
* Fallback avatar image (base64-encoded SVG with "?" icon) * Fallback avatar image (base64-encoded SVG with "?" icon)
* Using base64 to avoid quote-encoding issues in HTML attributes * Using base64 to avoid quote-encoding issues in HTML attributes
-1
View File
@@ -5,7 +5,6 @@
import { import {
extensionSettings, extensionSettings,
pendingDiceRoll,
setPendingDiceRoll setPendingDiceRoll
} from '../../core/state.js'; } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js'; import { saveSettings } from '../../core/persistence.js';
+1 -7
View File
@@ -3,11 +3,10 @@
* Adds maximum activation limit to SillyTavern's World Info system * Adds maximum activation limit to SillyTavern's World Info system
*/ */
import { eventSource, event_types } from '../../../../../../../script.js'; import { eventSource } from '../../../../../../../script.js';
let maxActivations = 0; // 0 = unlimited let maxActivations = 0; // 0 = unlimited
let settingsInitialized = false; let settingsInitialized = false;
let activatedEntriesThisGeneration = [];
/** /**
* Initialize the lorebook limiter * Initialize the lorebook limiter
@@ -136,11 +135,6 @@ function injectMaxActivationsUI() {
function patchWorldInfoActivation() { function patchWorldInfoActivation() {
console.log('[Lorebook Limiter] Setting up activation limiter...'); console.log('[Lorebook Limiter] Setting up activation limiter...');
// We need to intercept at the module level
// Use a Proxy on the module loader
const originalDefine = window.define;
const originalRequire = window.require;
// Try multiple approaches to hook into the WI system // Try multiple approaches to hook into the WI system
const attemptPatch = () => { const attemptPatch = () => {
// Approach 1: Direct window access // Approach 1: Direct window access
+2 -66
View File
@@ -3,11 +3,9 @@
* Handles generation of lorebook entries from chat history * Handles generation of lorebook entries from chat history
*/ */
import { chat, characters, this_chid, generateRaw, substituteParams, eventSource, event_types } from '../../../../../../../script.js'; import { chat, generateRaw, eventSource, event_types } from '../../../../../../../script.js';
import { selected_group } from '../../../../../../group-chats.js';
import { extensionSettings, addDebugLog } from '../../core/state.js'; import { extensionSettings, addDebugLog } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js'; import { checkWorldInfo, createNewWorldInfo, openWorldInfoEditor, saveWorldInfo } from '../../../../../../world-info.js';
import { checkWorldInfo, createNewWorldInfo, openWorldInfoEditor, saveWorldInfo, setWorldInfoSettings } from '../../../../../../world-info.js';
/** /**
* Helper to log to both console and debug logs array * Helper to log to both console and debug logs array
@@ -117,68 +115,6 @@ function createConstantHeaderEntry() {
return entry; return entry;
} }
/**
* Save a world info entry to a lorebook
* @param {string} lorebookUid - The filename/UID of the lorebook
* @param {Object} entry - The entry data
*/
async function saveWorldInfoEntry(lorebookUid, entry) {
try {
debugLog('[Memory Recollection] Saving entry to lorebook:', lorebookUid);
// Open the world info editor for this lorebook to load its data
await openWorldInfoEditor(lorebookUid);
// Wait for it to load
await new Promise(resolve => setTimeout(resolve, 500));
// Now access the loaded world info data
const worldInfo = window.world_info;
debugLog('[Memory Recollection] World info after opening:', {
type: typeof worldInfo,
isArray: Array.isArray(worldInfo),
hasEntries: worldInfo?.entries !== undefined,
keys: worldInfo ? Object.keys(worldInfo).slice(0, 10) : null
});
// Try different structures - it might be an array or might have different properties
let entries;
if (worldInfo && typeof worldInfo === 'object') {
if (worldInfo.entries) {
entries = worldInfo.entries;
} else if (Array.isArray(worldInfo)) {
// If it's an array, convert to entries object
entries = {};
worldInfo.forEach((e, i) => {
if (e && e.uid) {
entries[e.uid] = e;
}
});
}
}
if (!entries) {
entries = {};
}
// Add the entry
entries[entry.uid] = entry;
debugLog('[Memory Recollection] Entry added, saving world info...');
// Save using the imported saveWorldInfo function
// Pass the entries as the data structure
await saveWorldInfo(lorebookUid, { entries });
debugLog('[Memory Recollection] Entry saved successfully');
return { success: true };
} catch (error) {
console.error('[Memory Recollection] Error saving entry:', error);
throw error;
}
}
/** /**
* Save multiple world info entries to a lorebook at once * Save multiple world info entries to a lorebook at once
* @param {string} lorebookUid - The filename/UID of the lorebook * @param {string} lorebookUid - The filename/UID of the lorebook
-11
View File
@@ -70,7 +70,6 @@ export function setupPlotButtons(handlePlotClick) {
*/ */
export async function sendPlotProgression(type) { export async function sendPlotProgression(type) {
if (!extensionSettings.enabled) { if (!extensionSettings.enabled) {
// console.log('[RPG Companion] Extension is disabled');
return; return;
} }
@@ -83,8 +82,6 @@ export async function sendPlotProgression(type) {
extensionSettings.enabled = false; extensionSettings.enabled = false;
try { try {
// console.log(`[RPG Companion] Sending ${type} plot progression request...`);
// Build the prompt based on type // Build the prompt based on type
let prompt = ''; let prompt = '';
if (type === 'random') { if (type === 'random') {
@@ -100,13 +97,8 @@ export async function sendPlotProgression(type) {
prompt += '\n\n' + htmlPromptText; prompt += '\n\n' + htmlPromptText;
} }
// Set flag to indicate we're doing plot progression
// This will be used by onMessageReceived to clear the prompt after generation completes
setIsPlotProgression(true); setIsPlotProgression(true);
// console.log('[RPG Companion] Calling Generate with continuation and plot prompt');
// console.log('[RPG Companion] Full prompt:', prompt);
// Pass the prompt via options with the correct property name // Pass the prompt via options with the correct property name
// Based on /continue slash command implementation, it uses quiet_prompt (underscore, not camelCase) // Based on /continue slash command implementation, it uses quiet_prompt (underscore, not camelCase)
const options = { const options = {
@@ -114,10 +106,7 @@ export async function sendPlotProgression(type) {
quietToLoud: true quietToLoud: true
}; };
// Call Generate with 'continue' type and our custom prompt
await Generate('continue', options); await Generate('continue', options);
// console.log('[RPG Companion] Plot progression generation triggered');
} catch (error) { } catch (error) {
console.error('[RPG Companion] Error sending plot progression:', error); console.error('[RPG Companion] Error sending plot progression:', error);
setIsPlotProgression(false); setIsPlotProgression(false);
+20 -66
View File
@@ -10,17 +10,11 @@ import {
lastGeneratedData, lastGeneratedData,
committedTrackerData, committedTrackerData,
isGenerating, isGenerating,
lastActionWasSwipe, setIsGenerating
setIsGenerating,
setLastActionWasSwipe
} from '../../core/state.js'; } from '../../core/state.js';
import { saveChatData } from '../../core/persistence.js'; import { saveChatData } from '../../core/persistence.js';
import { generateSeparateUpdatePrompt } from './promptBuilder.js'; import { generateSeparateUpdatePrompt } from './promptBuilder.js';
import { parseResponse, parseUserStats, parseSkills, tryParseJSONResponse } 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';
import { renderInventory } from '../rendering/inventory.js';
import { renderQuests } from '../rendering/quests.js'; import { renderQuests } from '../rendering/quests.js';
import { renderSkills } from '../rendering/skills.js'; import { renderSkills } from '../rendering/skills.js';
import { i18n } from '../../core/i18n.js'; import { i18n } from '../../core/i18n.js';
@@ -37,12 +31,8 @@ async function getCurrentPresetName() {
// Use /preset without arguments to get the current preset name // Use /preset without arguments to get the current preset name
const result = await executeSlashCommandsOnChatInput('/preset', { quiet: true }); const result = await executeSlashCommandsOnChatInput('/preset', { quiet: true });
// console.log('[RPG Companion] /preset result:', result);
// The result should be an object with a 'pipe' property containing the preset name
if (result && typeof result === 'object' && result.pipe) { if (result && typeof result === 'object' && result.pipe) {
const presetName = String(result.pipe).trim(); const presetName = String(result.pipe).trim();
// console.log('[RPG Companion] Extracted preset name:', presetName);
return presetName || null; return presetName || null;
} }
@@ -63,11 +53,7 @@ async function getCurrentPresetName() {
*/ */
async function switchToPreset(presetName) { async function switchToPreset(presetName) {
try { try {
// Use the /preset slash command to switch presets
// This is the proper way to change presets in SillyTavern
await executeSlashCommandsOnChatInput(`/preset ${presetName}`, { quiet: true }); await executeSlashCommandsOnChatInput(`/preset ${presetName}`, { quiet: true });
// console.log(`[RPG Companion] Switched to preset "${presetName}"`);
return true; return true;
} catch (error) { } catch (error) {
console.error('[RPG Companion] Error switching preset:', error); console.error('[RPG Companion] Error switching preset:', error);
@@ -88,7 +74,6 @@ async function switchToPreset(presetName) {
*/ */
export async function updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory) { export async function updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory) {
if (isGenerating) { if (isGenerating) {
// console.log('[RPG Companion] Already generating, skipping...');
return; return;
} }
@@ -97,7 +82,6 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
} }
if (extensionSettings.generationMode !== 'separate') { if (extensionSettings.generationMode !== 'separate') {
// console.log('[RPG Companion] Not in separate mode, skipping manual update');
return; return;
} }
@@ -133,9 +117,6 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
}); });
if (response) { if (response) {
// console.log('[RPG Companion] Raw AI response:', response);
// Try JSON parsing first if structured data mode is enabled
const jsonParsed = tryParseJSONResponse(response); const jsonParsed = tryParseJSONResponse(response);
if (jsonParsed) { if (jsonParsed) {
@@ -149,23 +130,13 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
renderSkills(); renderSkills();
saveChatData(); saveChatData();
} else { } else {
// JSON parsing failed - try legacy text-based parsing as fallback
console.warn('[RPG Companion] JSON parsing failed, attempting legacy text parsing...'); console.warn('[RPG Companion] JSON parsing failed, attempting legacy text parsing...');
const parsedData = parseResponse(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');
// Legacy text parsing does not provide structured characters; clear stale structured data extensionSettings.charactersData = [];
extensionSettings.charactersData = []; const parsedCharacterThoughts = parsedData.characterThoughts || '';
const parsedCharacterThoughts = parsedData.characterThoughts || '';
// 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; 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 && !lastMessage.is_user) {
if (!lastMessage.extra) { if (!lastMessage.extra) {
lastMessage.extra = {}; lastMessage.extra = {};
@@ -175,16 +146,13 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
} }
const currentSwipeId = lastMessage.swipe_id || 0; const currentSwipeId = lastMessage.swipe_id || 0;
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = { lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
userStats: parsedData.userStats, userStats: parsedData.userStats,
infoBox: parsedData.infoBox, infoBox: parsedData.infoBox,
characterThoughts: parsedCharacterThoughts characterThoughts: parsedCharacterThoughts
}; };
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId); if (parsedData.userStats) {
// Update lastGeneratedData for display AND future commit
if (parsedData.userStats) {
lastGeneratedData.userStats = parsedData.userStats; lastGeneratedData.userStats = parsedData.userStats;
parseUserStats(parsedData.userStats); parseUserStats(parsedData.userStats);
} }
@@ -194,28 +162,19 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
if (parsedData.infoBox) { if (parsedData.infoBox) {
lastGeneratedData.infoBox = parsedData.infoBox; lastGeneratedData.infoBox = parsedData.infoBox;
} }
lastGeneratedData.characterThoughts = parsedCharacterThoughts; lastGeneratedData.characterThoughts = parsedCharacterThoughts;
// console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', {
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
// });
// Only auto-commit on TRULY first generation (no committed data exists at all) const hasAnyCommittedContent = (
// This prevents auto-commit after refresh when we have saved committed data (committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') ||
const hasAnyCommittedContent = ( (committedTrackerData.infoBox && committedTrackerData.infoBox.trim() !== '' && committedTrackerData.infoBox !== 'Info Box\n---\n') ||
(committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') || (committedTrackerData.characterThoughts && committedTrackerData.characterThoughts.trim() !== '' && committedTrackerData.characterThoughts !== 'Present Characters\n---\n')
(committedTrackerData.infoBox && committedTrackerData.infoBox.trim() !== '' && committedTrackerData.infoBox !== 'Info Box\n---\n') || );
(committedTrackerData.characterThoughts && committedTrackerData.characterThoughts.trim() !== '' && committedTrackerData.characterThoughts !== 'Present Characters\n---\n')
);
// Only commit if we have NO committed content at all (truly first time ever) if (!hasAnyCommittedContent) {
if (!hasAnyCommittedContent) { committedTrackerData.userStats = parsedData.userStats;
committedTrackerData.userStats = parsedData.userStats; committedTrackerData.infoBox = parsedData.infoBox;
committedTrackerData.infoBox = parsedData.infoBox; committedTrackerData.characterThoughts = parsedCharacterThoughts;
committedTrackerData.characterThoughts = parsedCharacterThoughts; }
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
}
// Render the updated data // Render the updated data
renderUserStats(); renderUserStats();
@@ -254,14 +213,9 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
setIsGenerating(false); setIsGenerating(false);
// Restore button to original state
const $updateBtn = $('#rpg-manual-update'); const $updateBtn = $('#rpg-manual-update');
const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info'; const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info';
$updateBtn.html(`<i class="fa-solid fa-sync"></i> ${refreshText}`).prop('disabled', false); $updateBtn.html(`<i class="fa-solid fa-sync"></i> ${refreshText}`).prop('disabled', false);
// Reset the flag after tracker generation completes
// This ensures the flag persists through both main generation AND tracker generation
// console.log('[RPG Companion] 🔄 Tracker generation complete - resetting lastActionWasSwipe to false');
setLastActionWasSwipe(false); setLastActionWasSwipe(false);
} }
} }
+5 -106
View File
@@ -9,9 +9,7 @@ import {
extensionSettings, extensionSettings,
committedTrackerData, committedTrackerData,
lastGeneratedData, lastGeneratedData,
isGenerating, isGenerating
lastActionWasSwipe,
setLastActionWasSwipe
} from '../../core/state.js'; } from '../../core/state.js';
import { evaluateSuppression } from './suppression.js'; import { evaluateSuppression } from './suppression.js';
import { parseUserStats } from './parser.js'; import { parseUserStats } from './parser.js';
@@ -39,15 +37,7 @@ function getTrackerInstructions(includeHtmlPrompt, includeContinuation) {
* @param {Object} data - Event data * @param {Object} data - Event data
*/ */
export function onGenerationStarted(type, data) { export function onGenerationStarted(type, data) {
// console.log('[RPG Companion] onGenerationStarted called');
// console.log('[RPG Companion] enabled:', extensionSettings.enabled);
// console.log('[RPG Companion] generationMode:', extensionSettings.generationMode);
// console.log('[RPG Companion] ⚡ EVENT: onGenerationStarted - lastActionWasSwipe =', lastActionWasSwipe, '| isGenerating =', isGenerating);
// console.log('[RPG Companion] Committed Prompt:', committedTrackerData);
// Skip tracker injection for image generation requests
if (data?.quietImage) { if (data?.quietImage) {
// console.log('[RPG Companion] Detected image generation (quietImage=true), skipping tracker injection');
return; return;
} }
@@ -57,74 +47,27 @@ export function onGenerationStarted(type, data) {
const context = getContext(); const context = getContext();
const chat = context.chat; const chat = context.chat;
// Detect if a guided generation is active (GuidedGenerations and similar extensions
// inject an ephemeral 'instruct' injection into chatMetadata.script_injects).
// If present, we should avoid injecting RPG tracker instructions that ask
// the model to include stats/etc. This prevents conflicts when guided prompts
// are used (e.g., GuidedGenerations Extension).
// Evaluate suppression using the shared helper
const suppression = evaluateSuppression(extensionSettings, context, data); const suppression = evaluateSuppression(extensionSettings, context, data);
const { shouldSuppress, skipMode, isGuidedGeneration, isImpersonationGeneration, hasQuietPrompt, instructContent, quietPromptRaw, matchedPattern } = suppression; const { shouldSuppress, skipMode, isGuidedGeneration, isImpersonationGeneration, hasQuietPrompt } = suppression;
if (shouldSuppress) { if (shouldSuppress) {
// Debugging: indicate active suppression and which source triggered it
console.debug(`[RPG Companion] Suppression active (mode=${skipMode}). isGuided=${isGuidedGeneration}, isImpersonation=${isImpersonationGeneration}, hasQuietPrompt=${hasQuietPrompt} - skipping RPG tracker injections for this generation.`); console.debug(`[RPG Companion] Suppression active (mode=${skipMode}). isGuided=${isGuidedGeneration}, isImpersonation=${isImpersonationGeneration}, hasQuietPrompt=${hasQuietPrompt} - skipping RPG tracker injections for this generation.`);
// Also clear any existing RPG Companion prompts so they do not leak into this generation
// (e.g., previously set extension prompts should not be used alongside a guided prompt)
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false); setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
} }
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
// For SEPARATE mode only: Check if we need to commit extension data
// BUT: Only do this for the MAIN generation, not the tracker update generation
// If isGenerating is true, this is the tracker update generation (second call), so skip flag logic
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
if (extensionSettings.generationMode === 'separate' && !isGenerating) { if (extensionSettings.generationMode === 'separate' && !isGenerating) {
if (!lastActionWasSwipe) { if (!lastActionWasSwipe) {
// User sent a new message - commit lastGeneratedData before generation
// console.log('[RPG Companion] 📝 COMMIT: New message - committing lastGeneratedData');
// console.log('[RPG Companion] BEFORE commit - committedTrackerData:', {
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
// });
// console.log('[RPG Companion] BEFORE commit - lastGeneratedData:', {
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
// });
committedTrackerData.userStats = lastGeneratedData.userStats; committedTrackerData.userStats = lastGeneratedData.userStats;
committedTrackerData.infoBox = lastGeneratedData.infoBox; committedTrackerData.infoBox = lastGeneratedData.infoBox;
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts; committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
// console.log('[RPG Companion] AFTER commit - committedTrackerData:', {
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
// });
// Reset flag after committing (ready for next cycle)
} else {
// console.log('[RPG Companion] 🔄 SWIPE: Using existing committedTrackerData (no commit)');
// console.log('[RPG Companion] committedTrackerData:', {
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
// });
// Reset flag after using it (swipe generation complete, ready for next action)
} }
} }
// For TOGETHER mode: Check if we need to commit extension data
// Only commit when user sends a new message (not on swipes)
if (extensionSettings.generationMode === 'together') { if (extensionSettings.generationMode === 'together') {
if (!lastActionWasSwipe) { if (!lastActionWasSwipe) {
// User sent a new message - commit data from the last assistant message they replied to
// This ensures swipes use consistent data from before the first swipe
console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: New message - committing from last assistant message'); console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: New message - committing from last assistant message');
// Find the last assistant message (before the user's new message) // Find the last assistant message (before the user's new message)
@@ -163,79 +106,44 @@ export function onGenerationStarted(type, data) {
} }
} }
// Use the committed tracker data as source for generation
// console.log('[RPG Companion] Using committedTrackerData for generation');
// console.log('[RPG Companion] committedTrackerData.userStats:', committedTrackerData.userStats);
// Parse stats from committed data to update the extensionSettings for prompt generation
if (committedTrackerData.userStats) { if (committedTrackerData.userStats) {
// console.log('[RPG Companion] Parsing committed userStats into extensionSettings');
parseUserStats(committedTrackerData.userStats); parseUserStats(committedTrackerData.userStats);
// console.log('[RPG Companion] After parsing, extensionSettings.userStats:', JSON.stringify(extensionSettings.userStats));
} }
if (extensionSettings.generationMode === 'together') { if (extensionSettings.generationMode === 'together') {
// console.log('[RPG Companion] In together mode, generating prompts...'); const example = '';
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 = getTrackerInstructions(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); setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
// console.log('[RPG Companion] Example:', example ? 'exists' : 'empty'); let lastAssistantDepth = -1;
// console.log('[RPG Companion] Chat length:', chat ? chat.length : 'chat is null');
// Find the last assistant message in the chat history
let lastAssistantDepth = -1; // -1 means not found
if (chat && chat.length > 0) { if (chat && chat.length > 0) {
// console.log('[RPG Companion] Searching for last assistant message...');
// Start from depth 1 (skip depth 0 which is usually user's message or prefill)
for (let depth = 1; depth < chat.length; depth++) { for (let depth = 1; depth < chat.length; depth++) {
const index = chat.length - 1 - depth; // Convert depth to index const index = chat.length - 1 - depth;
const message = chat[index]; const message = chat[index];
// console.log('[RPG Companion] Checking depth', depth, 'index', index, 'message properties:', Object.keys(message));
// Check for assistant message: not user and not system
if (!message.is_user && !message.is_system) { if (!message.is_user && !message.is_system) {
// Found assistant message at this depth
// Inject at the SAME depth to prepend to this assistant message
lastAssistantDepth = depth; lastAssistantDepth = depth;
// console.log('[RPG Companion] Found last assistant message at depth', depth, '-> injecting at same depth:', lastAssistantDepth);
break; break;
} }
} }
} }
// If we have previous tracker data and found an assistant message, inject it as an assistant message
if (!shouldSuppress && example && lastAssistantDepth > 0) { if (!shouldSuppress && example && lastAssistantDepth > 0) {
setExtensionPrompt('rpg-companion-example', example, extension_prompt_types.IN_CHAT, lastAssistantDepth, false, extension_prompt_roles.ASSISTANT); setExtensionPrompt('rpg-companion-example', example, extension_prompt_types.IN_CHAT, lastAssistantDepth, false, extension_prompt_roles.ASSISTANT);
// console.log('[RPG Companion] Injected tracker example as assistant message at depth:', lastAssistantDepth);
} else {
// console.log('[RPG Companion] NOT injecting example. example:', !!example, 'lastAssistantDepth:', lastAssistantDepth);
} }
// Inject the instructions as a user message at depth 0 (right before generation)
// If this is a guided generation (user explicitly injected 'instruct'), skip adding
// our tracker instructions to avoid clobbering the guided prompt.
if (!shouldSuppress) { if (!shouldSuppress) {
setExtensionPrompt('rpg-companion-inject', instructions, extension_prompt_types.IN_CHAT, 0, false, extension_prompt_roles.USER); setExtensionPrompt('rpg-companion-inject', instructions, extension_prompt_types.IN_CHAT, 0, false, extension_prompt_roles.USER);
} }
// console.log('[RPG Companion] Injected RPG tracking instructions at depth 0 (right before generation)');
// Inject HTML prompt separately at depth 0 if enabled (prevents duplication on swipes)
if (extensionSettings.enableHtmlPrompt && !shouldSuppress) { if (extensionSettings.enableHtmlPrompt && !shouldSuppress) {
// Use custom HTML prompt if set, otherwise use default
const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT; const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
const htmlPrompt = `\n${htmlPromptText}`; const htmlPrompt = `\n${htmlPromptText}`;
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for together mode');
} else { } else {
// Clear HTML prompt if disabled
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
} }
} else if (extensionSettings.generationMode === 'separate') { } else if (extensionSettings.generationMode === 'separate') {
// In SEPARATE mode, inject the current state as JSON for main roleplay generation
const currentStateJSON = generateContextualSummary(); const currentStateJSON = generateContextualSummary();
if (currentStateJSON) { if (currentStateJSON) {
@@ -246,27 +154,18 @@ ${currentStateJSON}
\`\`\` \`\`\`
</context>\n\n`; </context>\n\n`;
// Inject context at depth 1 (before last user message) as SYSTEM
// Skip when a guided generation injection is present to avoid conflicting instructions
if (!shouldSuppress) { if (!shouldSuppress) {
setExtensionPrompt('rpg-companion-context', wrappedContext, extension_prompt_types.IN_CHAT, 1, false); setExtensionPrompt('rpg-companion-context', wrappedContext, extension_prompt_types.IN_CHAT, 1, false);
} }
// console.log('[RPG Companion] Injected current state JSON for separate mode:', currentStateJSON);
} else { } else {
// Clear if no data yet
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false); setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
} }
// Inject HTML prompt separately at depth 0 if enabled (same as together mode pattern)
if (extensionSettings.enableHtmlPrompt && !shouldSuppress) { if (extensionSettings.enableHtmlPrompt && !shouldSuppress) {
// Use custom HTML prompt if set, otherwise use default
const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT; const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
const htmlPrompt = `\n${htmlPromptText}`; const htmlPrompt = `\n${htmlPromptText}`;
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for separate mode');
} else { } else {
// Clear HTML prompt if disabled
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
} }
+12 -23
View File
@@ -4,10 +4,10 @@
* Supports both legacy text format and new JSON format * Supports both legacy text format and new JSON format
*/ */
import { extensionSettings, FEATURE_FLAGS, addDebugLog, lastGeneratedData, committedTrackerData } from '../../core/state.js'; import { extensionSettings, addDebugLog, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { saveSettings, saveChatData } from '../../core/persistence.js'; import { saveSettings, saveChatData } from '../../core/persistence.js';
import { extractInventory } from './inventoryParser.js'; import { extractInventory } from './inventoryParser.js';
import { validateTrackerData, mergeTrackerData } from '../../types/trackerData.js'; import { validateTrackerData } from '../../types/trackerData.js';
import { handleItemRemoved } from '../rendering/skills.js'; import { handleItemRemoved } from '../rendering/skills.js';
/** /**
@@ -850,24 +850,13 @@ export function parseUserStats(statsText) {
} }
} }
// Extract inventory - use v2 parser if feature flag enabled, otherwise fallback to v1 // Extract inventory - extractInventory() handles v2 format and falls back to v1 if needed
if (FEATURE_FLAGS.useNewInventory) { const inventoryData = extractInventory(statsText);
const inventoryData = extractInventory(statsText); if (inventoryData) {
if (inventoryData) { extensionSettings.userStats.inventory = inventoryData;
extensionSettings.userStats.inventory = inventoryData; debugLog('[RPG Parser] Inventory extracted:', inventoryData);
debugLog('[RPG Parser] Inventory v2 extracted:', inventoryData);
} else {
debugLog('[RPG Parser] Inventory v2 extraction failed');
}
} else { } else {
// Legacy v1 parsing for backward compatibility debugLog('[RPG Parser] Inventory extraction failed');
const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i);
if (inventoryMatch) {
extensionSettings.userStats.inventory = inventoryMatch[1].trim();
debugLog('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim());
} else {
debugLog('[RPG Parser] Inventory v1 not found');
}
} }
// Extract quests // Extract quests
@@ -900,7 +889,7 @@ export function parseUserStats(statsText) {
arousal: extensionSettings.userStats.arousal, arousal: extensionSettings.userStats.arousal,
mood: extensionSettings.userStats.mood, mood: extensionSettings.userStats.mood,
conditions: extensionSettings.userStats.conditions, conditions: extensionSettings.userStats.conditions,
inventory: FEATURE_FLAGS.useNewInventory ? 'v2 object' : extensionSettings.userStats.inventory inventory: extensionSettings.userStats.inventory
}); });
saveSettings(); saveSettings();
@@ -947,11 +936,11 @@ export function parseSkills(skillsText) {
extensionSettings.skillAbilityLinks = {}; extensionSettings.skillAbilityLinks = {};
} }
// Get configured skill categories (handle both old string and new object format) // Migration function handles string array → object array conversion on load
const rawCategories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || []; const rawCategories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
const configuredCategories = rawCategories const configuredCategories = rawCategories
.filter(cat => typeof cat === 'string' || cat.enabled !== false) .filter(cat => cat.enabled !== false)
.map(cat => typeof cat === 'string' ? cat : cat.name); .map(cat => cat.name);
const lines = skillsText.split('\n'); const lines = skillsText.split('\n');
const newSkillAbilityLinks = {}; const newSkillAbilityLinks = {};
+11 -13
View File
@@ -4,10 +4,9 @@
*/ */
import { getContext } from '../../../../../../extensions.js'; import { getContext } from '../../../../../../extensions.js';
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js'; import { chat, characters, this_chid } from '../../../../../../../script.js';
import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js'; import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js';
import { extensionSettings, committedTrackerData } from '../../core/state.js'; import { extensionSettings } from '../../core/state.js';
import { generateSchemaExample } from '../../types/trackerData.js';
// Type imports // Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
@@ -28,7 +27,7 @@ export const DEFAULT_JSON_TRACKER_PROMPT = `At the start of every reply, output
* Default message interception prompt text * Default message interception prompt text
* Guides the LLM to rewrite the user's message based on current RPG state and recent chat * Guides the LLM to rewrite the user's message based on current RPG state and recent chat
*/ */
export const DEFAULT_MESSAGE_INTERCEPTION_PROMPT = `Act as an uncompromising Immersive Copy Editor who rewrites the user's draft to strictly adhere to {{user}}'s persona and RPG state (JSON). You must validate the feasibility of the user's intended actions for {{user}} against the JSON state; if the draft contradicts the state (e.g., acting smart while 'Intelligence' is low, or running while having a 'Leg Injury'), you are required to override the core intent, rewriting the action to portray immediate failure, struggle, or involuntary reaction instead of the user's desired success. Even further, if the intended course of action is physically impossible via the state or represents a thought process conceptually alien to the character's nature or current state, you are mandated to completely overwrite the user's intent. Aggressively rephrase vocabulary and syntax to match the character's specific cognitive capacity and tone. Keep the output concise and devoid of fluff; do not expand the narrative beyond the necessary state-enforced correction. Never include information that was not already present in the original draft. Never narrate the consequences of {{user}}'s actions, only what they are. Return ONLY the modified message text.`; export const DEFAULT_MESSAGE_INTERCEPTION_PROMPT = `Act as an uncompromising Immersive Copy Editor who rewrites the user's draft to strictly adhere to {{user}}'s persona and RPG state (JSON). You must validate the feasibility of the user's intended actions for {{user}} to take against the JSON state; if the draft contradicts said state (e.g. acting smart while 'Intelligence' is low, or running while having a 'Leg Injury'), you are required to override the core intent, rewriting the action to portray immediate failure, struggle, or an involuntary reaction instead of the user's desired success. Even further, if the intended course of action is physically impossible via the state or represents a thought process that is conceptually alien to the character's nature or current state, you are mandated to completely overwrite the user's intent. Be careful not to confuse communicated intent (e.g. walk towards some direction, or throw a punch) with intended speech. Intent must always be overwritten with intent (e.g. if user wanted for {{user}} to run, but they have a leg injury, your correction will make them limp). Speech must always be overwritten with speech (e.g. if user means for {{user}} to speak eloquently, but they're not smart enough, you'd dumb down their choice of words or speech patterns). Never replace intent with failed speech (e.g. user communicates that {{user}} will throw a punch, or make a gesture, but you incorrectly decide that they will make muffled noises instead, as they are gagged). Aggressively rephrase vocabulary and syntax to match the character's specific cognitive capacity and tone. Keep the output concise and devoid of fluff; do not expand the narrative beyond the necessary state-enforced correction. Never include information that was not already present in the original draft. Never narrate the consequences of {{user}}'s actions, only what they are. Return ONLY the modified message text.`;
/** /**
* Gets character card information for current chat (handles both single and group chats) * Gets character card information for current chat (handles both single and group chats)
@@ -362,16 +361,13 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
// Skills section // Skills section
if (showSkills) { if (showSkills) {
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || []; const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
// Filter to only enabled categories and handle both old (string) and new (object) formats // Migration function handles string array → object array conversion on load
const enabledCategories = skillCategories.filter(cat => { const enabledCategories = skillCategories.filter(cat => cat.enabled !== false);
if (typeof cat === 'string') return true;
return cat.enabled !== false;
});
if (enabledCategories.length > 0) { if (enabledCategories.length > 0) {
let skillsSection = ' "skills": {\n'; let skillsSection = ' "skills": {\n';
const categoryExamples = enabledCategories.map(cat => { const categoryExamples = enabledCategories.map(cat => {
const catName = typeof cat === 'string' ? cat : cat.name; const catName = cat.name;
let skillExample = '{ "name": "Ability Name", "description": "What this ability does" }'; let skillExample = '{ "name": "Ability Name", "description": "What this ability does" }';
if (enableItemSkillLinks) { if (enableItemSkillLinks) {
skillExample = '{ "name": "Ability", "description": "Description", "grantedBy": "Item Name" }'; skillExample = '{ "name": "Ability", "description": "Description", "grantedBy": "Item Name" }';
@@ -407,6 +403,11 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
instructions += '- Level is a numeric value (typically 1+, represents character progression)\n'; instructions += '- Level is a numeric value (typically 1+, represents character progression)\n';
} }
if (showQuests) {
instructions += '- A main quest can be created when the current main objective changes\n';
instructions += '- Optional quests can be created for smaller matters that need to be resolved\n';
}
instructions += '- Items should be placeed in the inventory section, not the skills section\n'; instructions += '- Items should be placeed in the inventory section, not the skills section\n';
instructions += '- Characters should be removed as soon as they leave the scene\n'; instructions += '- Characters should be removed as soon as they leave the scene\n';
instructions += '- Your list of characters must never include {{user}}\n'; instructions += '- Your list of characters must never include {{user}}\n';
@@ -675,8 +676,6 @@ export function generateContextualSummary() {
* @returns {string} Full prompt text for separate tracker generation * @returns {string} Full prompt text for separate tracker generation
*/ */
export function generateRPGPromptText() { export function generateRPGPromptText() {
const userName = getContext().name1;
let promptText = ''; let promptText = '';
promptText += `Here are the previous trackers in JSON format that you should consider when responding:\n`; promptText += `Here are the previous trackers in JSON format that you should consider when responding:\n`;
@@ -814,7 +813,6 @@ export function generateRPGPromptText() {
*/ */
export async function generateSeparateUpdatePrompt() { export async function generateSeparateUpdatePrompt() {
const depth = extensionSettings.updateDepth; const depth = extensionSettings.updateDepth;
const userName = getContext().name1;
const messages = []; const messages = [];
-51
View File
@@ -62,15 +62,10 @@ export function commitTrackerData() {
const swipeData = message.extra.rpg_companion_swipes[swipeId]; const swipeData = message.extra.rpg_companion_swipes[swipeId];
if (swipeData) { if (swipeData) {
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
committedTrackerData.userStats = swipeData.userStats || null; committedTrackerData.userStats = swipeData.userStats || null;
committedTrackerData.infoBox = swipeData.infoBox || null; committedTrackerData.infoBox = swipeData.infoBox || null;
committedTrackerData.characterThoughts = swipeData.characterThoughts || null; committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
} else {
// console.log('[RPG Companion] No swipe data found for swipe', swipeId);
} }
} else {
// console.log('[RPG Companion] No RPG data found in last assistant message');
} }
break; break;
} }
@@ -85,9 +80,7 @@ export function commitTrackerData() {
export async function onMessageSent() { export async function onMessageSent() {
if (!extensionSettings.enabled) return; if (!extensionSettings.enabled) return;
// User sent a new message - NOT a swipe
setLastActionWasSwipe(false); setLastActionWasSwipe(false);
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent - lastActionWasSwipe =', lastActionWasSwipe);
// Optionally intercept and rewrite the user message via LLM // Optionally intercept and rewrite the user message via LLM
if (extensionSettings.enableMessageInterception && extensionSettings.messageInterceptionActive !== false) { if (extensionSettings.enableMessageInterception && extensionSettings.messageInterceptionActive !== false) {
@@ -106,10 +99,7 @@ export async function onMessageSent() {
committedTrackerData.infoBox = lastGeneratedData.infoBox; committedTrackerData.infoBox = lastGeneratedData.infoBox;
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts; committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
// Save to chat metadata
saveChatData(); saveChatData();
// console.log('[RPG Companion] 💾 Committed displayed tracker on user message (auto-update disabled)');
} }
} }
} }
@@ -206,7 +196,6 @@ export async function onMessageReceived(data) {
const lastMessage = chat[chat.length - 1]; const lastMessage = chat[chat.length - 1];
if (lastMessage && !lastMessage.is_user) { if (lastMessage && !lastMessage.is_user) {
const responseText = lastMessage.mes; const responseText = lastMessage.mes;
// console.log('[RPG Companion] Parsing together mode response:', responseText);
// Try JSON parsing first if structured data mode is enabled // Try JSON parsing first if structured data mode is enabled
const jsonParsed = tryParseJSONResponse(responseText); const jsonParsed = tryParseJSONResponse(responseText);
@@ -228,7 +217,6 @@ export async function onMessageReceived(data) {
// JSON parsing failed - fall back to legacy text-based parsing // JSON parsing failed - fall back to legacy text-based parsing
console.warn('[RPG Companion] JSON parsing failed in together mode, attempting legacy text parsing...'); console.warn('[RPG Companion] JSON parsing failed in together mode, attempting legacy text parsing...');
const parsedData = parseResponse(responseText); const parsedData = parseResponse(responseText);
// console.log('[RPG Companion] Parsed data:', parsedData);
// Legacy text parsing does not produce structured characters; clear old state to avoid stale UI/state // Legacy text parsing does not produce structured characters; clear old state to avoid stale UI/state
extensionSettings.charactersData = []; extensionSettings.charactersData = [];
@@ -264,16 +252,11 @@ export async function onMessageReceived(data) {
characterThoughts: parsedCharacterThoughts characterThoughts: parsedCharacterThoughts
}; };
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
// If there's no committed data yet (first time generating), automatically commit // If there's no committed data yet (first time generating), automatically commit
if (!committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts) { if (!committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts) {
committedTrackerData.userStats = parsedData.userStats; committedTrackerData.userStats = parsedData.userStats;
committedTrackerData.infoBox = parsedData.infoBox; committedTrackerData.infoBox = parsedData.infoBox;
committedTrackerData.characterThoughts = parsedCharacterThoughts; committedTrackerData.characterThoughts = parsedCharacterThoughts;
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
} else {
// console.log('[RPG Companion] Data will be committed when user replies');
} }
// Remove the tracker code blocks from the visible message // Remove the tracker code blocks from the visible message
@@ -307,8 +290,6 @@ export async function onMessageReceived(data) {
const messageId = chat.length - 1; const messageId = chat.length - 1;
updateMessageBlock(messageId, lastMessage, { rerenderMessage: true }); updateMessageBlock(messageId, lastMessage, { rerenderMessage: true });
// console.log('[RPG Companion] Cleaned message, removed tracker code blocks from DOM');
// Save to chat metadata // Save to chat metadata
saveChatData(); saveChatData();
} }
@@ -321,9 +302,7 @@ export async function onMessageReceived(data) {
// Reset the swipe flag after generation completes // Reset the swipe flag after generation completes
// This ensures that if the user swiped → auto-reply generated → flag is now cleared // This ensures that if the user swiped → auto-reply generated → flag is now cleared
// so the next user message will be treated as a new message (not a swipe)
if (lastActionWasSwipe) { if (lastActionWasSwipe) {
// console.log('[RPG Companion] 🔄 Generation complete after swipe - resetting lastActionWasSwipe to false');
setLastActionWasSwipe(false); setLastActionWasSwipe(false);
} }
@@ -331,7 +310,6 @@ export async function onMessageReceived(data) {
// Note: No need to clear extension prompt since we used quiet_prompt option // Note: No need to clear extension prompt since we used quiet_prompt option
if (isPlotProgression) { if (isPlotProgression) {
setIsPlotProgression(false); setIsPlotProgression(false);
// console.log('[RPG Companion] Plot progression generation completed');
} }
} }
@@ -375,8 +353,6 @@ export function onMessageSwiped(messageIndex) {
return; return;
} }
// console.log('[RPG Companion] Message swiped at index:', messageIndex);
// Get the message that was swiped // Get the message that was swiped
const message = chat[messageIndex]; const message = chat[messageIndex];
if (!message || message.is_user) { if (!message || message.is_user) {
@@ -393,16 +369,9 @@ export function onMessageSwiped(messageIndex) {
message.swipes[currentSwipeId].length > 0; message.swipes[currentSwipeId].length > 0;
if (!isExistingSwipe) { if (!isExistingSwipe) {
// This is a NEW swipe that will trigger generation
setLastActionWasSwipe(true); setLastActionWasSwipe(true);
// console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped (NEW generation) - lastActionWasSwipe =', lastActionWasSwipe);
} else {
// This is navigating to an EXISTING swipe - don't change the flag
// console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped (existing swipe navigation) - lastActionWasSwipe unchanged =', lastActionWasSwipe);
} }
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
// Load RPG data for this swipe into lastGeneratedData (for display only) // Load RPG data for this swipe into lastGeneratedData (for display only)
// This updates what the user sees, but does NOT commit it // This updates what the user sees, but does NOT commit it
// Committed data will be updated when/if the user replies to this swipe // Committed data will be updated when/if the user replies to this swipe
@@ -418,13 +387,6 @@ export function onMessageSwiped(messageIndex) {
if (swipeData.userStats) { if (swipeData.userStats) {
parseUserStats(swipeData.userStats); parseUserStats(swipeData.userStats);
} }
// console.log('[RPG Companion] Loaded RPG data for swipe', currentSwipeId, '(display only, NOT committed)');
// console.log('[RPG Companion] committedTrackerData unchanged - will be updated if user replies to this swipe');
} else {
// No data for this swipe - keep existing lastGeneratedData (don't clear it)
// This ensures the display remains consistent and data is available for next commit
// console.log('[RPG Companion] No RPG data for swipe', currentSwipeId, '- keeping existing lastGeneratedData');
} }
// Re-render the panels (display only - committedTrackerData unchanged) // Re-render the panels (display only - committedTrackerData unchanged)
@@ -444,7 +406,6 @@ export function onMessageSwiped(messageIndex) {
export function updatePersonaAvatar() { export function updatePersonaAvatar() {
const portraitImg = document.querySelector('.rpg-user-portrait'); const portraitImg = document.querySelector('.rpg-user-portrait');
if (!portraitImg) { if (!portraitImg) {
// console.log('[RPG Companion] Portrait image element not found in DOM');
return; return;
} }
@@ -452,23 +413,13 @@ export function updatePersonaAvatar() {
const context = getContext(); const context = getContext();
const currentUserAvatar = context.user_avatar || user_avatar; const currentUserAvatar = context.user_avatar || user_avatar;
// console.log('[RPG Companion] Attempting to update persona avatar:', currentUserAvatar);
// Try to get a valid thumbnail URL using our safe helper // Try to get a valid thumbnail URL using our safe helper
if (currentUserAvatar) { if (currentUserAvatar) {
const thumbnailUrl = getSafeThumbnailUrl('persona', currentUserAvatar); const thumbnailUrl = getSafeThumbnailUrl('persona', currentUserAvatar);
if (thumbnailUrl) { if (thumbnailUrl) {
// Only update the src if we got a valid URL
portraitImg.src = thumbnailUrl; portraitImg.src = thumbnailUrl;
// console.log('[RPG Companion] Persona avatar updated successfully');
} else {
// Don't update the src if we couldn't get a valid URL
// This prevents 400 errors and keeps the existing image
// console.warn('[RPG Companion] Could not get valid thumbnail URL for persona avatar, keeping existing image');
} }
} else {
// console.log('[RPG Companion] No user avatar configured, keeping existing image');
} }
} }
@@ -480,6 +431,4 @@ export function clearExtensionPrompts() {
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false); setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
// Note: rpg-companion-plot is not cleared here since it's passed via quiet_prompt option
// console.log('[RPG Companion] Cleared all extension prompts');
} }
@@ -5,7 +5,6 @@
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js'; import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js'; import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
import { buildUserStatsText } from '../rendering/userStats.js'; import { buildUserStatsText } from '../rendering/userStats.js';
import { renderInventory, getLocationId } from '../rendering/inventory.js'; import { renderInventory, getLocationId } from '../rendering/inventory.js';
import { parseItems, serializeItems } from '../../utils/itemParser.js'; import { parseItems, serializeItems } from '../../utils/itemParser.js';
@@ -293,8 +292,6 @@ export function removeItem(field, itemIndex, location) {
inventory[field] = newString; inventory[field] = newString;
} }
// console.log('[RPG Companion] DEBUG inventory after save:', inventory);
updateLastGeneratedDataInventory(); updateLastGeneratedDataInventory();
saveSettings(); saveSettings();
saveChatData(); saveChatData();
@@ -375,15 +372,11 @@ export function saveAddLocation() {
* @param {string} locationName - Name of location to remove * @param {string} locationName - Name of location to remove
*/ */
export function showRemoveConfirmation(locationName) { export function showRemoveConfirmation(locationName) {
// console.log('[RPG Companion] DEBUG showRemoveConfirmation called for:', locationName);
const confirmId = `rpg-remove-confirm-${getLocationId(locationName)}`; const confirmId = `rpg-remove-confirm-${getLocationId(locationName)}`;
// console.log('[RPG Companion] DEBUG confirmId:', confirmId);
const confirmUI = $(`#${confirmId}`); const confirmUI = $(`#${confirmId}`);
// console.log('[RPG Companion] DEBUG confirmUI element found:', confirmUI.length);
if (confirmUI.length > 0) { if (confirmUI.length > 0) {
confirmUI.show(); confirmUI.show();
// console.log('[RPG Companion] DEBUG confirmation shown');
} else { } else {
console.warn('[RPG Companion] DEBUG confirmation element not found!'); console.warn('[RPG Companion] DEBUG confirmation element not found!');
} }
@@ -407,12 +400,9 @@ export function hideRemoveConfirmation(locationName) {
* @param {string} locationName - Name of location to remove * @param {string} locationName - Name of location to remove
*/ */
export function confirmRemoveLocation(locationName) { export function confirmRemoveLocation(locationName) {
// console.log('[RPG Companion] DEBUG confirmRemoveLocation called for:', locationName);
const inventory = extensionSettings.userStats.inventory; const inventory = extensionSettings.userStats.inventory;
// console.log('[RPG Companion] DEBUG inventory.stored before deletion:', inventory.stored);
delete inventory.stored[locationName]; delete inventory.stored[locationName];
// console.log('[RPG Companion] DEBUG inventory.stored after deletion:', inventory.stored);
// Remove from collapsed list if present // Remove from collapsed list if present
const index = collapsedLocations.indexOf(locationName); const index = collapsedLocations.indexOf(locationName);
@@ -424,9 +414,6 @@ export function confirmRemoveLocation(locationName) {
saveSettings(); saveSettings();
saveChatData(); saveChatData();
updateMessageSwipeData(); updateMessageSwipeData();
// Re-render inventory UI
// console.log('[RPG Companion] DEBUG calling renderInventory()');
renderInventory(); renderInventory();
}/** }/**
* Toggles the collapsed state of a storage location section. * Toggles the collapsed state of a storage location section.
@@ -621,8 +608,6 @@ export function initInventoryEventListeners() {
const view = $(this).data('view'); const view = $(this).data('view');
switchViewMode(field, view); switchViewMode(field, view);
}); });
// console.log('[RPG Companion] Inventory event listeners initialized');
} }
/** /**
@@ -645,7 +630,6 @@ export function restoreFormStates() {
// Restore add location form // Restore add location form
if (openForms.addLocation) { if (openForms.addLocation) {
const form = $('#rpg-add-location-form'); const form = $('#rpg-add-location-form');
const input = $('#rpg-new-location-name');
if (form.length > 0) { if (form.length > 0) {
form.show(); form.show();
// Don't refocus to avoid disrupting user interaction // Don't refocus to avoid disrupting user interaction
@@ -655,7 +639,6 @@ export function restoreFormStates() {
// Restore add item on person form // Restore add item on person form
if (openForms.addItemOnPerson) { if (openForms.addItemOnPerson) {
const form = $('#rpg-add-item-form-onPerson'); const form = $('#rpg-add-item-form-onPerson');
const input = $('#rpg-new-item-onPerson');
if (form.length > 0) { if (form.length > 0) {
form.show(); form.show();
} }
@@ -664,7 +647,6 @@ export function restoreFormStates() {
// Restore add item assets form // Restore add item assets form
if (openForms.addItemAssets) { if (openForms.addItemAssets) {
const form = $('#rpg-add-item-form-assets'); const form = $('#rpg-add-item-form-assets');
const input = $('#rpg-new-item-assets');
if (form.length > 0) { if (form.length > 0) {
form.show(); form.show();
} }
@@ -673,7 +655,6 @@ export function restoreFormStates() {
// Restore add item simplified form // Restore add item simplified form
if (openForms.addItemSimplified) { if (openForms.addItemSimplified) {
const form = $('#rpg-add-item-form-simplified'); const form = $('#rpg-add-item-form-simplified');
const input = $('#rpg-new-item-simplified');
if (form.length > 0) { if (form.length > 0) {
form.show(); form.show();
} }
+45 -54
View File
@@ -128,11 +128,7 @@ export function renderInfoBox() {
return; return;
} }
// console.log('[RPG Companion] renderInfoBox called with data:', infoBoxData);
// Parse the info box data
const lines = infoBoxData.split('\n'); const lines = infoBoxData.split('\n');
// console.log('[RPG Companion] Info Box split into lines:', lines);
const data = { const data = {
date: '', date: '',
weekday: '', weekday: '',
@@ -158,8 +154,6 @@ export function renderInfoBox() {
}; };
for (const line of lines) { for (const line of lines) {
// console.log('[RPG Companion] Processing line:', line);
// Helper to check if a value is valid (not null/empty) // Helper to check if a value is valid (not null/empty)
const isValidParsedValue = (val) => val && val !== 'null' && val !== 'undefined' && val.toLowerCase() !== 'none'; const isValidParsedValue = (val) => val && val !== 'null' && val !== 'undefined' && val.toLowerCase() !== 'none';
@@ -167,7 +161,6 @@ export function renderInfoBox() {
// Prioritize text format over emoji format // Prioritize text format over emoji format
if (line.startsWith('Date:')) { if (line.startsWith('Date:')) {
if (!parsedFields.date) { if (!parsedFields.date) {
// console.log('[RPG Companion] → Matched DATE (text format)');
const dateStr = line.replace('Date:', '').trim(); const dateStr = line.replace('Date:', '').trim();
if (isValidParsedValue(dateStr)) { if (isValidParsedValue(dateStr)) {
const dateParts = dateStr.split(',').map(p => p.trim()); const dateParts = dateStr.split(',').map(p => p.trim());
@@ -180,7 +173,6 @@ export function renderInfoBox() {
} }
} else if (line.includes('🗓️:')) { } else if (line.includes('🗓️:')) {
if (!parsedFields.date) { if (!parsedFields.date) {
// console.log('[RPG Companion] → Matched DATE (emoji format)');
const dateStr = line.replace('🗓️:', '').trim(); const dateStr = line.replace('🗓️:', '').trim();
if (isValidParsedValue(dateStr)) { if (isValidParsedValue(dateStr)) {
const dateParts = dateStr.split(',').map(p => p.trim()); const dateParts = dateStr.split(',').map(p => p.trim());
@@ -193,7 +185,6 @@ export function renderInfoBox() {
} }
} else if (line.startsWith('Temperature:')) { } else if (line.startsWith('Temperature:')) {
if (!parsedFields.temperature) { if (!parsedFields.temperature) {
// console.log('[RPG Companion] → Matched TEMPERATURE (text format)');
const tempStr = line.replace('Temperature:', '').trim(); const tempStr = line.replace('Temperature:', '').trim();
if (isValidParsedValue(tempStr)) { if (isValidParsedValue(tempStr)) {
data.temperature = tempStr; data.temperature = tempStr;
@@ -206,7 +197,6 @@ export function renderInfoBox() {
} }
} else if (line.includes('🌡️:')) { } else if (line.includes('🌡️:')) {
if (!parsedFields.temperature) { if (!parsedFields.temperature) {
// console.log('[RPG Companion] → Matched TEMPERATURE (emoji format)');
const tempStr = line.replace('🌡️:', '').trim(); const tempStr = line.replace('🌡️:', '').trim();
if (isValidParsedValue(tempStr)) { if (isValidParsedValue(tempStr)) {
data.temperature = tempStr; data.temperature = tempStr;
@@ -219,7 +209,6 @@ export function renderInfoBox() {
} }
} else if (line.startsWith('Time:')) { } else if (line.startsWith('Time:')) {
if (!parsedFields.time) { if (!parsedFields.time) {
// console.log('[RPG Companion] → Matched TIME (text format)');
const timeStr = line.replace('Time:', '').trim(); const timeStr = line.replace('Time:', '').trim();
if (isValidParsedValue(timeStr)) { if (isValidParsedValue(timeStr)) {
data.time = timeStr; data.time = timeStr;
@@ -231,7 +220,6 @@ export function renderInfoBox() {
} }
} else if (line.includes('🕒:')) { } else if (line.includes('🕒:')) {
if (!parsedFields.time) { if (!parsedFields.time) {
// console.log('[RPG Companion] → Matched TIME (emoji format)');
const timeStr = line.replace('🕒:', '').trim(); const timeStr = line.replace('🕒:', '').trim();
if (isValidParsedValue(timeStr)) { if (isValidParsedValue(timeStr)) {
data.time = timeStr; data.time = timeStr;
@@ -243,7 +231,6 @@ export function renderInfoBox() {
} }
} else if (line.startsWith('Location:')) { } else if (line.startsWith('Location:')) {
if (!parsedFields.location) { if (!parsedFields.location) {
// console.log('[RPG Companion] → Matched LOCATION (text format)');
const locStr = line.replace('Location:', '').trim(); const locStr = line.replace('Location:', '').trim();
if (isValidParsedValue(locStr)) { if (isValidParsedValue(locStr)) {
data.location = locStr; data.location = locStr;
@@ -252,7 +239,6 @@ export function renderInfoBox() {
} }
} else if (line.includes('🗺️:')) { } else if (line.includes('🗺️:')) {
if (!parsedFields.location) { if (!parsedFields.location) {
// console.log('[RPG Companion] → Matched LOCATION (emoji format)');
const locStr = line.replace('🗺️:', '').trim(); const locStr = line.replace('🗺️:', '').trim();
if (isValidParsedValue(locStr)) { if (isValidParsedValue(locStr)) {
data.location = locStr; data.location = locStr;
@@ -297,45 +283,22 @@ export function renderInfoBox() {
const notDivider = !line.includes('---'); const notDivider = !line.includes('---');
const notCodeFence = !line.trim().startsWith('```'); const notCodeFence = !line.trim().startsWith('```');
// console.log('[RPG Companion] → Checking weather conditions:', {
// line: line,
// hasColon: hasColon,
// notInfoBox: notInfoBox,
// notDivider: notDivider
// });
if (hasColon && notInfoBox && notDivider && notCodeFence && line.trim().length > 0) { if (hasColon && notInfoBox && notDivider && notCodeFence && line.trim().length > 0) {
// Match format: [Weather Emoji]: [Forecast]
// Capture everything before colon as emoji, everything after as forecast
// console.log('[RPG Companion] → Testing WEATHER match for:', line);
const weatherMatch = line.match(/^\s*([^:]+):\s*(.+)$/); const weatherMatch = line.match(/^\s*([^:]+):\s*(.+)$/);
if (weatherMatch) { if (weatherMatch) {
const potentialEmoji = weatherMatch[1].trim(); const potentialEmoji = weatherMatch[1].trim();
const forecast = weatherMatch[2].trim(); const forecast = weatherMatch[2].trim();
// If the first part is short (likely emoji), treat as weather
if (potentialEmoji.length <= 5) { if (potentialEmoji.length <= 5) {
data.weatherEmoji = potentialEmoji; data.weatherEmoji = potentialEmoji;
data.weatherForecast = forecast; data.weatherForecast = forecast;
parsedFields.weather = true; parsedFields.weather = true;
// console.log('[RPG Companion] ✓ Weather parsed:', data.weatherEmoji, data.weatherForecast);
} else {
// console.log('[RPG Companion] ✗ First part too long for emoji:', potentialEmoji);
} }
} else {
// console.log('[RPG Companion] ✗ Weather regex did not match');
} }
} else {
// console.log('[RPG Companion] → No match for this line');
} }
} }
} }
} }
// console.log('[RPG Companion] Parsed Info Box data:', {
// date: data.date,
// weatherEmoji: data.weatherEmoji,
// weatherForecast: data.weatherForecast,
// temperature: data.temperature, // temperature: data.temperature,
// timeStart: data.timeStart, // timeStart: data.timeStart,
// location: data.location // location: data.location
@@ -635,14 +598,12 @@ export function updateInfoBoxField(field, value) {
// Reconstruct the Info Box text with updated field // Reconstruct the Info Box text with updated field
const lines = lastGeneratedData.infoBox.split('\n'); const lines = lastGeneratedData.infoBox.split('\n');
let dateLineFound = false; let dateLineFound = false;
let dateLineIndex = -1;
let weatherLineIndex = -1; let weatherLineIndex = -1;
// Find the date line // Find the date line
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('🗓️:') || lines[i].startsWith('Date:')) { if (lines[i].includes('🗓️:') || lines[i].startsWith('Date:')) {
dateLineFound = true; dateLineFound = true;
dateLineIndex = i;
break; break;
} }
} }
@@ -876,7 +837,6 @@ export function updateInfoBoxField(field, value) {
const swipeId = message.swipe_id || 0; const swipeId = message.swipe_id || 0;
if (message.extra.rpg_companion_swipes[swipeId]) { if (message.extra.rpg_companion_swipes[swipeId]) {
message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n'); message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n');
// console.log('[RPG Companion] Updated infoBox in message swipe data');
} }
} }
break; break;
@@ -907,34 +867,57 @@ function updateRecentEvent(field, value) {
}[field]; }[field];
if (eventIndex !== undefined) { if (eventIndex !== undefined) {
// Parse current infoBox to get existing events // Get existing events - prioritize structured data (same logic as renderInfoBox)
const lines = (committedTrackerData.infoBox || '').split('\n');
let recentEvents = []; let recentEvents = [];
// Find existing Recent Events line // First check structured infoBoxData (from JSON parsing)
const recentEventsLine = lines.find(line => line.startsWith('Recent Events:')); if (extensionSettings.infoBoxData?.recentEvents) {
if (recentEventsLine) { const events = extensionSettings.infoBoxData.recentEvents;
const eventsString = recentEventsLine.replace('Recent Events:', '').trim(); if (Array.isArray(events)) {
if (eventsString) { // Get all valid events, preserving order (max 3)
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e); recentEvents = events.filter(e => e && e !== 'null').slice(0, 3);
} else if (typeof events === 'string' && events !== 'null') {
recentEvents = [events];
}
}
// Fallback to text format from committedTrackerData
if (recentEvents.length === 0 && committedTrackerData.infoBox) {
const lines = (committedTrackerData.infoBox || '').split('\n');
const recentEventsLine = lines.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).slice(0, 3);
}
} }
} }
// Ensure array has enough slots // Filter out placeholder text - treat it as empty
const placeholderText = i18n.getTranslation('infobox.recentEvents.addEventPlaceholder');
const cleanedValue = (value === placeholderText || value === 'Add event...' || value === 'Click to add event') ? '' : value.trim();
// Update the specific event in the array
// Ensure array has enough slots for the index we're updating
while (recentEvents.length <= eventIndex) { while (recentEvents.length <= eventIndex) {
recentEvents.push(''); recentEvents.push('');
} }
// Update the specific event // Update the specific event
recentEvents[eventIndex] = value; recentEvents[eventIndex] = cleanedValue;
// Filter out empty events and rebuild the line // Filter out empty events for final storage (but preserve order of non-empty ones)
const validEvents = recentEvents.filter(e => e && e.trim()); const validEvents = recentEvents.filter(e => e && e.trim());
const newRecentEventsLine = validEvents.length > 0 const newRecentEventsLine = validEvents.length > 0
? `Recent Events: ${validEvents.join(', ')}` ? `Recent Events: ${validEvents.join(', ')}`
: ''; : '';
// Update infoBox with new Recent Events line // Update infoBox with new Recent Events line
// Need to get lines from committedTrackerData if we haven't already
let lines = [];
if (committedTrackerData.infoBox) {
lines = committedTrackerData.infoBox.split('\n');
}
const updatedLines = lines.filter(line => !line.startsWith('Recent Events:')); const updatedLines = lines.filter(line => !line.startsWith('Recent Events:'));
if (newRecentEventsLine) { if (newRecentEventsLine) {
// Add Recent Events line at the end (before any empty lines) // Add Recent Events line at the end (before any empty lines)
@@ -951,6 +934,14 @@ function updateRecentEvent(field, value) {
committedTrackerData.infoBox = updatedLines.join('\n'); committedTrackerData.infoBox = updatedLines.join('\n');
lastGeneratedData.infoBox = updatedLines.join('\n'); lastGeneratedData.infoBox = updatedLines.join('\n');
// Also update the structured data to keep it in sync
// Store only valid events (renderInfoBox will handle showing placeholders for empty slots)
// This prevents renderInfoBox() from using stale structured data
if (!extensionSettings.infoBoxData) {
extensionSettings.infoBoxData = {};
}
extensionSettings.infoBoxData.recentEvents = validEvents;
// Update the message's swipe data // Update the message's swipe data
const chat = getContext().chat; const chat = getContext().chat;
if (chat && chat.length > 0) { if (chat && chat.length > 0) {
+3 -13
View File
@@ -8,7 +8,7 @@ import { getInventoryRenderOptions, restoreFormStates } from '../interaction/inv
import { updateInventoryItem } from '../interaction/inventoryEdit.js'; import { updateInventoryItem } from '../interaction/inventoryEdit.js';
import { parseItems } from '../../utils/itemParser.js'; import { parseItems } from '../../utils/itemParser.js';
import { i18n } from '../../core/i18n.js'; import { i18n } from '../../core/i18n.js';
import { itemHasLinkedSkills, navigateToLinkedSkills } from './skills.js'; import { itemHasLinkedSkills } from './skills.js';
// Type imports // Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
@@ -443,18 +443,9 @@ function generateInventoryHTML(inventory, options = {}) {
collapsedLocations = [] collapsedLocations = []
} = options; } = options;
// Handle legacy v1 format - convert to v2 for display
let v2Inventory = inventory;
if (typeof inventory === 'string') {
v2Inventory = {
version: 2,
onPerson: inventory,
stored: {},
assets: 'None'
};
}
// Ensure v2 structure has all required fields // Ensure v2 structure has all required fields
// Note: Migration functions handle v1→v2 conversion on load, so inventory should always be v2 here
let v2Inventory = inventory;
if (!v2Inventory || typeof v2Inventory !== 'object') { if (!v2Inventory || typeof v2Inventory !== 'object') {
v2Inventory = { v2Inventory = {
version: 2, version: 2,
@@ -686,7 +677,6 @@ export function renderInventory() {
$inventoryContainer.html(html); $inventoryContainer.html(html);
// Restore form states after re-rendering (fixes Bug #1)
restoreFormStates(); restoreFormStates();
// Event listener for editing item names (mobile-friendly contenteditable) // Event listener for editing item names (mobile-friendly contenteditable)
+111 -180
View File
@@ -1,10 +1,11 @@
/** /**
* Quests Rendering Module * Quests Rendering Module
* Handles UI rendering for quests system (main and optional quests) * Handles UI rendering for quests system (main and optional quests)
* Uses the same structure and styling as items/skills
*/ */
import { extensionSettings, $questsContainer } from '../../core/state.js'; import { extensionSettings, $questsContainer } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js'; import { saveSettings, saveChatData } from '../../core/persistence.js';
import { i18n } from '../../core/i18n.js'; import { i18n } from '../../core/i18n.js';
/** /**
@@ -19,45 +20,22 @@ function escapeHtml(text) {
} }
/** /**
* Checks if we have structured quests data (v2 format with name + description) * Gets the main quest (migration handles legacy format conversion)
* @returns {boolean}
*/
function hasStructuredQuests() {
const q = extensionSettings.questsV2;
return q && (q.main !== undefined || q.optional !== undefined);
}
/**
* Gets the main quest (supports both legacy and structured format)
* @returns {{name: string, description: string}|null} * @returns {{name: string, description: string}|null}
*/ */
function getMainQuest() { function getMainQuest() {
if (hasStructuredQuests() && extensionSettings.questsV2.main) { if (extensionSettings.questsV2?.main) {
return extensionSettings.questsV2.main; return extensionSettings.questsV2.main;
} }
// Legacy format
const title = extensionSettings.quests?.main;
if (title && title !== 'None') {
return { name: title, description: extensionSettings.quests?.mainDescription || '' };
}
return null; return null;
} }
/** /**
* Gets optional quests (supports both legacy and structured format) * Gets optional quests (migration handles legacy format conversion)
* @returns {Array<{name: string, description: string}>} * @returns {Array<{name: string, description: string}>}
*/ */
function getOptionalQuests() { function getOptionalQuests() {
if (hasStructuredQuests() && extensionSettings.questsV2.optional) { return extensionSettings.questsV2?.optional || [];
return extensionSettings.questsV2.optional;
}
// Legacy format
const titles = extensionSettings.quests?.optional || [];
const descriptions = extensionSettings.quests?.optionalDescriptions || [];
return titles.map((title, i) => ({
name: title,
description: descriptions[i] || ''
}));
} }
/** /**
@@ -79,94 +57,90 @@ export function renderQuestsSubTabs(activeTab = 'main') {
} }
/** /**
* Renders the main quest view * Renders the main quest view (matches items/skills structure)
* @param {string} mainQuest - Current main quest title (legacy param, ignored if structured)
* @returns {string} HTML for main quest view * @returns {string} HTML for main quest view
*/ */
export function renderMainQuestView(mainQuest) { export function renderMainQuestView() {
// Use structured data helpers
const quest = getMainQuest(); const quest = getMainQuest();
const hasQuest = quest !== null; const hasQuest = quest !== null;
const questName = quest?.name || ''; const questName = quest?.name || '';
const questDesc = quest?.description || ''; const questDesc = quest?.description || '';
// Track if add form is open
const isFormOpen = openAddForms?.main || false;
let itemsHtml = '';
if (hasQuest) {
// Render quest as item (list view style, matching items/skills)
itemsHtml = `
<div class="rpg-item-row" data-field="main">
<div class="rpg-item-main-row">
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="main" data-prop="name" title="Click to edit">${escapeHtml(questName)}</span>
<button class="rpg-item-remove" data-action="remove-quest" data-field="main" title="Complete/Remove quest">
<i class="fa-solid fa-times"></i>
</button>
</div>
<div class="rpg-item-desc-row">
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="main" data-prop="description" title="Click to edit description">${escapeHtml(questDesc)}</span>
</div>
</div>
`;
}
return ` return `
<div class="rpg-quest-section"> <div class="rpg-quest-section">
<div class="rpg-quest-header"> <div class="rpg-quest-header">
<h3 class="rpg-quest-section-title" data-i18n-key="quests.main.title">${i18n.getTranslation('quests.main.title')}</h3> <h3 class="rpg-quest-section-title" data-i18n-key="quests.main.title">${i18n.getTranslation('quests.main.title')}</h3>
${!hasQuest ? `<button class="rpg-add-quest-btn" data-action="add-quest" data-field="main" title="${i18n.getTranslation('quests.main.addQuestTitle')}"> <button class="rpg-inventory-add-btn" data-action="add-quest" data-field="main" title="${i18n.getTranslation('quests.main.addQuestTitle')}">
<i class="fa-solid fa-plus"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span> <i class="fa-solid fa-plus"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
</button>` : ''} </button>
</div> </div>
<div class="rpg-quest-content"> <div class="rpg-quest-content">
${hasQuest ? ` <div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: ${isFormOpen ? 'flex' : 'none'};">
<div class="rpg-inline-form" id="rpg-edit-quest-form-main" style="display: none;"> <input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="${i18n.getTranslation('quests.main.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.main.addQuestPlaceholder" />
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-main" value="${escapeHtml(questName)}" placeholder="Quest name" /> <div class="rpg-inline-buttons">
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-desc-main" value="${escapeHtml(questDesc)}" placeholder="Description (optional)" /> <button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
<div class="rpg-inline-buttons"> <i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-edit-quest" data-field="main"> </button>
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span> <button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="main">
</button> <i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-edit-quest" data-field="main"> </button>
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.save">${i18n.getTranslation('global.save')}</span>
</button>
</div>
</div> </div>
<div class="rpg-quest-item" data-field="main"> </div>
<div class="rpg-quest-title">${escapeHtml(questName)}</div> <div class="rpg-item-list rpg-item-list-view">
<div class="rpg-quest-actions"> ${itemsHtml || `<div class="rpg-inventory-empty" data-i18n-key="quests.main.empty">${i18n.getTranslation('quests.main.empty')}</div>`}
<button class="rpg-quest-edit" data-action="edit-quest" data-field="main" title="Edit quest"> </div>
<i class="fa-solid fa-edit"></i>
</button>
<button class="rpg-quest-remove" data-action="remove-quest" data-field="main" title="Complete/Remove quest">
<i class="fa-solid fa-check"></i>
</button>
</div>
</div>
` : `
<div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: none;">
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="${i18n.getTranslation('quests.main.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.main.addQuestPlaceholder" />
<div class="rpg-inline-actions">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
<i class="fa-solid fa-times"></i> <span data-i18n-key="global.cancel">${i18n.getTranslation('global.cancel')}</span>
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="main">
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
</button>
</div>
</div>
<div class="rpg-quest-empty" data-i18n-key="quests.main.empty">${i18n.getTranslation('quests.main.empty')}</div>
`}
</div>
<div class="rpg-quest-hint">
<i class="fa-solid fa-lightbulb"></i>
<span data-i18n-key="quests.main.hint">${i18n.getTranslation('quests.main.hint')}</span>
</div> </div>
</div> </div>
`; `;
} }
/** /**
* Renders the optional quests view * Renders the optional quests view (matches items/skills structure)
* @param {string[]} optionalQuests - Array of optional quest titles
* @returns {string} HTML for optional quests view * @returns {string} HTML for optional quests view
*/ */
export function renderOptionalQuestsView(optionalQuests) { export function renderOptionalQuestsView() {
// Use structured data helpers
const quests = getOptionalQuests().filter(q => q && q.name && q.name !== 'None'); const quests = getOptionalQuests().filter(q => q && q.name && q.name !== 'None');
// Track if add form is open
const isFormOpen = openAddForms?.optional || false;
let questsHtml = ''; let itemsHtml = '';
if (quests.length === 0) { if (quests.length === 0) {
questsHtml = `<div class="rpg-quest-empty" data-i18n-key="quests.optional.empty">${i18n.getTranslation('quests.optional.empty')}</div>`; itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="quests.optional.empty">${i18n.getTranslation('quests.optional.empty')}</div>`;
} else { } else {
questsHtml = quests.map((quest, index) => ` // Render quests as items (list view style, matching items/skills)
<div class="rpg-quest-item" data-field="optional" data-index="${index}"> itemsHtml = quests.map((quest, index) => `
<div class="rpg-quest-title rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" title="Click to edit">${escapeHtml(quest.name)}</div> <div class="rpg-item-row" data-field="optional" data-index="${index}">
<div class="rpg-quest-actions"> <div class="rpg-item-main-row">
<button class="rpg-quest-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="Complete/Remove quest"> <span class="rpg-item-name rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" data-prop="name" title="Click to edit">${escapeHtml(quest.name)}</span>
<i class="fa-solid fa-check"></i> <button class="rpg-item-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="Complete/Remove quest">
<i class="fa-solid fa-times"></i>
</button> </button>
</div> </div>
<div class="rpg-item-desc-row">
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(quest.description || '')}</span>
</div>
</div> </div>
`).join(''); `).join('');
} }
@@ -175,12 +149,12 @@ export function renderOptionalQuestsView(optionalQuests) {
<div class="rpg-quest-section"> <div class="rpg-quest-section">
<div class="rpg-quest-header"> <div class="rpg-quest-header">
<h3 class="rpg-quest-section-title" data-i18n-key="quests.optional.title">${i18n.getTranslation('quests.optional.title')}</h3> <h3 class="rpg-quest-section-title" data-i18n-key="quests.optional.title">${i18n.getTranslation('quests.optional.title')}</h3>
<button class="rpg-add-quest-btn" data-action="add-quest" data-field="optional" title="${i18n.getTranslation('quests.optional.addQuestTitle')}"> <button class="rpg-inventory-add-btn" data-action="add-quest" data-field="optional" title="${i18n.getTranslation('quests.optional.addQuestTitle')}">
<i class="fa-solid fa-plus"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span> <i class="fa-solid fa-plus"></i> <span data-i18n-key="global.add">${i18n.getTranslation('global.add')}</span>
</button> </button>
</div> </div>
<div class="rpg-quest-content"> <div class="rpg-quest-content">
<div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: none;"> <div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: ${isFormOpen ? 'flex' : 'none'};">
<input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="${i18n.getTranslation('quests.optional.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.optional.addQuestPlaceholder" /> <input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="${i18n.getTranslation('quests.optional.addQuestPlaceholder')}" data-i18n-placeholder-key="quests.optional.addQuestPlaceholder" />
<div class="rpg-inline-buttons"> <div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="optional"> <button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="optional">
@@ -191,18 +165,17 @@ export function renderOptionalQuestsView(optionalQuests) {
</button> </button>
</div> </div>
</div> </div>
<div class="rpg-quest-list"> <div class="rpg-item-list rpg-item-list-view">
${questsHtml} ${itemsHtml}
</div>
<div class="rpg-quest-hint">
<i class="fa-solid fa-info-circle"></i>
<span data-i18n-key="quests.optional.hint">${i18n.getTranslation('quests.optional.hint')}</span>
</div> </div>
</div> </div>
</div> </div>
`; `;
} }
// Track open add forms (matching items/skills pattern)
let openAddForms = {};
/** /**
* Main render function for quests * Main render function for quests
*/ */
@@ -214,10 +187,6 @@ export function renderQuests() {
// Get current sub-tab from container or default to 'main' // Get current sub-tab from container or default to 'main'
const activeSubTab = $questsContainer.data('active-subtab') || 'main'; const activeSubTab = $questsContainer.data('active-subtab') || 'main';
// Get quests data
const mainQuest = extensionSettings.quests.main || 'None';
const optionalQuests = extensionSettings.quests.optional || [];
// Build HTML // Build HTML
let html = '<div class="rpg-quests-wrapper">'; let html = '<div class="rpg-quests-wrapper">';
html += renderQuestsSubTabs(activeSubTab); html += renderQuestsSubTabs(activeSubTab);
@@ -225,9 +194,9 @@ export function renderQuests() {
// Render active sub-tab // Render active sub-tab
html += '<div class="rpg-quests-panels">'; html += '<div class="rpg-quests-panels">';
if (activeSubTab === 'main') { if (activeSubTab === 'main') {
html += renderMainQuestView(mainQuest); html += renderMainQuestView();
} else { } else {
html += renderOptionalQuestsView(optionalQuests); html += renderOptionalQuestsView();
} }
html += '</div></div>'; html += '</div></div>';
@@ -238,37 +207,39 @@ export function renderQuests() {
} }
/** /**
* Attach event handlers for quest interactions * Attach event handlers for quest interactions (matching items/skills pattern)
*/ */
function attachQuestEventHandlers() { function attachQuestEventHandlers() {
// Sub-tab switching // Sub-tab switching
$questsContainer.find('.rpg-quests-subtab').on('click', function() { $questsContainer.find('.rpg-quests-subtab').off('click').on('click', function() {
const tab = $(this).data('tab'); const tab = $(this).data('tab');
$questsContainer.data('active-subtab', tab); $questsContainer.data('active-subtab', tab);
renderQuests(); renderQuests();
}); });
// Add quest button // Add quest button
$questsContainer.find('[data-action="add-quest"]').on('click', function() { $questsContainer.find('[data-action="add-quest"]').off('click').on('click', function() {
const field = $(this).data('field'); const field = $(this).data('field');
$(`#rpg-add-quest-form-${field}`).show(); openAddForms[field] = true;
$(`#rpg-new-quest-${field}`).focus(); renderQuests();
setTimeout(() => {
$(`#rpg-new-quest-${field}`).focus();
}, 50);
}); });
// Cancel add quest // Cancel add quest
$questsContainer.find('[data-action="cancel-add-quest"]').on('click', function() { $questsContainer.find('[data-action="cancel-add-quest"]').off('click').on('click', function() {
const field = $(this).data('field'); const field = $(this).data('field');
$(`#rpg-add-quest-form-${field}`).hide(); openAddForms[field] = false;
$(`#rpg-new-quest-${field}`).val(''); $(`#rpg-new-quest-${field}`).val('');
renderQuests();
}); });
// Save add quest // Save add quest
$questsContainer.find('[data-action="save-add-quest"]').on('click', function() { $questsContainer.find('[data-action="save-add-quest"]').off('click').on('click', function() {
const field = $(this).data('field'); const field = $(this).data('field');
const nameInput = $(`#rpg-new-quest-${field}`); const nameInput = $(`#rpg-new-quest-${field}`);
const descInput = $(`#rpg-new-quest-desc-${field}`);
const questTitle = nameInput.val().trim(); const questTitle = nameInput.val().trim();
const questDesc = descInput?.val()?.trim() || '';
if (questTitle) { if (questTitle) {
// Ensure structured format exists // Ensure structured format exists
@@ -277,117 +248,77 @@ function attachQuestEventHandlers() {
} }
if (field === 'main') { if (field === 'main') {
extensionSettings.quests.main = questTitle; extensionSettings.questsV2.main = { name: questTitle, description: '' };
extensionSettings.questsV2.main = { name: questTitle, description: questDesc };
} else { } else {
if (!extensionSettings.quests.optional) {
extensionSettings.quests.optional = [];
}
if (!extensionSettings.questsV2.optional) { if (!extensionSettings.questsV2.optional) {
extensionSettings.questsV2.optional = []; extensionSettings.questsV2.optional = [];
} }
extensionSettings.quests.optional.push(questTitle); extensionSettings.questsV2.optional.push({ name: questTitle, description: '' });
extensionSettings.questsV2.optional.push({ name: questTitle, description: questDesc });
} }
openAddForms[field] = false;
saveSettings(); saveSettings();
renderQuests(); saveChatData();
}
});
// Edit quest (main only)
$questsContainer.find('[data-action="edit-quest"]').on('click', function() {
const field = $(this).data('field');
$(`#rpg-edit-quest-form-${field}`).show();
$('.rpg-quest-item[data-field="main"]').hide();
$(`#rpg-edit-quest-${field}`).focus();
});
// Cancel edit quest
$questsContainer.find('[data-action="cancel-edit-quest"]').on('click', function() {
const field = $(this).data('field');
$(`#rpg-edit-quest-form-${field}`).hide();
$('.rpg-quest-item[data-field="main"]').show();
});
// Save edit quest (main)
$questsContainer.find('[data-action="save-edit-quest"]').on('click', function() {
const field = $(this).data('field');
const nameInput = $(`#rpg-edit-quest-${field}`);
const descInput = $(`#rpg-edit-quest-desc-${field}`);
const questTitle = nameInput.val().trim();
const questDesc = descInput.val()?.trim() || '';
if (questTitle) {
// Use structured format
if (!extensionSettings.questsV2) {
extensionSettings.questsV2 = { main: null, optional: [] };
}
extensionSettings.questsV2.main = { name: questTitle, description: questDesc };
// Also update legacy for backwards compatibility
extensionSettings.quests.main = questTitle;
saveSettings();
renderQuests(); renderQuests();
} }
}); });
// Remove quest // Remove quest
$questsContainer.find('[data-action="remove-quest"]').on('click', function() { $questsContainer.find('[data-action="remove-quest"]').off('click').on('click', function() {
const field = $(this).data('field'); const field = $(this).data('field');
const index = $(this).data('index'); const index = $(this).data('index');
if (field === 'main') { if (field === 'main') {
extensionSettings.quests.main = 'None';
if (extensionSettings.questsV2) { if (extensionSettings.questsV2) {
extensionSettings.questsV2.main = null; extensionSettings.questsV2.main = null;
} }
} else { } else {
extensionSettings.quests.optional.splice(index, 1);
if (extensionSettings.questsV2?.optional) { if (extensionSettings.questsV2?.optional) {
extensionSettings.questsV2.optional.splice(index, 1); extensionSettings.questsV2.optional.splice(index, 1);
} }
} }
saveSettings(); saveSettings();
saveChatData();
renderQuests(); renderQuests();
}); });
// Inline editing for optional quests (name and description) // Inline editing for quests (name and description) - matching items/skills pattern
$questsContainer.find('.rpg-quest-title.rpg-editable, .rpg-quest-description.rpg-editable').on('blur', function() { $questsContainer.off('blur', '.rpg-item-name.rpg-editable, .rpg-item-description.rpg-editable')
.on('blur', '.rpg-item-name.rpg-editable, .rpg-item-description.rpg-editable', function() {
const $this = $(this); const $this = $(this);
const field = $this.data('field'); const field = $this.data('field');
const index = $this.data('index'); const index = $this.data('index');
const prop = $this.data('prop') || 'name'; const prop = $this.data('prop') || 'name';
const newValue = $this.text().trim(); const newValue = $this.text().trim();
if (field === 'optional' && index !== undefined) { // Ensure structured format exists
// Ensure structured format exists if (!extensionSettings.questsV2) {
if (!extensionSettings.questsV2) { extensionSettings.questsV2 = { main: null, optional: [] };
extensionSettings.questsV2 = { main: null, optional: [] }; }
if (field === 'main') {
// Update main quest
if (!extensionSettings.questsV2.main) {
extensionSettings.questsV2.main = { name: '', description: '' };
} }
extensionSettings.questsV2.main[prop] = newValue;
} else if (field === 'optional' && index !== undefined) {
// Update optional quest
if (!extensionSettings.questsV2.optional[index]) { if (!extensionSettings.questsV2.optional[index]) {
extensionSettings.questsV2.optional[index] = { name: '', description: '' }; extensionSettings.questsV2.optional[index] = { name: '', description: '' };
} }
extensionSettings.questsV2.optional[index][prop] = newValue; extensionSettings.questsV2.optional[index][prop] = newValue;
// Also update legacy for backwards compatibility
if (prop === 'name') {
extensionSettings.quests.optional[index] = newValue;
}
saveSettings();
} }
saveSettings();
saveChatData();
}); });
// Enter key to save in forms // Enter key to save in forms (matching items/skills pattern)
$questsContainer.find('.rpg-inline-input').on('keypress', function(e) { $questsContainer.find('.rpg-inline-input').off('keypress').on('keypress', function(e) {
if (e.which === 13) { if (e.which === 13) {
const field = $(this).attr('id').includes('edit') ? const field = $(this).attr('id').replace('rpg-new-quest-', '');
$(this).attr('id').replace('rpg-edit-quest-', '') : $(`[data-action="save-add-quest"][data-field="${field}"]`).click();
$(this).attr('id').replace('rpg-new-quest-', '');
if ($(this).attr('id').includes('edit')) {
$(`[data-action="save-edit-quest"][data-field="${field}"]`).click();
} else {
$(`[data-action="save-add-quest"][data-field="${field}"]`).click();
}
} }
}); });
} }
+3 -3
View File
@@ -37,10 +37,10 @@ function serializeItems(items) {
*/ */
export function getSkillCategories() { export function getSkillCategories() {
const categories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || []; const categories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
// Handle both old format (string array) and new format (object array) // Migration function handles string array → object array conversion on load
return categories return categories
.filter(cat => typeof cat === 'string' || cat.enabled !== false) .filter(cat => cat.enabled !== false)
.map(cat => typeof cat === 'string' ? cat : cat.name); .map(cat => cat.name);
} }
/** /**
-63
View File
@@ -101,45 +101,6 @@ function namesMatch(cardName, aiName) {
return wordBoundary.test(aiCore); return wordBoundary.test(aiCore);
} }
/**
* Gets relationship emoji from relationship string
* Returns a default emoji () if relationship is not in the predefined map
*/
function getRelationshipEmoji(relationship) {
if (!relationship) return null;
const map = {
'Enemy': '⚔️',
'Neutral': '⚖️',
'Friend': '⭐',
'Lover': '❤️',
'Ally': '🤝',
'Rival': '🎯',
'Family': '👨‍👩‍👧',
'Stranger': '❓'
};
// Return mapped emoji or default '⚖️' for unknown relationships
return map[relationship] || '⚖️';
}
/**
* Gets character avatar URL
*/
function getCharacterAvatarUrl(characterName) {
// Try to find matching character from SillyTavern
try {
const context = getContext();
if (context && characters) {
const char = characters.find(c => namesMatch(c.name, characterName));
if (char && char.avatar) {
return getSafeThumbnailUrl('avatar', char.avatar);
}
}
} catch (e) {
debugLog('[RPG Thoughts] Error getting avatar:', e);
}
return FALLBACK_AVATAR_DATA_URI;
}
export function renderThoughts() { export function renderThoughts() {
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) { if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
return; return;
@@ -818,11 +779,9 @@ export function removeCharacter(characterName) {
} }
const lines = lastGeneratedData.characterThoughts.split('\n'); const lines = lastGeneratedData.characterThoughts.split('\n');
const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
let characterFound = false; let characterFound = false;
let inTargetCharacter = false; let inTargetCharacter = false;
let characterStartIndex = -1;
let characterEndIndex = -1; let characterEndIndex = -1;
const linesToRemove = []; const linesToRemove = [];
@@ -835,7 +794,6 @@ export function removeCharacter(characterName) {
if (name.toLowerCase() === characterName.toLowerCase()) { if (name.toLowerCase() === characterName.toLowerCase()) {
characterFound = true; characterFound = true;
inTargetCharacter = true; inTargetCharacter = true;
characterStartIndex = i;
linesToRemove.push(i); linesToRemove.push(i);
} else if (inTargetCharacter) { } else if (inTargetCharacter) {
characterEndIndex = i; characterEndIndex = i;
@@ -902,12 +860,6 @@ export function removeCharacter(characterName) {
* Creates floating thought bubbles positioned near character avatars. * Creates floating thought bubbles positioned near character avatars.
*/ */
export function updateChatThoughts() { export function updateChatThoughts() {
// console.log('[RPG Companion] ======== updateChatThoughts called ========');
// console.log('[RPG Companion] Extension enabled:', extensionSettings.enabled);
// console.log('[RPG Companion] showThoughtsInChat setting:', extensionSettings.showThoughtsInChat);
// console.log('[RPG Companion] Toggle element checked:', $('#rpg-toggle-thoughts-in-chat').prop('checked'));
// console.log('[RPG Companion] lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts);
// Remove existing thought panel and icon // Remove existing thought panel and icon
$('#rpg-thought-panel').remove(); $('#rpg-thought-panel').remove();
$('#rpg-thought-icon').remove(); $('#rpg-thought-icon').remove();
@@ -915,9 +867,7 @@ export function updateChatThoughts() {
$(window).off('resize.thoughtPanel'); $(window).off('resize.thoughtPanel');
$(document).off('click.thoughtPanel'); $(document).off('click.thoughtPanel');
// If extension is disabled, thoughts in chat are disabled, or no thoughts, just return
if (!extensionSettings.enabled || !extensionSettings.showThoughtsInChat || !lastGeneratedData.characterThoughts) { if (!extensionSettings.enabled || !extensionSettings.showThoughtsInChat || !lastGeneratedData.characterThoughts) {
// console.log('[RPG Companion] Thoughts in chat disabled or no data');
return; return;
} }
@@ -927,8 +877,6 @@ export function updateChatThoughts() {
const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts; const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts;
const thoughtsLabel = thoughtsConfig?.name || 'Thoughts'; const thoughtsLabel = thoughtsConfig?.name || 'Thoughts';
// console.log('[RPG Companion] Parsing thoughts from lines:', lines);
// Parse new format to build character map and thoughts // Parse new format to build character map and thoughts
let currentCharName = null; let currentCharName = null;
let currentCharEmoji = null; let currentCharEmoji = null;
@@ -985,13 +933,9 @@ export function updateChatThoughts() {
// If no thoughts parsed, return // If no thoughts parsed, return
if (thoughtsArray.length === 0) { if (thoughtsArray.length === 0) {
// console.log('[RPG Companion] No thoughts parsed, returning');
return; return;
} }
// console.log('[RPG Companion] Total thoughts:', thoughtsArray.length);
// console.log('[RPG Companion] Thoughts array:', thoughtsArray);
// Find the last message to position near // Find the last message to position near
const $messages = $('#chat .mes'); const $messages = $('#chat .mes');
let $targetMessage = null; let $targetMessage = null;
@@ -1006,7 +950,6 @@ export function updateChatThoughts() {
} }
if (!$targetMessage) { if (!$targetMessage) {
// console.log('[RPG Companion] No target message found');
return; return;
} }
@@ -1026,10 +969,8 @@ export function createThoughtPanel($message, thoughtsArray) {
$('#rpg-thought-panel').remove(); $('#rpg-thought-panel').remove();
$('#rpg-thought-icon').remove(); $('#rpg-thought-icon').remove();
// Get the avatar position from the message
const $avatar = $message.find('.avatar img'); const $avatar = $message.find('.avatar img');
if (!$avatar.length) { if (!$avatar.length) {
// console.log('[RPG Companion] No avatar found in message');
return; return;
} }
@@ -1226,14 +1167,10 @@ export function createThoughtPanel($message, thoughtsArray) {
}); });
} }
// console.log('[RPG Companion] Thought panel created at:', { top, left });
// Add event handlers for editable thoughts in the bubble
$thoughtPanel.find('.rpg-editable').on('blur', function() { $thoughtPanel.find('.rpg-editable').on('blur', function() {
const character = $(this).data('character'); const character = $(this).data('character');
const field = $(this).data('field'); const field = $(this).data('field');
const value = $(this).text().trim(); const value = $(this).text().trim();
// console.log('[RPG Companion] 💭 Thought bubble blur event - character:', character, 'field:', field, 'value:', value);
updateCharacterField(character, field, value); updateCharacterField(character, field, value);
}); });
+12 -16
View File
@@ -317,23 +317,19 @@ export function renderUserStats() {
const field = $(this).data('field'); const field = $(this).data('field');
const value = $(this).text().trim().replace(':', ''); const value = $(this).text().trim().replace(':', '');
if (!extensionSettings.statNames) { // Update the stat name in customStats array (new format)
extensionSettings.statNames = { const config = extensionSettings.trackerConfig?.userStats;
health: 'Health', if (config && config.customStats) {
satiety: 'Satiety', const stat = config.customStats.find(s => s.id === field);
energy: 'Energy', if (stat && value) {
hygiene: 'Hygiene', stat.name = value;
arousal: 'Arousal' saveSettings();
}; saveChatData();
// Re-render to update the display
renderUserStats();
}
} }
extensionSettings.statNames[field] = value || extensionSettings.statNames[field];
saveSettings();
saveChatData();
// Re-render to update the display
renderUserStats();
}); });
// Add event listener for level editing // Add event listener for level editing
-4
View File
@@ -727,7 +727,6 @@ export function removeMobileTabs() {
*/ */
export function setupMobileKeyboardHandling() { export function setupMobileKeyboardHandling() {
if (!window.visualViewport) { if (!window.visualViewport) {
// console.log('[RPG Mobile] Visual Viewport API not supported');
return; return;
} }
@@ -750,12 +749,9 @@ export function setupMobileKeyboardHandling() {
// Keyboard just appeared // Keyboard just appeared
keyboardVisible = true; keyboardVisible = true;
$panel.addClass('rpg-keyboard-visible'); $panel.addClass('rpg-keyboard-visible');
// console.log('[RPG Mobile] Keyboard opened');
} else if (!isKeyboardShowing && keyboardVisible) { } else if (!isKeyboardShowing && keyboardVisible) {
// Keyboard just disappeared
keyboardVisible = false; keyboardVisible = false;
$panel.removeClass('rpg-keyboard-visible'); $panel.removeClass('rpg-keyboard-visible');
// console.log('[RPG Mobile] Keyboard closed');
} }
}); });
} }
+1 -4
View File
@@ -369,7 +369,6 @@ export function setupSettingsPopup() {
const message = chat[i]; const message = chat[i];
if (message.extra && message.extra.rpg_companion_swipes) { if (message.extra && message.extra.rpg_companion_swipes) {
delete message.extra.rpg_companion_swipes; delete message.extra.rpg_companion_swipes;
// console.log('[RPG Companion] Cleared swipe data from message at index', i);
} }
} }
} }
@@ -434,9 +433,7 @@ export function setupSettingsPopup() {
updateDiceDisplayCore(); updateDiceDisplayCore();
updateChatThoughts(); // Clear the thought bubble in chat updateChatThoughts(); // Clear the thought bubble in chat
renderQuests(); // Clear and re-render quests UI renderQuests(); // Clear and re-render quests UI
renderSkills(); // Clear and re-render skills UI renderSkills();
// console.log('[RPG Companion] Chat cache cleared');
}); });
return settingsModal; return settingsModal;
+5 -23
View File
@@ -321,13 +321,12 @@ function renderUserStatsTab() {
html += `<label>${i18n.getTranslation('template.trackerEditorModal.userStatsTab.skillsListLabel')}</label>`; html += `<label>${i18n.getTranslation('template.trackerEditorModal.userStatsTab.skillsListLabel')}</label>`;
html += '<div class="rpg-editor-stats-list" id="rpg-editor-skills-list">'; html += '<div class="rpg-editor-stats-list" id="rpg-editor-skills-list">';
// Handle both old format (string array) and new format (object array) // Migration function handles string array → object array conversion on load
const skillFields = config.skillsSection.customFields || []; const skillFields = config.skillsSection.customFields || [];
skillFields.forEach((skill, index) => { skillFields.forEach((skill, index) => {
// Support both old format (string) and new format (object) const skillName = skill.name || '';
const skillName = typeof skill === 'string' ? skill : (skill.name || ''); const skillDesc = skill.description || '';
const skillDesc = typeof skill === 'string' ? '' : (skill.description || ''); const skillEnabled = skill.enabled !== false;
const skillEnabled = typeof skill === 'string' ? true : (skill.enabled !== false);
html += ` html += `
<div class="rpg-editor-stat-item rpg-editor-skill-item" data-index="${index}"> <div class="rpg-editor-stat-item rpg-editor-skill-item" data-index="${index}">
@@ -518,9 +517,9 @@ function setupUserStatsListeners() {
}); });
// Toggle skill category // Toggle skill category
// Migration function handles string array → object array conversion on load
$('.rpg-skill-toggle').off('change').on('change', function() { $('.rpg-skill-toggle').off('change').on('change', function() {
const index = $(this).data('index'); const index = $(this).data('index');
ensureSkillIsObject(index);
extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].enabled = $(this).is(':checked'); extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].enabled = $(this).is(':checked');
saveSettings(); saveSettings();
renderSkills(); renderSkills();
@@ -529,7 +528,6 @@ function setupUserStatsListeners() {
// Rename skill category // Rename skill category
$('.rpg-skill-name').off('blur').on('blur', function() { $('.rpg-skill-name').off('blur').on('blur', function() {
const index = $(this).data('index'); const index = $(this).data('index');
ensureSkillIsObject(index);
extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].name = $(this).val(); extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].name = $(this).val();
saveSettings(); saveSettings();
renderSkills(); renderSkills();
@@ -538,27 +536,11 @@ function setupUserStatsListeners() {
// Update skill description // Update skill description
$('.rpg-skill-desc').off('blur').on('blur', function() { $('.rpg-skill-desc').off('blur').on('blur', function() {
const index = $(this).data('index'); const index = $(this).data('index');
ensureSkillIsObject(index);
extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].description = $(this).val(); extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].description = $(this).val();
saveSettings(); saveSettings();
}); });
} }
/**
* Helper to convert old string-format skill to object format
*/
function ensureSkillIsObject(index) {
const skill = extensionSettings.trackerConfig.userStats.skillsSection.customFields[index];
if (typeof skill === 'string') {
extensionSettings.trackerConfig.userStats.skillsSection.customFields[index] = {
id: 'skill_' + Date.now(),
name: skill,
description: '',
enabled: true
};
}
}
/** /**
* Render Info Box configuration tab * Render Info Box configuration tab
*/ */
-4
View File
@@ -14,9 +14,7 @@ import { getThumbnailUrl } from '../../../../../../script.js';
* @returns {string|null} Thumbnail URL or null if unavailable/error * @returns {string|null} Thumbnail URL or null if unavailable/error
*/ */
export function getSafeThumbnailUrl(type, filename) { export function getSafeThumbnailUrl(type, filename) {
// Return null if no filename provided
if (!filename || filename === 'none') { if (!filename || filename === 'none') {
// console.log(`[RPG Companion] No valid filename provided for ${type} thumbnail`);
return null; return null;
} }
@@ -24,13 +22,11 @@ export function getSafeThumbnailUrl(type, filename) {
// Attempt to get thumbnail URL from SillyTavern API // Attempt to get thumbnail URL from SillyTavern API
const url = getThumbnailUrl(type, filename); const url = getThumbnailUrl(type, filename);
// Validate that we got a string back
if (typeof url !== 'string' || url.trim() === '') { if (typeof url !== 'string' || url.trim() === '') {
console.warn(`[RPG Companion] getThumbnailUrl returned invalid result for ${type}:`, filename); console.warn(`[RPG Companion] getThumbnailUrl returned invalid result for ${type}:`, filename);
return null; return null;
} }
// console.log(`[RPG Companion] Successfully generated ${type} thumbnail URL for: ${filename}`);
return url; return url;
} catch (error) { } catch (error) {
// Log detailed error information for debugging // Log detailed error information for debugging
-9
View File
@@ -27,9 +27,7 @@ const DEFAULT_INVENTORY_V2 = {
* @returns {MigrationResult} Migration result with v2 inventory and metadata * @returns {MigrationResult} Migration result with v2 inventory and metadata
*/ */
export function migrateInventory(inventory) { export function migrateInventory(inventory) {
// Case 1: Already v2 format (has version property and is an object)
if (inventory && typeof inventory === 'object' && inventory.version === 2) { if (inventory && typeof inventory === 'object' && inventory.version === 2) {
// console.log('[RPG Companion Migration] Inventory already v2, no migration needed');
return { return {
inventory: inventory, inventory: inventory,
migrated: false, migrated: false,
@@ -37,9 +35,7 @@ export function migrateInventory(inventory) {
}; };
} }
// Case 2: null or undefined → use defaults
if (inventory === null || inventory === undefined) { if (inventory === null || inventory === undefined) {
// console.log('[RPG Companion Migration] Inventory is null/undefined, using defaults');
return { return {
inventory: { ...DEFAULT_INVENTORY_V2 }, inventory: { ...DEFAULT_INVENTORY_V2 },
migrated: true, migrated: true,
@@ -47,12 +43,9 @@ export function migrateInventory(inventory) {
}; };
} }
// Case 3: v1 string format → migrate to v2
if (typeof inventory === 'string') { if (typeof inventory === 'string') {
// Check if it's an empty/default string
const trimmed = inventory.trim(); const trimmed = inventory.trim();
if (trimmed === '' || trimmed.toLowerCase() === 'none') { if (trimmed === '' || trimmed.toLowerCase() === 'none') {
// console.log('[RPG Companion Migration] Inventory is empty/None, using defaults');
return { return {
inventory: { ...DEFAULT_INVENTORY_V2 }, inventory: { ...DEFAULT_INVENTORY_V2 },
migrated: true, migrated: true,
@@ -60,8 +53,6 @@ export function migrateInventory(inventory) {
}; };
} }
// Non-empty v1 string → migrate to v2.onPerson
// console.log('[RPG Companion Migration] Migrating v1 string to v2.onPerson:', inventory);
return { return {
inventory: { inventory: {
version: 2, version: 2,
+3 -43
View File
@@ -1833,31 +1833,11 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s ease; transition: all 0.2s ease;
width: 100%; /* Ensure cards take full width */ width: 100%; /* Ensure cards take full width */
max-height: clamp(200px, 18vh, 250px);
box-sizing: border-box; /* Include padding and border in width calculation */ box-sizing: border-box; /* Include padding and border in width calculation */
flex-shrink: 0; /* Prevent cards from shrinking */ flex-shrink: 0; /* Prevent cards from shrinking */
overflow: hidden; overflow: visible;
scrollbar-width: thin;
scrollbar-color: var(--rpg-border) transparent;
} }
.rpg-character-card::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.rpg-character-card::-webkit-scrollbar-track {
background: transparent;
}
.rpg-character-card::-webkit-scrollbar-thumb {
background: var(--rpg-border);
border-radius: 2px;
}
.rpg-character-card::-webkit-scrollbar-thumb:hover {
background: var(--rpg-highlight);
}
.rpg-character-card:hover { .rpg-character-card:hover {
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
@@ -1903,37 +1883,17 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
overflow: hidden; overflow: visible;
} }
.rpg-character-info { .rpg-character-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: clamp(3px, 0.5vh, 5px); gap: clamp(3px, 0.5vh, 5px);
overflow-y: auto; overflow: visible;
overflow-x: hidden;
flex: 1; flex: 1;
min-height: 0;
scrollbar-width: thin;
scrollbar-color: var(--rpg-border) transparent;
} }
.rpg-character-info::-webkit-scrollbar {
width: 4px;
}
.rpg-character-info::-webkit-scrollbar-track {
background: transparent;
}
.rpg-character-info::-webkit-scrollbar-thumb {
background: var(--rpg-border);
border-radius: 2px;
}
.rpg-character-info::-webkit-scrollbar-thumb:hover {
background: var(--rpg-highlight);
}
/* Character header with emoji and name */ /* Character header with emoji and name */
.rpg-character-header { .rpg-character-header {
-355
View File
@@ -1,355 +0,0 @@
/**
* RPG Companion Integration Test Helper
*
* This module provides functions for testing JSON parsing and validation
* within SillyTavern. It can be loaded in the browser console or integrated
* into the extension's debug mode.
*
* Usage in browser console:
* 1. Open SillyTavern with RPG Companion enabled
* 2. Open browser dev tools (F12)
* 3. Copy/paste this file's contents into console
* 4. Run: RPGTestHelper.validateLastResponse() or other methods
*/
window.RPGTestHelper = {
/**
* Validates the last generated tracker data against expected JSON structure
*/
validateLastResponse() {
console.log('🔍 Validating last generated tracker data...\n');
// Access extension settings (this assumes RPG Companion is loaded)
const settings = window.extension_settings?.['rpg-companion'];
if (!settings) {
console.error('❌ RPG Companion not found in extension_settings');
return false;
}
const results = {
inventoryV3: this.validateInventory(settings.inventoryV3),
skillsV2: this.validateSkills(settings.skillsV2),
questsV2: this.validateQuests(settings.questsV2),
infoBoxData: this.validateInfoBox(settings.infoBoxData),
charactersData: this.validateCharacters(settings.charactersData)
};
console.log('\n📊 Validation Results:');
Object.entries(results).forEach(([key, valid]) => {
console.log(` ${valid ? '✅' : '❌'} ${key}`);
});
return Object.values(results).every(v => v);
},
/**
* Validates inventory structure
*/
validateInventory(inv) {
console.log('\n📦 Validating Inventory...');
if (!inv) {
console.log(' ⚠️ inventoryV3 is null/undefined');
return true; // Not an error if not populated yet
}
let valid = true;
// Check onPerson array
if (inv.onPerson && !Array.isArray(inv.onPerson)) {
console.log(' ❌ onPerson should be an array');
valid = false;
} else if (inv.onPerson?.length > 0) {
const item = inv.onPerson[0];
if (typeof item !== 'object' || !item.name) {
console.log(' ❌ onPerson items should be objects with name property');
valid = false;
} else {
console.log(` ✅ onPerson: ${inv.onPerson.length} items (e.g., "${item.name}")`);
}
}
// Check stored object
if (inv.stored && typeof inv.stored !== 'object') {
console.log(' ❌ stored should be an object');
valid = false;
} else if (inv.stored) {
const locations = Object.keys(inv.stored);
console.log(` ✅ stored: ${locations.length} locations`);
}
// Check assets array
if (inv.assets && !Array.isArray(inv.assets)) {
console.log(' ❌ assets should be an array');
valid = false;
} else if (inv.assets?.length > 0) {
console.log(` ✅ assets: ${inv.assets.length} items`);
}
// Check simplified array
if (inv.simplified && !Array.isArray(inv.simplified)) {
console.log(' ❌ simplified should be an array');
valid = false;
} else if (inv.simplified?.length > 0) {
console.log(` ✅ simplified: ${inv.simplified.length} items`);
}
return valid;
},
/**
* Validates skills structure
*/
validateSkills(skills) {
console.log('\n⚔️ Validating Skills...');
if (!skills) {
console.log(' ⚠️ skillsV2 is null/undefined');
return true;
}
if (typeof skills !== 'object') {
console.log(' ❌ skillsV2 should be an object');
return false;
}
let valid = true;
for (const [category, abilities] of Object.entries(skills)) {
if (!Array.isArray(abilities)) {
console.log(`${category} should be an array`);
valid = false;
continue;
}
abilities.forEach((ability, i) => {
if (typeof ability !== 'object' || !ability.name) {
console.log(`${category}[${i}] should be an object with name`);
valid = false;
}
});
console.log(`${category}: ${abilities.length} abilities`);
}
return valid;
},
/**
* Validates quests structure
*/
validateQuests(quests) {
console.log('\n📜 Validating Quests...');
if (!quests) {
console.log(' ⚠️ questsV2 is null/undefined');
return true;
}
let valid = true;
if (quests.main !== null && quests.main !== undefined) {
if (typeof quests.main === 'string') {
console.log(` ✅ main: "${quests.main}"`);
} else if (typeof quests.main === 'object' && quests.main.name) {
console.log(` ✅ main: "${quests.main.name}" (structured)`);
} else {
console.log(' ❌ main should be string or {name, description}');
valid = false;
}
}
if (quests.optional) {
if (!Array.isArray(quests.optional)) {
console.log(' ❌ optional should be an array');
valid = false;
} else {
console.log(` ✅ optional: ${quests.optional.length} quests`);
}
}
return valid;
},
/**
* Validates info box structure
*/
validateInfoBox(info) {
console.log('\n📍 Validating Info Box...');
if (!info) {
console.log(' ⚠️ infoBoxData is null/undefined');
return true;
}
const fields = ['date', 'weather', 'temperature', 'time', 'location'];
let valid = true;
fields.forEach(field => {
if (info[field] !== undefined && info[field] !== null) {
if (typeof info[field] !== 'string') {
console.log(`${field} should be a string`);
valid = false;
} else {
console.log(`${field}: "${info[field]}"`);
}
}
});
if (info.recentEvents) {
if (!Array.isArray(info.recentEvents)) {
console.log(' ❌ recentEvents should be an array');
valid = false;
} else {
console.log(` ✅ recentEvents: ${info.recentEvents.length} events`);
}
}
return valid;
},
/**
* Validates characters structure
*/
validateCharacters(chars) {
console.log('\n👥 Validating Characters...');
if (!chars) {
console.log(' ⚠️ charactersData is null/undefined');
return true;
}
if (!Array.isArray(chars)) {
console.log(' ❌ charactersData should be an array');
return false;
}
let valid = true;
chars.forEach((char, i) => {
if (typeof char !== 'object' || !char.name) {
console.log(` ❌ character[${i}] should have name`);
valid = false;
} else {
console.log(`${char.name}: ${char.relationship || 'no relationship'}`);
}
});
return valid;
},
/**
* Tests JSON extraction from a raw response string
*/
testJSONExtraction(responseText) {
console.log('\n🔬 Testing JSON Extraction...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = responseText.match(jsonRegex);
if (!match) {
console.log('❌ No JSON code block found');
return null;
}
console.log('✅ Found JSON code block');
try {
const parsed = JSON.parse(match[1].trim());
console.log('✅ JSON parsed successfully');
console.log('📋 Structure:', Object.keys(parsed).join(', '));
return parsed;
} catch (e) {
console.log('❌ JSON parse failed:', e.message);
// Try to fix common issues
console.log('🔧 Attempting to fix JSON...');
const fixed = match[1].trim()
.replace(/,\s*}/g, '}')
.replace(/,\s*]/g, ']');
try {
const fixedParsed = JSON.parse(fixed);
console.log('✅ Fixed JSON parsed successfully');
return fixedParsed;
} catch (e2) {
console.log('❌ Could not fix JSON:', e2.message);
return null;
}
}
},
/**
* Simulates a full parse cycle with a sample response
*/
simulateParseResponse(sampleResponse) {
console.log('\n🔄 Simulating Parse Response...\n');
const parsed = this.testJSONExtraction(sampleResponse);
if (parsed) {
console.log('\n📊 Validating parsed structure:');
if (parsed.userStats) {
console.log(' ✅ userStats present');
}
if (parsed.skills) {
console.log(' ✅ skills present');
}
if (parsed.inventory) {
console.log(' ✅ inventory present');
}
if (parsed.quests) {
console.log(' ✅ quests present');
}
if (parsed.infoBox) {
console.log(' ✅ infoBox present');
}
if (parsed.presentCharacters) {
console.log(' ✅ presentCharacters present');
}
}
return parsed;
},
/**
* Prints current extension settings for debugging
*/
printCurrentState() {
const settings = window.extension_settings?.['rpg-companion'];
if (!settings) {
console.error('RPG Companion not found');
return;
}
console.log('📋 Current RPG Companion State:\n');
console.log('inventoryV3:', JSON.stringify(settings.inventoryV3, null, 2));
console.log('skillsV2:', JSON.stringify(settings.skillsV2, null, 2));
console.log('questsV2:', JSON.stringify(settings.questsV2, null, 2));
console.log('infoBoxData:', JSON.stringify(settings.infoBoxData, null, 2));
console.log('charactersData:', JSON.stringify(settings.charactersData, null, 2));
},
/**
* Help message
*/
help() {
console.log(`
🧪 RPG Companion Test Helper Commands:
RPGTestHelper.validateLastResponse() - Validate current structured data
RPGTestHelper.testJSONExtraction(text) - Test JSON extraction from text
RPGTestHelper.simulateParseResponse(text) - Full parse simulation
RPGTestHelper.printCurrentState() - Print current extension state
RPGTestHelper.help() - Show this help message
Example:
RPGTestHelper.validateLastResponse()
`);
}
};
// Print help on load
console.log('🧪 RPG Companion Test Helper loaded. Run RPGTestHelper.help() for commands.');
-504
View File
@@ -1,504 +0,0 @@
/**
* JSON Format Tests for RPG Companion
*
* These tests can be run in two ways:
* 1. In browser console: Copy/paste or load as module in SillyTavern
* 2. Via Node.js: Run with `node tests/jsonFormat.test.js`
*
* Tests cover:
* - JSON prompt generation
* - JSON response parsing
* - Data structure validation
*/
// Mock SillyTavern context for Node.js testing
const isBrowser = typeof window !== 'undefined';
// Sample mock data for testing
const mockTrackerConfig = {
userStats: {
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'energy', name: 'Energy', enabled: true }
],
showRPGAttributes: true,
rpgAttributes: [
{ id: 'str', name: 'Strength', enabled: true },
{ id: 'dex', name: 'Dexterity', enabled: true }
],
statusSection: {
enabled: true,
showMoodEmoji: true,
customFields: ['Conditions']
},
skillsSection: {
enabled: true,
customFields: ['Combat', 'Stealth', 'Magic']
}
},
infoBox: {
widgets: {
date: { enabled: true },
weather: { enabled: true },
temperature: { enabled: true, unit: 'C' },
time: { enabled: true },
location: { enabled: true },
recentEvents: { enabled: true }
}
},
presentCharacters: {
showEmoji: true,
relationshipFields: ['Enemy', 'Neutral', 'Friend', 'Lover'],
customFields: [
{ id: 'appearance', name: 'Appearance', enabled: true },
{ id: 'demeanor', name: 'Demeanor', enabled: true }
],
thoughts: { enabled: true, name: 'Thoughts' },
characterStats: {
enabled: true,
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
]
}
}
};
// Sample JSON responses for testing parser
const sampleValidJSONResponse = `
Here's an interesting development in the story...
\`\`\`json
{
"userStats": {
"health": 85,
"energy": 60,
"str": 14,
"dex": 12,
"status": {
"mood": "😊",
"conditions": "Well-rested"
}
},
"skills": {
"combat": [
{ "name": "Sword Fighting", "description": "Basic melee combat with swords", "linkedItem": "Iron Sword" },
{ "name": "Parry", "description": "Deflect incoming attacks" }
],
"stealth": [
{ "name": "Sneak", "description": "Move quietly" }
],
"magic": []
},
"inventory": {
"onPerson": [
{ "name": "Iron Sword", "description": "A sturdy blade" },
{ "name": "Leather Armor", "description": "Basic protection" },
{ "name": "Health Potion", "description": "Restores 50 HP" }
],
"stored": {
"Backpack": [
{ "name": "Rope", "description": "50 feet of hemp rope" }
]
},
"assets": [
{ "name": "Small Cottage", "description": "A humble dwelling in the village" }
]
},
"quests": {
"main": "Find the Lost Artifact",
"optional": ["Gather herbs for the healer", "Clear the rat infestation"]
},
"infoBox": {
"date": "15th of Sunstone, Year 1423",
"weather": "☀️ Sunny",
"temperature": "22°C",
"time": "Midday",
"location": "Village Square",
"recentEvents": ["Met the village elder", "Bought supplies"]
},
"presentCharacters": [
{
"name": "Elena",
"description": "A young healer with kind eyes",
"emoji": "😊",
"relationship": "Friend",
"stats": { "health": 100, "arousal": 10 },
"appearance": "Long brown hair, green robes",
"demeanor": "Cheerful and helpful",
"thoughts": "I hope they can help me find the rare herbs..."
}
]
}
\`\`\`
The village was bustling with activity...
`;
const sampleMalformedJSONResponse = `
Some story text here...
\`\`\`json
{
"userStats": {
"health": 85,
"energy": 60,
},
"inventory": {
"onPerson": [
{ "name": "Sword", "description": "Sharp" }
]
}
}
\`\`\`
`;
const sampleSimplifiedInventoryResponse = `
\`\`\`json
{
"userStats": {
"health": 75,
"energy": 50
},
"inventory": {
"simplified": [
{ "name": "Magic Staff", "description": "Channels arcane energy" },
{ "name": "Spell Book", "description": "Contains basic spells" },
{ "name": "Mana Potion", "description": "Restores magical energy" }
]
}
}
\`\`\`
`;
// Test results accumulator
const testResults = {
passed: 0,
failed: 0,
errors: []
};
/**
* Simple assertion helper
*/
function assert(condition, message) {
if (condition) {
testResults.passed++;
console.log(`✅ PASS: ${message}`);
} else {
testResults.failed++;
testResults.errors.push(message);
console.error(`❌ FAIL: ${message}`);
}
}
/**
* Test JSON extraction from markdown code blocks
*/
function testJSONExtraction() {
console.log('\n📋 Testing JSON Extraction from Code Blocks...\n');
// Test 1: Extract valid JSON from code block
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
assert(match !== null, 'Should find JSON code block in response');
if (match) {
let parsed;
try {
parsed = JSON.parse(match[1].trim());
assert(true, 'Should parse extracted JSON successfully');
assert(parsed.userStats !== undefined, 'Parsed JSON should have userStats');
assert(parsed.inventory !== undefined, 'Parsed JSON should have inventory');
assert(parsed.skills !== undefined, 'Parsed JSON should have skills');
assert(parsed.quests !== undefined, 'Parsed JSON should have quests');
assert(parsed.infoBox !== undefined, 'Parsed JSON should have infoBox');
assert(parsed.presentCharacters !== undefined, 'Parsed JSON should have presentCharacters');
} catch (e) {
assert(false, `Should not throw parsing error: ${e.message}`);
}
}
// Test 2: Handle malformed JSON (trailing comma)
const malformedMatch = sampleMalformedJSONResponse.match(jsonRegex);
assert(malformedMatch !== null, 'Should find malformed JSON code block');
if (malformedMatch) {
try {
JSON.parse(malformedMatch[1].trim());
assert(false, 'Malformed JSON should throw parsing error');
} catch (e) {
assert(true, 'Malformed JSON correctly throws parsing error');
// Test JSON fixing (remove trailing commas)
const fixed = malformedMatch[1].trim()
.replace(/,\s*}/g, '}')
.replace(/,\s*]/g, ']');
try {
const fixedParsed = JSON.parse(fixed);
assert(fixedParsed.userStats.health === 85, 'Fixed JSON should parse correctly');
} catch (e2) {
assert(false, `JSON fixing should work: ${e2.message}`);
}
}
}
}
/**
* Test inventory data structure validation
*/
function testInventoryStructure() {
console.log('\n📦 Testing Inventory Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const inv = data.inventory;
// Test onPerson array structure
assert(Array.isArray(inv.onPerson), 'onPerson should be an array');
assert(inv.onPerson.length > 0, 'onPerson should have items');
assert(inv.onPerson[0].name !== undefined, 'Items should have name property');
assert(inv.onPerson[0].description !== undefined, 'Items should have description property');
// Test stored object structure
assert(typeof inv.stored === 'object', 'stored should be an object');
assert(inv.stored.Backpack !== undefined, 'stored should have location keys');
assert(Array.isArray(inv.stored.Backpack), 'stored locations should be arrays');
// Test assets array
assert(Array.isArray(inv.assets), 'assets should be an array');
// Test simplified inventory
const simplifiedMatch = sampleSimplifiedInventoryResponse.match(jsonRegex);
const simplifiedData = JSON.parse(simplifiedMatch[1].trim());
assert(Array.isArray(simplifiedData.inventory.simplified), 'simplified inventory should be an array');
}
/**
* Test skills data structure validation
*/
function testSkillsStructure() {
console.log('\n⚔️ Testing Skills Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const skills = data.skills;
// Test skill categories
assert(typeof skills === 'object', 'skills should be an object');
assert(skills.combat !== undefined, 'skills should have combat category');
assert(Array.isArray(skills.combat), 'skill categories should be arrays');
// Test ability structure
const ability = skills.combat[0];
assert(ability.name !== undefined, 'Abilities should have name');
assert(ability.description !== undefined, 'Abilities should have description');
// Test linked item
assert(ability.linkedItem === 'Iron Sword', 'First combat ability should be linked to Iron Sword');
assert(skills.combat[1].linkedItem === undefined || skills.combat[1].linkedItem === null,
'Second combat ability should not have linkedItem');
}
/**
* Test quests data structure
*/
function testQuestsStructure() {
console.log('\n📜 Testing Quests Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const quests = data.quests;
assert(typeof quests.main === 'string', 'main quest should be a string');
assert(Array.isArray(quests.optional), 'optional quests should be an array');
assert(quests.optional.length === 2, 'Should have 2 optional quests');
}
/**
* Test characters data structure
*/
function testCharactersStructure() {
console.log('\n👥 Testing Characters Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const chars = data.presentCharacters;
assert(Array.isArray(chars), 'presentCharacters should be an array');
assert(chars.length > 0, 'Should have at least one character');
const char = chars[0];
assert(char.name === 'Elena', 'Character should have name');
assert(char.description !== undefined, 'Character should have description');
assert(char.emoji !== undefined, 'Character should have emoji');
assert(char.relationship !== undefined, 'Character should have relationship');
assert(char.stats !== undefined, 'Character should have stats');
assert(char.thoughts !== undefined, 'Character should have thoughts');
}
/**
* Test info box data structure
*/
function testInfoBoxStructure() {
console.log('\n📍 Testing Info Box Structure...\n');
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/i;
const match = sampleValidJSONResponse.match(jsonRegex);
const data = JSON.parse(match[1].trim());
const info = data.infoBox;
assert(typeof info.date === 'string', 'date should be a string');
assert(typeof info.weather === 'string', 'weather should be a string');
assert(typeof info.temperature === 'string', 'temperature should be a string');
assert(typeof info.time === 'string', 'time should be a string');
assert(typeof info.location === 'string', 'location should be a string');
assert(Array.isArray(info.recentEvents), 'recentEvents should be an array');
}
/**
* Test JSON prompt schema generation (mock)
*/
function testPromptSchemaGeneration() {
console.log('\n📝 Testing Prompt Schema Generation...\n');
// This tests the expected schema structure that generateJSONTrackerInstructions should produce
const expectedSchemaProperties = [
'userStats',
'skills',
'inventory',
'quests',
'infoBox',
'presentCharacters'
];
// Mock schema generation based on config
const schema = {
type: 'object',
properties: {}
};
// User stats
if (mockTrackerConfig.userStats) {
schema.properties.userStats = { type: 'object', properties: {} };
// Custom stats
mockTrackerConfig.userStats.customStats.forEach(stat => {
if (stat.enabled) {
schema.properties.userStats.properties[stat.id] = {
type: 'integer',
minimum: 0,
maximum: 100
};
}
});
// RPG attributes
if (mockTrackerConfig.userStats.showRPGAttributes) {
mockTrackerConfig.userStats.rpgAttributes.forEach(attr => {
if (attr.enabled) {
schema.properties.userStats.properties[attr.id] = {
type: 'integer',
minimum: 1
};
}
});
}
}
// Skills
if (mockTrackerConfig.userStats.skillsSection?.enabled) {
schema.properties.skills = { type: 'object', properties: {} };
mockTrackerConfig.userStats.skillsSection.customFields.forEach(field => {
const fieldId = field.toLowerCase();
schema.properties.skills.properties[fieldId] = {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
linkedItem: { type: 'string', nullable: true }
}
}
};
});
}
// Inventory
schema.properties.inventory = {
type: 'object',
properties: {
onPerson: { type: 'array' },
stored: { type: 'object' },
assets: { type: 'array' }
}
};
// Validate schema structure
assert(schema.properties.userStats !== undefined, 'Schema should include userStats');
assert(schema.properties.userStats.properties.health !== undefined, 'Schema should include health stat');
assert(schema.properties.skills !== undefined, 'Schema should include skills');
assert(schema.properties.skills.properties.combat !== undefined, 'Schema should include combat skill');
assert(schema.properties.inventory !== undefined, 'Schema should include inventory');
console.log('Generated schema structure:', JSON.stringify(schema, null, 2).substring(0, 500) + '...');
}
/**
* Run all tests
*/
function runAllTests() {
console.log('🧪 RPG Companion JSON Format Tests\n');
console.log('='.repeat(50));
try {
testJSONExtraction();
testInventoryStructure();
testSkillsStructure();
testQuestsStructure();
testCharactersStructure();
testInfoBoxStructure();
testPromptSchemaGeneration();
} catch (e) {
console.error('💥 Test suite error:', e);
testResults.failed++;
testResults.errors.push(e.message);
}
console.log('\n' + '='.repeat(50));
console.log(`\n📊 Results: ${testResults.passed} passed, ${testResults.failed} failed`);
if (testResults.errors.length > 0) {
console.log('\n❌ Failed tests:');
testResults.errors.forEach(err => console.log(` - ${err}`));
}
return testResults;
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
runAllTests,
sampleValidJSONResponse,
sampleMalformedJSONResponse,
sampleSimplifiedInventoryResponse,
mockTrackerConfig
};
}
// Auto-run if executed directly
if (!isBrowser || (isBrowser && window.RPG_RUN_TESTS)) {
runAllTests();
}
-342
View File
@@ -1,342 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RPG Companion - Test Runner</title>
<style>
:root {
--bg: #1a1a2e;
--surface: #16213e;
--border: #0f3460;
--text: #e8e8e8;
--accent: #e94560;
--success: #4caf50;
--error: #f44336;
--warning: #ff9800;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
padding: 2rem;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
color: var(--accent);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
h1::before {
content: '🧪';
}
.description {
background: var(--surface);
padding: 1rem;
border-radius: 8px;
margin-bottom: 2rem;
border-left: 4px solid var(--accent);
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}
button {
background: var(--accent);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(233, 69, 96, 0.3);
}
button.secondary {
background: var(--surface);
border: 1px solid var(--border);
}
.results-summary {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
padding: 1rem;
background: var(--surface);
border-radius: 8px;
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
}
.stat-label {
font-size: 0.85rem;
opacity: 0.7;
text-transform: uppercase;
}
.stat.passed .stat-value { color: var(--success); }
.stat.failed .stat-value { color: var(--error); }
.stat.total .stat-value { color: var(--accent); }
.test-output {
background: #0d1117;
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.9rem;
max-height: 500px;
overflow-y: auto;
}
.test-output .pass { color: var(--success); }
.test-output .fail { color: var(--error); }
.test-output .section {
color: var(--accent);
font-weight: bold;
margin-top: 1rem;
}
.test-output .info { color: #8b949e; }
.test-section {
margin-bottom: 2rem;
}
h2 {
color: var(--accent);
margin-bottom: 1rem;
font-size: 1.25rem;
}
.json-sample {
background: #0d1117;
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.8rem;
overflow-x: auto;
white-space: pre;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab {
padding: 0.5rem 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px 6px 0 0;
cursor: pointer;
transition: all 0.2s;
}
.tab.active {
background: var(--accent);
border-color: var(--accent);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.note {
background: rgba(255, 152, 0, 0.1);
border: 1px solid var(--warning);
padding: 1rem;
border-radius: 8px;
margin-top: 1rem;
}
.note::before {
content: '⚠️ ';
}
</style>
</head>
<body>
<div class="container">
<h1>RPG Companion Test Runner</h1>
<div class="description">
<p>This test suite validates the JSON format used for structured tracker data.</p>
<p>Tests cover: JSON extraction, parsing, data structure validation, and schema generation.</p>
</div>
<div class="controls">
<button onclick="runTests()">▶️ Run All Tests</button>
<button class="secondary" onclick="clearResults()">🗑️ Clear Results</button>
</div>
<div class="results-summary" id="results-summary" style="display: none;">
<div class="stat total">
<span class="stat-value" id="total-count">0</span>
<span class="stat-label">Total</span>
</div>
<div class="stat passed">
<span class="stat-value" id="passed-count">0</span>
<span class="stat-label">Passed</span>
</div>
<div class="stat failed">
<span class="stat-value" id="failed-count">0</span>
<span class="stat-label">Failed</span>
</div>
</div>
<div class="test-section">
<h2>Test Output</h2>
<div class="test-output" id="test-output">
<span class="info">Click "Run All Tests" to start...</span>
</div>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('valid')">Valid JSON</div>
<div class="tab" onclick="showTab('malformed')">Malformed JSON</div>
<div class="tab" onclick="showTab('simplified')">Simplified Inventory</div>
</div>
<div id="tab-valid" class="tab-content active">
<h2>Sample Valid JSON Response</h2>
<div class="json-sample" id="valid-json"></div>
</div>
<div id="tab-malformed" class="tab-content">
<h2>Sample Malformed JSON (with trailing comma)</h2>
<div class="json-sample" id="malformed-json"></div>
</div>
<div id="tab-simplified" class="tab-content">
<h2>Sample Simplified Inventory Response</h2>
<div class="json-sample" id="simplified-json"></div>
</div>
<div class="note">
<strong>Note:</strong> These tests run in isolation and don't require SillyTavern to be running.
For integration testing with actual LLM responses, use the Debug Mode in the extension settings.
</div>
</div>
<script type="module">
// Import test module
import {
runAllTests,
sampleValidJSONResponse,
sampleMalformedJSONResponse,
sampleSimplifiedInventoryResponse
} from './jsonFormat.test.js';
// Display sample JSON
document.getElementById('valid-json').textContent = sampleValidJSONResponse;
document.getElementById('malformed-json').textContent = sampleMalformedJSONResponse;
document.getElementById('simplified-json').textContent = sampleSimplifiedInventoryResponse;
// Override console for capturing test output
const originalLog = console.log;
const originalError = console.error;
let outputBuffer = [];
function captureConsole() {
outputBuffer = [];
console.log = (...args) => {
outputBuffer.push({ type: 'log', message: args.join(' ') });
originalLog.apply(console, args);
};
console.error = (...args) => {
outputBuffer.push({ type: 'error', message: args.join(' ') });
originalError.apply(console, args);
};
}
function restoreConsole() {
console.log = originalLog;
console.error = originalError;
}
function formatOutput(buffer) {
return buffer.map(item => {
let cls = 'info';
if (item.message.includes('✅')) cls = 'pass';
else if (item.message.includes('❌')) cls = 'fail';
else if (item.message.includes('📋') || item.message.includes('📦') ||
item.message.includes('⚔️') || item.message.includes('📜') ||
item.message.includes('👥') || item.message.includes('📍') ||
item.message.includes('📝')) cls = 'section';
return `<div class="${cls}">${escapeHtml(item.message)}</div>`;
}).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.runTests = function() {
captureConsole();
const results = runAllTests();
restoreConsole();
document.getElementById('test-output').innerHTML = formatOutput(outputBuffer);
document.getElementById('results-summary').style.display = 'flex';
document.getElementById('total-count').textContent = results.passed + results.failed;
document.getElementById('passed-count').textContent = results.passed;
document.getElementById('failed-count').textContent = results.failed;
};
window.clearResults = function() {
document.getElementById('test-output').innerHTML = '<span class="info">Click "Run All Tests" to start...</span>';
document.getElementById('results-summary').style.display = 'none';
};
window.showTab = function(tabId) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('tab-' + tabId).classList.add('active');
};
</script>
</body>
</html>