/** * Tracker Editor Module * Provides UI for customizing tracker configurations */ import { i18n } from '../../core/i18n.js'; import { extensionSettings } from '../../core/state.js'; import { saveSettings, getPresets, getPreset, getActivePresetId, getDefaultPresetId, setDefaultPreset, isDefaultPreset, createPreset, saveToPreset, loadPreset, renamePreset, deletePreset, associatePresetWithCurrentEntity, removePresetAssociationForCurrentEntity, getPresetForCurrentEntity, hasPresetAssociation, isAssociatedWithCurrentPreset, getCurrentEntityKey, getCurrentEntityName, exportPresets, importPresets } from '../../core/persistence.js'; import { renderUserStats } from '../rendering/userStats.js'; import { renderInfoBox } from '../rendering/infoBox.js'; import { renderThoughts } from '../rendering/thoughts.js'; import { updateFabWidgets } from './mobile.js'; let $editorModal = null; let activeTab = 'userStats'; let tempConfig = null; // Temporary config for cancel functionality let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null } let originalAssociation = null; // Original association when editor opened /** * Initialize the tracker editor modal */ export function initTrackerEditor() { // Modal will be in template.html, just set up event listeners $editorModal = $('#rpg-tracker-editor-popup'); if (!$editorModal.length) { console.error('[RPG Companion] Tracker editor modal not found in template'); return; } // Tab switching $(document).on('click', '.rpg-editor-tab', function () { $('.rpg-editor-tab').removeClass('active'); $(this).addClass('active'); activeTab = $(this).data('tab'); $('.rpg-editor-tab-content').hide(); $(`#rpg-editor-tab-${activeTab}`).show(); }); // Save button $(document).on('click', '#rpg-editor-save', function () { applyTrackerConfig(); closeTrackerEditor(); }); // Cancel button $(document).on('click', '#rpg-editor-cancel', function () { closeTrackerEditor(); }); // Close X button $(document).on('click', '#rpg-close-tracker-editor', function () { closeTrackerEditor(); }); // Reset button $(document).on('click', '#rpg-editor-reset', function () { resetToDefaults(); renderEditorUI(); }); // Close on background click $(document).on('click', '#rpg-tracker-editor-popup', function (e) { if (e.target.id === 'rpg-tracker-editor-popup') { closeTrackerEditor(); } }); // Open button $(document).on('click', '#rpg-open-tracker-editor', function () { openTrackerEditor(); }); // Export button $(document).on('click', '#rpg-editor-export', function () { exportTrackerPreset(); }); // Import button $(document).on('click', '#rpg-editor-import', function () { importTrackerPreset(); }); // Preset select change $(document).on('change', '#rpg-preset-select', function () { const presetId = $(this).val(); if (presetId && presetId !== getActivePresetId()) { // Check if the current character had an association (either original or pending) const entityKey = getCurrentEntityKey(); const wasAssociated = tempAssociation ? tempAssociation.presetId !== null : hasPresetAssociation(); // Save current changes to the old preset before switching const currentPresetId = getActivePresetId(); if (currentPresetId) { saveToPreset(currentPresetId); } // Load the new preset if (loadPreset(presetId)) { tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig)); renderEditorUI(); // If the character was associated with a preset, update temp association to new preset if (wasAssociated && entityKey) { tempAssociation = { presetId: presetId, entityKey: entityKey }; const preset = getPreset(presetId); toastr.info(`"${preset?.name || 'Unknown'}" will be associated with ${getCurrentEntityName()} when saved.`); } else { toastr.success(`Switched to preset "${getPreset(presetId)?.name || 'Unknown'}".`); } updatePresetUI(); } } }); // New preset button $(document).on('click', '#rpg-preset-new', function () { const name = prompt('Enter a name for the new preset:'); if (name && name.trim()) { const newId = createPreset(name.trim()); updatePresetUI(); $('#rpg-preset-select').val(newId); toastr.success(`Created preset "${name.trim()}".`); } }); // Set as default preset button $(document).on('click', '#rpg-preset-default', function () { const currentPresetId = getActivePresetId(); if (currentPresetId) { setDefaultPreset(currentPresetId); updatePresetUI(); const preset = getPreset(currentPresetId); toastr.success(`"${preset?.name || 'Unknown'}" is now the default preset.`); } }); // Delete preset button $(document).on('click', '#rpg-preset-delete', function () { const currentPresetId = getActivePresetId(); const presets = getPresets(); if (Object.keys(presets).length <= 1) { toastr.warning('Cannot delete the last preset.'); return; } const preset = getPreset(currentPresetId); if (confirm(`Are you sure you want to delete the preset "${preset?.name || 'Unknown'}"?`)) { if (deletePreset(currentPresetId)) { tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig)); renderEditorUI(); updatePresetUI(); toastr.success('Preset deleted.'); } } }); // Associate preset checkbox $(document).on('change', '#rpg-preset-associate', function () { const activePresetId = getActivePresetId(); const preset = getPreset(activePresetId); const entityName = getCurrentEntityName(); const entityKey = getCurrentEntityKey(); if ($(this).is(':checked')) { // Store pending association (don't save yet) tempAssociation = { presetId: activePresetId, entityKey: entityKey }; toastr.info(`"${preset?.name || 'Unknown'}" will be associated with ${entityName} when saved.`); } else { // Store pending removal (don't save yet) tempAssociation = { presetId: null, entityKey: entityKey }; const defaultPresetId = getDefaultPresetId(); const defaultPreset = getPreset(defaultPresetId); if (defaultPreset && defaultPresetId !== activePresetId) { toastr.info(`Association will be removed when saved. Default preset "${defaultPreset.name}" will apply on next character switch.`); } else { toastr.info(`Association will be removed for ${entityName} when saved.`); } } }); } /** * Updates the preset management UI (dropdown, association checkbox, entity name) */ function updatePresetUI() { const presets = getPresets(); const activePresetId = getActivePresetId(); const defaultPresetId = getDefaultPresetId(); const $select = $('#rpg-preset-select'); // Populate the dropdown $select.empty(); for (const [id, preset] of Object.entries(presets)) { const isDefault = id === defaultPresetId; const starPrefix = isDefault ? '★ ' : ''; $select.append(``); } $select.val(activePresetId); // Update the default button appearance const $defaultBtn = $('#rpg-preset-default'); if (isDefaultPreset(activePresetId)) { $defaultBtn.addClass('rpg-btn-active').attr('title', 'This is the default preset'); } else { $defaultBtn.removeClass('rpg-btn-active').attr('title', 'Set as Default Preset'); } // Update the entity name display const entityName = getCurrentEntityName(); $('#rpg-preset-entity-name').text(entityName); // Update the association checkbox // Use temp state if available, otherwise check actual association with CURRENT preset let isAssociated; if (tempAssociation !== null) { // Use pending state: checked if pending preset matches active preset isAssociated = tempAssociation.presetId === activePresetId; } else { // No pending changes, check actual state isAssociated = isAssociatedWithCurrentPreset(); } $('#rpg-preset-associate').prop('checked', isAssociated); } /** * Open the tracker editor modal */ function openTrackerEditor() { // Create temporary copy for cancel functionality tempConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig)); // Store original association state for cancel functionality const entityKey = getCurrentEntityKey(); const currentAssociatedPreset = getPresetForCurrentEntity(); originalAssociation = { presetId: currentAssociatedPreset, entityKey: entityKey }; tempAssociation = null; // Reset pending changes // Set theme to match current extension theme const theme = extensionSettings.theme || 'modern'; $editorModal.attr('data-theme', theme); // Update preset UI updatePresetUI(); renderEditorUI(); $editorModal.addClass('is-open').css('display', ''); } /** * Close the tracker editor modal */ function closeTrackerEditor() { // Restore from temp if canceling if (tempConfig) { extensionSettings.trackerConfig = tempConfig; tempConfig = null; } // Discard pending association changes (cancel = no save) tempAssociation = null; originalAssociation = null; $editorModal.removeClass('is-open').addClass('is-closing'); setTimeout(() => { $editorModal.removeClass('is-closing').hide(); }, 200); } /** * Apply the tracker configuration and refresh all trackers */ function applyTrackerConfig() { tempConfig = null; // Clear temp config // Apply pending association changes if (tempAssociation) { if (tempAssociation.presetId !== null) { // Associate with the pending preset associatePresetWithCurrentEntity(); const preset = getPreset(tempAssociation.presetId); toastr.success(`"${preset?.name || 'Unknown'}" is now associated with ${getCurrentEntityName()}.`); } else { // Remove association removePresetAssociationForCurrentEntity(); } tempAssociation = null; } originalAssociation = null; // Save to the current preset const currentPresetId = getActivePresetId(); if (currentPresetId) { saveToPreset(currentPresetId); } else { saveSettings(); } // Re-render all trackers with new config renderUserStats(); renderInfoBox(); renderThoughts(); updateFabWidgets(); // Update FAB widgets to reflect new config } /** * Reset configuration to defaults */ function resetToDefaults() { extensionSettings.trackerConfig = { userStats: { customStats: [ { id: 'health', name: i18n.getTranslation('stats.health'), enabled: true, persistInHistory: false }, { id: 'satiety', name: i18n.getTranslation('stats.satiety'), enabled: true, persistInHistory: false }, { id: 'energy', name: i18n.getTranslation('stats.energy'), enabled: true, persistInHistory: false }, { id: 'hygiene', name: i18n.getTranslation('stats.hygiene'), enabled: true, persistInHistory: false }, { id: 'arousal', name: i18n.getTranslation('stats.arousal'), enabled: true, persistInHistory: false } ], showRPGAttributes: true, rpgAttributes: [ { id: 'str', name: i18n.getTranslation('stats.str'), enabled: true, persistInHistory: false }, { id: 'dex', name: i18n.getTranslation('stats.dex'), enabled: true, persistInHistory: false }, { id: 'con', name: i18n.getTranslation('stats.con'), enabled: true, persistInHistory: false }, { id: 'int', name: i18n.getTranslation('stats.int'), enabled: true, persistInHistory: false }, { id: 'wis', name: i18n.getTranslation('stats.wis'), enabled: true, persistInHistory: false }, { id: 'cha', name: i18n.getTranslation('stats.cha'), enabled: true, persistInHistory: false } ], statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'], persistInHistory: false }, skillsSection: { enabled: false, label: 'Skills', customFields: [], persistInHistory: false }, inventoryPersistInHistory: false, questsPersistInHistory: false }, infoBox: { widgets: { date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, weather: { enabled: true, persistInHistory: true }, temperature: { enabled: true, unit: 'C', persistInHistory: false }, time: { enabled: true, persistInHistory: true }, location: { enabled: true, persistInHistory: true }, recentEvents: { enabled: true, persistInHistory: false } } }, presentCharacters: { showEmoji: true, showName: true, relationships: { enabled: true, relationshipEmojis: { 'Lover': '❤️', 'Friend': '⭐', 'Ally': '🤝', 'Enemy': '⚔️', 'Neutral': '⚖️' } }, relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'], relationshipEmojis: { 'Lover': '❤️', 'Friend': '⭐', 'Ally': '🤝', 'Enemy': '⚔️', 'Neutral': '⚖️' }, customFields: [ { id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)', persistInHistory: false }, { id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false } ], thoughts: { enabled: true, name: 'Thoughts', description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)', persistInHistory: false }, characterStats: { enabled: false, customStats: [ { id: 'health', name: i18n.getTranslation('stats.health'), enabled: true, colorLow: '#ff4444', colorHigh: '#44ff44' }, { id: 'energy', name: i18n.getTranslation('stats.energy'), enabled: true, colorLow: '#ffaa00', colorHigh: '#44ffff' } ] } } }; // Reset history persistence settings extensionSettings.historyPersistence = { enabled: false, messageCount: 5, injectionPosition: 'assistant_message_end', contextPreamble: '', sendAllEnabledOnRefresh: false }; } /** * Export current tracker configuration to a JSON file */ function exportTrackerPreset() { try { // Get the current tracker configuration const config = extensionSettings.trackerConfig; const historyPersistence = extensionSettings.historyPersistence; // Create a preset object with metadata const preset = { name: 'Custom Tracker Preset', version: '1.1', // Bumped version for historyPersistence support exportDate: new Date().toISOString(), trackerConfig: JSON.parse(JSON.stringify(config)), // Deep copy historyPersistence: historyPersistence ? JSON.parse(JSON.stringify(historyPersistence)) : null // Include history persistence settings }; // Convert to JSON const jsonString = JSON.stringify(preset, null, 2); const blob = new Blob([jsonString], { type: 'application/json' }); // Create download link const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; // Generate filename with timestamp const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); link.download = `rpg-tracker-preset-${timestamp}.json`; // Trigger download document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); // console.log('[RPG Companion] Tracker preset exported successfully'); toastr.success(i18n.getTranslation('template.trackerEditorModal.messages.exportSuccess') || 'Tracker preset exported successfully!'); } catch (error) { console.error('[RPG Companion] Error exporting tracker preset:', error); toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.exportError') || 'Failed to export tracker preset. Check console for details.'); } } /** * Migrates old tracker preset format to current format * @param {Object} config - The tracker config to migrate * @returns {Object} - Migrated tracker config */ function migrateTrackerPreset(config) { // Create a deep copy to avoid modifying the original const migrated = JSON.parse(JSON.stringify(config)); // Migrate relationships structure (v3.0.0 -> v3.1.0) if (migrated.presentCharacters) { // Old format: relationshipEmojis directly on presentCharacters // New format: relationships.relationshipEmojis if (migrated.presentCharacters.relationshipEmojis && !migrated.presentCharacters.relationships) { migrated.presentCharacters.relationships = { enabled: migrated.presentCharacters.enableRelationships || true, relationshipEmojis: migrated.presentCharacters.relationshipEmojis }; // Keep legacy fields for backward compatibility migrated.presentCharacters.relationshipFields = Object.keys(migrated.presentCharacters.relationshipEmojis); } // Ensure relationships object exists if (!migrated.presentCharacters.relationships) { migrated.presentCharacters.relationships = { enabled: false, relationshipEmojis: {} }; } // Ensure relationshipEmojis exists within relationships if (!migrated.presentCharacters.relationships.relationshipEmojis) { migrated.presentCharacters.relationships.relationshipEmojis = {}; } // Add persistInHistory to customFields if missing (v3.4.0) if (migrated.presentCharacters.customFields) { migrated.presentCharacters.customFields = migrated.presentCharacters.customFields.map(field => ({ ...field, persistInHistory: field.persistInHistory ?? false })); } // Add persistInHistory to thoughts if missing (v3.4.0) if (migrated.presentCharacters.thoughts && migrated.presentCharacters.thoughts.persistInHistory === undefined) { migrated.presentCharacters.thoughts.persistInHistory = false; } } // Add persistInHistory to userStats fields if missing (v3.4.0) if (migrated.userStats) { // Custom stats if (migrated.userStats.customStats) { migrated.userStats.customStats = migrated.userStats.customStats.map(stat => ({ ...stat, persistInHistory: stat.persistInHistory ?? false })); } // RPG Attributes if (migrated.userStats.rpgAttributes) { migrated.userStats.rpgAttributes = migrated.userStats.rpgAttributes.map(attr => ({ ...attr, persistInHistory: attr.persistInHistory ?? false })); } // Status section if (migrated.userStats.statusSection && migrated.userStats.statusSection.persistInHistory === undefined) { migrated.userStats.statusSection.persistInHistory = false; } // Skills section if (migrated.userStats.skillsSection && migrated.userStats.skillsSection.persistInHistory === undefined) { migrated.userStats.skillsSection.persistInHistory = false; } // Inventory and quests persistence if (migrated.userStats.inventoryPersistInHistory === undefined) { migrated.userStats.inventoryPersistInHistory = false; } if (migrated.userStats.questsPersistInHistory === undefined) { migrated.userStats.questsPersistInHistory = false; } } // Add persistInHistory to infoBox widgets if missing (v3.4.0) if (migrated.infoBox && migrated.infoBox.widgets) { for (const [widgetId, widget] of Object.entries(migrated.infoBox.widgets)) { if (widget.persistInHistory === undefined) { // Default to false for backwards compatibility - user must explicitly enable widget.persistInHistory = false; } } } return migrated; } /** * Import tracker configuration from a JSON file */ function importTrackerPreset() { // Create file input const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); // Validate the imported data if (!data.trackerConfig) { throw new Error('Invalid preset file: missing trackerConfig'); } // Validate required sections if (!data.trackerConfig.userStats || !data.trackerConfig.infoBox || !data.trackerConfig.presentCharacters) { throw new Error('Invalid preset file: missing required configuration sections'); } // Migrate old preset format to current format const migratedConfig = migrateTrackerPreset(data.trackerConfig); // Extract historyPersistence if present in the import file const historyPersistence = data.historyPersistence || null; // Show import mode selection dialog showImportModeDialog(migratedConfig, data.name || file.name.replace('.json', ''), historyPersistence); } catch (error) { console.error('[RPG Companion] Error importing tracker preset:', error); toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.importError') || `Failed to import tracker preset: ${error.message}`); } }; // Trigger file selection input.click(); } /** * Show dialog to choose import mode * @param {Object} migratedConfig - The migrated tracker config * @param {string} suggestedName - Suggested name for new preset * @param {Object|null} historyPersistence - The history persistence settings from import (if any) */ function showImportModeDialog(migratedConfig, suggestedName, historyPersistence = null) { // Create dialog overlay const dialogHtml = `

Import Configuration

How would you like to import this configuration?

`; $('body').append(dialogHtml); const $dialog = $('#rpg-import-mode-dialog'); // Import to current preset $('#rpg-import-to-current').on('click', () => { $dialog.remove(); // Apply the migrated configuration to current extensionSettings.trackerConfig = migratedConfig; // Apply historyPersistence settings if present in import if (historyPersistence) { extensionSettings.historyPersistence = historyPersistence; } // Save to the active preset (saveToPreset uses current trackerConfig) const activePresetId = getActivePresetId(); if (activePresetId) { saveToPreset(activePresetId); } // Re-render the editor UI renderEditorUI(); toastr.success('Configuration applied to current preset.'); }); // Import as new preset $('#rpg-import-as-new').on('click', () => { $dialog.remove(); // Prompt for preset name const presetName = prompt('Enter a name for the new preset:', suggestedName); if (!presetName) return; // Set the migrated config as current first extensionSettings.trackerConfig = migratedConfig; // Apply historyPersistence settings if present in import if (historyPersistence) { extensionSettings.historyPersistence = historyPersistence; } // Create new preset (createPreset uses current trackerConfig) const newPresetId = createPreset(presetName); if (newPresetId) { // Load the new preset loadPreset(newPresetId); renderEditorUI(); updatePresetUI(); toastr.success(`Created new preset: ${presetName}.`); } }); // Cancel $('#rpg-import-cancel').on('click', () => { $dialog.remove(); }); // Close on overlay click $dialog.on('click', (e) => { if (e.target === $dialog[0]) { $dialog.remove(); } }); } /** * Render the editor UI based on current config */ function renderEditorUI() { renderUserStatsTab(); renderInfoBoxTab(); renderPresentCharactersTab(); renderHistoryPersistenceTab(); } /** * Render User Stats configuration tab */ function renderUserStatsTab() { const config = extensionSettings.trackerConfig.userStats; let html = '
'; // Custom Stats section html += `

${i18n.getTranslation('template.trackerEditorModal.userStatsTab.customStatsTitle')}

`; // Stats display mode toggle const statsDisplayMode = config.statsDisplayMode || 'percentage'; html += '
'; html += ''; html += '
'; html += ``; html += ``; html += '
'; html += '
'; html += '
'; config.customStats.forEach((stat, index) => { const showMaxValue = statsDisplayMode === 'number'; const maxValue = stat.maxValue || 100; html += `
`; }); html += '
'; html += ``; // RPG Attributes section html += `

${i18n.getTranslation('template.trackerEditorModal.userStatsTab.rpgAttributesTitle')}

`; // Enable/disable toggle for entire RPG Attributes section const showRPGAttributes = config.showRPGAttributes !== undefined ? config.showRPGAttributes : true; html += '
'; html += ``; html += ``; html += '
'; // Show/hide level toggle const showLevel = config.showLevel !== undefined ? config.showLevel : true; html += '
'; html += ``; html += ``; html += '
'; // Always send attributes toggle const alwaysSendAttributes = config.alwaysSendAttributes !== undefined ? config.alwaysSendAttributes : false; html += '
'; html += ``; html += ``; html += '
'; html += `${i18n.getTranslation('template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote')}`; html += '
'; // Ensure rpgAttributes exists in the actual config (not just local fallback) if (!config.rpgAttributes || config.rpgAttributes.length === 0) { config.rpgAttributes = [ { id: 'str', name: 'STR', enabled: true }, { id: 'dex', name: 'DEX', enabled: true }, { id: 'con', name: 'CON', enabled: true }, { id: 'int', name: 'INT', enabled: true }, { id: 'wis', name: 'WIS', enabled: true }, { id: 'cha', name: 'CHA', enabled: true } ]; // Save the defaults back to the actual config extensionSettings.trackerConfig.userStats.rpgAttributes = config.rpgAttributes; } const rpgAttributes = config.rpgAttributes; rpgAttributes.forEach((attr, index) => { html += `
`; }); html += '
'; html += ``; // Status Section html += `

${i18n.getTranslation('template.trackerEditorModal.userStatsTab.statusSectionTitle')}

`; html += '
'; html += ``; html += ``; html += '
'; html += '
'; html += ``; html += ``; html += '
'; html += ``; html += ``; // Skills Section html += `

${i18n.getTranslation('template.trackerEditorModal.userStatsTab.skillsSectionTitle')}

`; html += '
'; html += ``; html += ``; html += '
'; html += ``; html += ``; html += ``; const skillFields = config.skillsSection.customFields || []; html += ``; html += '
'; $('#rpg-editor-tab-userStats').html(html); setupUserStatsListeners(); } /** * Set up event listeners for User Stats tab */ function setupUserStatsListeners() { // Add stat $('#rpg-add-stat').off('click').on('click', function () { const newId = 'custom_' + Date.now(); extensionSettings.trackerConfig.userStats.customStats.push({ id: newId, name: 'New Stat', enabled: true, maxValue: 100 }); // Initialize value if doesn't exist if (extensionSettings.userStats[newId] === undefined) { extensionSettings.userStats[newId] = 100; } renderUserStatsTab(); }); // Remove stat $('.rpg-stat-remove').off('click').on('click', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.userStats.customStats.splice(index, 1); renderUserStatsTab(); }); // Toggle stat $('.rpg-stat-toggle').off('change').on('change', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.userStats.customStats[index].enabled = $(this).is(':checked'); }); // Rename stat $('.rpg-stat-name').off('blur').on('blur', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.userStats.customStats[index].name = $(this).val(); }); // Change stat max value $('.rpg-stat-max').off('blur').on('blur', function () { const index = $(this).data('index'); const value = parseInt($(this).val()) || 100; extensionSettings.trackerConfig.userStats.customStats[index].maxValue = Math.max(1, value); }); // Stats display mode toggle $('input[name="stats-display-mode"]').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.statsDisplayMode = $(this).val(); renderUserStatsTab(); // Re-render to show/hide max value fields }); // Add attribute $('#rpg-add-attr').off('click').on('click', function () { // Ensure rpgAttributes array exists with defaults if needed if (!extensionSettings.trackerConfig.userStats.rpgAttributes || extensionSettings.trackerConfig.userStats.rpgAttributes.length === 0) { extensionSettings.trackerConfig.userStats.rpgAttributes = [ { id: 'str', name: 'STR', enabled: true }, { id: 'dex', name: 'DEX', enabled: true }, { id: 'con', name: 'CON', enabled: true }, { id: 'int', name: 'INT', enabled: true }, { id: 'wis', name: 'WIS', enabled: true }, { id: 'cha', name: 'CHA', enabled: true } ]; } const newId = 'attr_' + Date.now(); extensionSettings.trackerConfig.userStats.rpgAttributes.push({ id: newId, name: 'NEW', enabled: true }); // Initialize value in classicStats if doesn't exist if (extensionSettings.classicStats[newId] === undefined) { extensionSettings.classicStats[newId] = 10; } renderUserStatsTab(); }); // Remove attribute $('.rpg-attr-remove').off('click').on('click', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.userStats.rpgAttributes.splice(index, 1); renderUserStatsTab(); }); // Toggle attribute $('.rpg-attr-toggle').off('change').on('change', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.userStats.rpgAttributes[index].enabled = $(this).is(':checked'); }); // Rename attribute $('.rpg-attr-name').off('blur').on('blur', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.userStats.rpgAttributes[index].name = $(this).val(); }); // Enable/disable RPG Attributes section toggle $('#rpg-show-rpg-attrs').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.showRPGAttributes = $(this).is(':checked'); }); // Show/hide level toggle $('#rpg-show-level').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.showLevel = $(this).is(':checked'); }); // Always send attributes toggle $('#rpg-always-send-attrs').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.alwaysSendAttributes = $(this).is(':checked'); }); // Status section toggles $('#rpg-status-enabled').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.statusSection.enabled = $(this).is(':checked'); }); $('#rpg-mood-emoji').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.statusSection.showMoodEmoji = $(this).is(':checked'); }); $('#rpg-status-fields').off('blur').on('blur', function () { const fields = $(this).val().split(',').map(f => f.trim()).filter(f => f); extensionSettings.trackerConfig.userStats.statusSection.customFields = fields; }); // Skills section toggles $('#rpg-skills-enabled').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.skillsSection.enabled = $(this).is(':checked'); }); $('#rpg-skills-label').off('blur').on('blur', function () { extensionSettings.trackerConfig.userStats.skillsSection.label = $(this).val(); saveSettings(); }); $('#rpg-skills-fields').off('blur').on('blur', function () { const fields = $(this).val().split(',').map(f => f.trim()).filter(f => f); extensionSettings.trackerConfig.userStats.skillsSection.customFields = fields; saveSettings(); }); } /** * Render Info Box configuration tab */ function renderInfoBoxTab() { const config = extensionSettings.trackerConfig.infoBox; let html = '
'; html += `

${i18n.getTranslation('template.trackerEditorModal.infoBoxTab.widgetsTitle')}

`; // Date widget html += '
'; html += ``; html += ``; html += ''; html += '
'; // Weather widget html += '
'; html += ``; html += ``; html += '
'; // Temperature widget html += '
'; html += ``; html += ``; html += '
'; html += ``; html += ``; html += '
'; html += '
'; // Time widget html += '
'; html += ``; html += ``; html += '
'; // Location widget html += '
'; html += ``; html += ``; html += '
'; // Recent Events widget html += '
'; html += ``; html += ``; html += '
'; html += '
'; $('#rpg-editor-tab-infoBox').html(html); setupInfoBoxListeners(); } /** * Set up event listeners for Info Box tab */ function setupInfoBoxListeners() { const widgets = extensionSettings.trackerConfig.infoBox.widgets; $('#rpg-widget-date').off('change').on('change', function () { widgets.date.enabled = $(this).is(':checked'); }); $('#rpg-date-format').off('change').on('change', function () { widgets.date.format = $(this).val(); }); $('#rpg-widget-weather').off('change').on('change', function () { widgets.weather.enabled = $(this).is(':checked'); }); $('#rpg-widget-temperature').off('change').on('change', function () { widgets.temperature.enabled = $(this).is(':checked'); }); $('input[name="temp-unit"]').off('change').on('change', function () { widgets.temperature.unit = $(this).val(); }); $('#rpg-widget-time').off('change').on('change', function () { widgets.time.enabled = $(this).is(':checked'); }); $('#rpg-widget-location').off('change').on('change', function () { widgets.location.enabled = $(this).is(':checked'); }); $('#rpg-widget-events').off('change').on('change', function () { widgets.recentEvents.enabled = $(this).is(':checked'); }); } /** * Render Present Characters configuration tab */ function renderPresentCharactersTab() { const config = extensionSettings.trackerConfig.presentCharacters; let html = '
'; // Relationship Fields Section html += `

${i18n.getTranslation('template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle')}

`; // Toggle for enabling/disabling relationships const relationshipsEnabled = config.relationships?.enabled !== false; // Default to true if not set html += '
'; html += ``; html += ``; html += '
'; html += `

${i18n.getTranslation('template.trackerEditorModal.presentCharactersTab.relationshipStatusHint')}

`; html += '
'; // Show existing relationships as field → emoji pairs const relationshipEmojis = config.relationships?.relationshipEmojis || config.relationshipEmojis || { 'Lover': '❤️', 'Friend': '⭐', 'Ally': '🤝', 'Enemy': '⚔️', 'Neutral': '⚖️' }; for (const [relationship, emoji] of Object.entries(relationshipEmojis)) { html += `
`; } html += '
'; html += ``; // Custom Fields Section html += `

${i18n.getTranslation('template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle')}

`; html += `

${i18n.getTranslation('template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint')}

`; html += '
'; config.customFields.forEach((field, index) => { html += `
`; }); html += '
'; html += ``; // Thoughts Section html += `

${i18n.getTranslation('template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle')}

`; html += '
'; html += ``; html += ``; html += '
'; html += '
'; html += '
'; html += ``; html += ``; html += '
'; html += '
'; html += ``; html += ``; html += '
'; html += '
'; // Character Stats html += `

${i18n.getTranslation('template.trackerEditorModal.presentCharactersTab.characterStatsTitle')}

`; html += '
'; html += ``; html += ``; html += '
'; html += `

${i18n.getTranslation('template.trackerEditorModal.presentCharactersTab.characterStatsHint')}

`; html += '
'; const charStats = config.characterStats?.customStats || []; charStats.forEach((stat, index) => { html += `
`; }); html += '
'; html += ``; html += '
'; $('#rpg-editor-tab-presentCharacters').html(html); setupPresentCharactersListeners(); } /** * Set up event listeners for Present Characters tab */ function setupPresentCharactersListeners() { // Relationships enabled toggle $('#rpg-relationships-enabled').off('change').on('change', function () { if (!extensionSettings.trackerConfig.presentCharacters.relationships) { extensionSettings.trackerConfig.presentCharacters.relationships = { enabled: true, relationshipEmojis: {} }; } extensionSettings.trackerConfig.presentCharacters.relationships.enabled = $(this).is(':checked'); }); // Add new relationship $('#rpg-add-relationship').off('click').on('click', function () { // Ensure relationships object exists if (!extensionSettings.trackerConfig.presentCharacters.relationships) { extensionSettings.trackerConfig.presentCharacters.relationships = { enabled: true, relationshipEmojis: {} }; } if (!extensionSettings.trackerConfig.presentCharacters.relationships.relationshipEmojis) { extensionSettings.trackerConfig.presentCharacters.relationships.relationshipEmojis = {}; } // Generate a unique relationship name let baseName = 'New Relationship'; let relationshipName = baseName; let counter = 1; const existingRelationships = extensionSettings.trackerConfig.presentCharacters.relationships.relationshipEmojis; while (existingRelationships[relationshipName]) { counter++; relationshipName = `${baseName} ${counter}`; } // Add to new structure extensionSettings.trackerConfig.presentCharacters.relationships.relationshipEmojis[relationshipName] = '😊'; // Also update legacy fields for backward compatibility if (!extensionSettings.trackerConfig.presentCharacters.relationshipEmojis) { extensionSettings.trackerConfig.presentCharacters.relationshipEmojis = {}; } extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[relationshipName] = '😊'; // Sync relationshipFields const emojis = extensionSettings.trackerConfig.presentCharacters.relationships.relationshipEmojis; extensionSettings.trackerConfig.presentCharacters.relationshipFields = Object.keys(emojis); renderPresentCharactersTab(); }); // Remove relationship $('.rpg-remove-relationship').off('click').on('click', function () { const relationship = $(this).data('relationship'); // Remove from new structure if (extensionSettings.trackerConfig.presentCharacters.relationships?.relationshipEmojis) { delete extensionSettings.trackerConfig.presentCharacters.relationships.relationshipEmojis[relationship]; } // Remove from legacy structure if (extensionSettings.trackerConfig.presentCharacters.relationshipEmojis) { delete extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[relationship]; } // Sync relationshipFields const emojis = extensionSettings.trackerConfig.presentCharacters.relationships?.relationshipEmojis || {}; extensionSettings.trackerConfig.presentCharacters.relationshipFields = Object.keys(emojis); renderPresentCharactersTab(); }); // Update relationship name $('.rpg-relationship-name').off('blur').on('blur', function () { const newName = $(this).val(); const $item = $(this).closest('.rpg-relationship-item'); const emoji = $item.find('.rpg-relationship-emoji').val(); // Ensure structures exist if (!extensionSettings.trackerConfig.presentCharacters.relationships) { extensionSettings.trackerConfig.presentCharacters.relationships = { enabled: true, relationshipEmojis: {} }; } if (!extensionSettings.trackerConfig.presentCharacters.relationshipEmojis) { extensionSettings.trackerConfig.presentCharacters.relationshipEmojis = {}; } // Find the old name by matching the emoji in new structure const emojis = extensionSettings.trackerConfig.presentCharacters.relationships.relationshipEmojis; const oldName = Object.keys(emojis).find( key => emojis[key] === emoji && key !== newName ); if (oldName && oldName !== newName) { // Update new structure delete emojis[oldName]; emojis[newName] = emoji; // Update legacy structure delete extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[oldName]; extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[newName] = emoji; // Sync relationshipFields extensionSettings.trackerConfig.presentCharacters.relationshipFields = Object.keys(emojis); } }); // Update relationship emoji $('.rpg-relationship-emoji').off('blur').on('blur', function () { const name = $(this).closest('.rpg-relationship-item').find('.rpg-relationship-name').val(); // Ensure structures exist if (!extensionSettings.trackerConfig.presentCharacters.relationships) { extensionSettings.trackerConfig.presentCharacters.relationships = { enabled: true, relationshipEmojis: {} }; } if (!extensionSettings.trackerConfig.presentCharacters.relationshipEmojis) { extensionSettings.trackerConfig.presentCharacters.relationshipEmojis = {}; } // Update both structures extensionSettings.trackerConfig.presentCharacters.relationships.relationshipEmojis[name] = $(this).val(); extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[name] = $(this).val(); }); // Thoughts configuration $('#rpg-thoughts-enabled').off('change').on('change', function () { if (!extensionSettings.trackerConfig.presentCharacters.thoughts) { extensionSettings.trackerConfig.presentCharacters.thoughts = {}; } extensionSettings.trackerConfig.presentCharacters.thoughts.enabled = $(this).is(':checked'); }); $('#rpg-thoughts-name').off('blur').on('blur', function () { if (!extensionSettings.trackerConfig.presentCharacters.thoughts) { extensionSettings.trackerConfig.presentCharacters.thoughts = {}; } extensionSettings.trackerConfig.presentCharacters.thoughts.name = $(this).val(); }); $('#rpg-thoughts-description').off('blur').on('blur', function () { if (!extensionSettings.trackerConfig.presentCharacters.thoughts) { extensionSettings.trackerConfig.presentCharacters.thoughts = {}; } extensionSettings.trackerConfig.presentCharacters.thoughts.description = $(this).val(); }); // Add field $('#rpg-add-field').off('click').on('click', function () { extensionSettings.trackerConfig.presentCharacters.customFields.push({ id: 'custom_' + Date.now(), name: 'New Field', enabled: true, description: 'Description for AI' }); renderPresentCharactersTab(); }); // Remove field $('.rpg-field-remove').off('click').on('click', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.presentCharacters.customFields.splice(index, 1); renderPresentCharactersTab(); }); // Move field up $('.rpg-field-move-up').off('click').on('click', function () { const index = $(this).data('index'); if (index > 0) { const fields = extensionSettings.trackerConfig.presentCharacters.customFields; [fields[index - 1], fields[index]] = [fields[index], fields[index - 1]]; renderPresentCharactersTab(); } }); // Move field down $('.rpg-field-move-down').off('click').on('click', function () { const index = $(this).data('index'); const fields = extensionSettings.trackerConfig.presentCharacters.customFields; if (index < fields.length - 1) { [fields[index], fields[index + 1]] = [fields[index + 1], fields[index]]; renderPresentCharactersTab(); } }); // Toggle field $('.rpg-field-toggle').off('change').on('change', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.presentCharacters.customFields[index].enabled = $(this).is(':checked'); }); // Rename field $('.rpg-field-label').off('blur').on('blur', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.presentCharacters.customFields[index].name = $(this).val(); }); // Update description $('.rpg-field-placeholder').off('blur').on('blur', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.presentCharacters.customFields[index].description = $(this).val(); }); // Character stats toggle $('#rpg-char-stats-enabled').off('change').on('change', function () { if (!extensionSettings.trackerConfig.presentCharacters.characterStats) { extensionSettings.trackerConfig.presentCharacters.characterStats = { enabled: false, customStats: [] }; } extensionSettings.trackerConfig.presentCharacters.characterStats.enabled = $(this).is(':checked'); }); // Add character stat $('#rpg-add-char-stat').off('click').on('click', function () { if (!extensionSettings.trackerConfig.presentCharacters.characterStats) { extensionSettings.trackerConfig.presentCharacters.characterStats = { enabled: false, customStats: [] }; } if (!extensionSettings.trackerConfig.presentCharacters.characterStats.customStats) { extensionSettings.trackerConfig.presentCharacters.characterStats.customStats = []; } extensionSettings.trackerConfig.presentCharacters.characterStats.customStats.push({ id: `stat-${Date.now()}`, name: 'New Stat', enabled: true }); renderPresentCharactersTab(); }); // Remove character stat $('.rpg-char-stat-remove').off('click').on('click', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.presentCharacters.characterStats.customStats.splice(index, 1); renderPresentCharactersTab(); }); // Toggle character stat $('.rpg-char-stat-toggle').off('change').on('change', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].enabled = $(this).is(':checked'); }); // Rename character stat $('.rpg-char-stat-label').off('blur').on('blur', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].name = $(this).val(); }); } /** * Render History Persistence configuration tab * Allows users to select which tracker data should be injected into historical messages */ function renderHistoryPersistenceTab() { const historyPersistence = extensionSettings.historyPersistence || { enabled: false, messageCount: 5, injectionPosition: 'assistant_message_end', contextPreamble: '', sendAllEnabledOnRefresh: false }; const userStatsConfig = extensionSettings.trackerConfig.userStats; const infoBoxConfig = extensionSettings.trackerConfig.infoBox; const presentCharsConfig = extensionSettings.trackerConfig.presentCharacters; const generationMode = extensionSettings.generationMode || 'together'; let html = '
'; // Main toggle and settings html += `

History Persistence Settings

`; html += `

Inject selected tracker data into historical messages to help the AI maintain continuity for time-sensitive events, weather changes, and location tracking.

`; // Enable toggle html += '
'; html += ``; html += ``; html += '
'; // External API Only toggle - only show for separate/external modes if (generationMode === 'separate' || generationMode === 'external') { html += '
'; html += ``; html += ``; html += '
'; html += `

When enabled, Refresh RPG Info will include all enabled stats from the preset in history context, ignoring the individual selections below.

`; } // Message count html += '
'; html += ``; html += ``; html += '
'; // Injection position html += '
'; html += ``; html += ``; html += '
'; // Custom preamble html += '
'; html += ``; html += ``; html += '
'; // User Stats section - which stats to persist html += `

User Stats

`; html += `

Select which stats should be included in historical messages.

`; // Custom stats html += '
'; userStatsConfig.customStats.forEach((stat, index) => { if (stat.enabled) { html += `
`; } }); // Status section if (userStatsConfig.statusSection?.enabled) { html += `
`; } // Skills section if (userStatsConfig.skillsSection?.enabled) { html += `
`; } // Inventory html += `
`; // Quests html += `
`; html += '
'; // Info Box section - which widgets to persist html += `

Info Box

`; html += `

Select which info box fields should be included in historical messages. These are recommended for time tracking.

`; html += '
'; const widgetLabels = { date: 'Date', weather: 'Weather', temperature: 'Temperature', time: 'Time', location: 'Location', recentEvents: 'Recent Events' }; for (const [widgetId, widget] of Object.entries(infoBoxConfig.widgets)) { if (widget.enabled) { html += `
`; } } html += '
'; // Present Characters section html += `

Present Characters

`; html += `

Select which character fields should be included in historical messages.

`; html += '
'; // Custom fields (appearance, demeanor, etc.) presentCharsConfig.customFields.forEach((field, index) => { if (field.enabled) { html += `
`; } }); // Thoughts if (presentCharsConfig.thoughts?.enabled) { html += `
`; } html += '
'; html += '
'; $('#rpg-editor-tab-historyPersistence').html(html); setupHistoryPersistenceListeners(); } /** * Set up event listeners for History Persistence tab */ function setupHistoryPersistenceListeners() { // Ensure historyPersistence object exists if (!extensionSettings.historyPersistence) { extensionSettings.historyPersistence = { enabled: false, messageCount: 5, injectionPosition: 'assistant_message_end', contextPreamble: '', externalApiOnly: false }; } // Main toggle $('#rpg-history-persistence-enabled').off('change').on('change', function () { extensionSettings.historyPersistence.enabled = $(this).is(':checked'); }); // Send All Enabled on Refresh toggle $('#rpg-history-send-all-enabled').off('change').on('change', function () { extensionSettings.historyPersistence.sendAllEnabledOnRefresh = $(this).is(':checked'); }); // Message count $('#rpg-history-message-count').off('change').on('change', function () { extensionSettings.historyPersistence.messageCount = parseInt($(this).val()) || 0; }); // Injection position $('#rpg-history-injection-position').off('change').on('change', function () { extensionSettings.historyPersistence.injectionPosition = $(this).val(); }); // Context preamble $('#rpg-history-context-preamble').off('blur').on('blur', function () { extensionSettings.historyPersistence.contextPreamble = $(this).val(); }); // User Stats toggles $('.rpg-history-stat-toggle').off('change').on('change', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.userStats.customStats[index].persistInHistory = $(this).is(':checked'); }); // Status section $('#rpg-history-status').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.statusSection.persistInHistory = $(this).is(':checked'); }); // Skills section $('#rpg-history-skills').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.skillsSection.persistInHistory = $(this).is(':checked'); }); // Inventory $('#rpg-history-inventory').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.inventoryPersistInHistory = $(this).is(':checked'); }); // Quests $('#rpg-history-quests').off('change').on('change', function () { extensionSettings.trackerConfig.userStats.questsPersistInHistory = $(this).is(':checked'); }); // Info Box widget toggles $('.rpg-history-widget-toggle').off('change').on('change', function () { const widgetId = $(this).data('widget'); extensionSettings.trackerConfig.infoBox.widgets[widgetId].persistInHistory = $(this).is(':checked'); }); // Present Characters field toggles $('.rpg-history-charfield-toggle').off('change').on('change', function () { const index = $(this).data('index'); extensionSettings.trackerConfig.presentCharacters.customFields[index].persistInHistory = $(this).is(':checked'); }); // Thoughts $('#rpg-history-thoughts').off('change').on('change', function () { extensionSettings.trackerConfig.presentCharacters.thoughts.persistInHistory = $(this).is(':checked'); }); }