Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 995f3a7a98 | |||
| 681b8ba2bc | |||
| 2d961936c2 | |||
| b534bd4c71 | |||
| 73cbb27713 | |||
| db2bed16a7 | |||
| ecb5d74d6e | |||
| fea59efe4e | |||
| b43cca5b6f | |||
| 94f562f1bb | |||
| 3d5fc5fee1 | |||
| 98ef751a9f | |||
| f5641ec1f0 | |||
| 0320c3fdd5 | |||
| 3d0ebe4694 | |||
| 510723cac4 | |||
| f6733f87a2 | |||
| ddc02d9bbc | |||
| 659b5bb82b | |||
| 5f72e6f549 |
@@ -7,11 +7,13 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
|
||||
|
||||
## 🆕 What's New
|
||||
|
||||
### v3.3.1
|
||||
### v3.4.0
|
||||
|
||||
- Thought bubble can now be collapsed into an icon.
|
||||
- Fixed a bug for Past Events being parsed incorrectly.
|
||||
- Added event emission on when the tracker generation is complete.
|
||||
- Added History Persistance in Edit Trackers that allows you to control how many past trackers in the chat history to include.
|
||||
- New mobile displays were added that show all the most important trackers from the panel as small, floating widgets around the RPG Companion button, when the main panel is closed.
|
||||
- Added CYOA toggle.
|
||||
- Added Deception System toggle.
|
||||
- The trackers are no longer sent together with an image generation request.
|
||||
|
||||
**Special thanks to all the other contributors for this project:**
|
||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
||||
|
||||
@@ -64,6 +64,7 @@ import { renderInfoBox, updateInfoBoxField } from './src/systems/rendering/infoB
|
||||
import {
|
||||
renderThoughts,
|
||||
updateCharacterField,
|
||||
removeCharacter,
|
||||
updateChatThoughts,
|
||||
createThoughtPanel
|
||||
} from './src/systems/rendering/thoughts.js';
|
||||
@@ -125,7 +126,8 @@ import {
|
||||
removeMobileTabs,
|
||||
setupMobileKeyboardHandling,
|
||||
setupContentEditableScrolling,
|
||||
updateMobileTabLabels
|
||||
updateMobileTabLabels,
|
||||
updateFabWidgets
|
||||
} from './src/systems/ui/mobile.js';
|
||||
import {
|
||||
setupDesktopTabs,
|
||||
@@ -375,6 +377,16 @@ async function initUI() {
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-deception').on('change', function() {
|
||||
extensionSettings.enableDeceptionSystem = $(this).prop('checked');
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-cyoa').on('change', function() {
|
||||
extensionSettings.enableCYOA = $(this).prop('checked');
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-spotify-music').on('change', function() {
|
||||
extensionSettings.enableSpotifyMusic = $(this).prop('checked');
|
||||
saveSettings();
|
||||
@@ -552,6 +564,18 @@ async function initUI() {
|
||||
updateFeatureTogglesVisibility();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-show-deception-toggle').on('change', function() {
|
||||
extensionSettings.showDeceptionToggle = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFeatureTogglesVisibility();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-show-cyoa-toggle').on('change', function() {
|
||||
extensionSettings.showCYOAToggle = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFeatureTogglesVisibility();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-show-spotify-toggle').on('change', function() {
|
||||
extensionSettings.showSpotifyToggle = $(this).prop('checked');
|
||||
saveSettings();
|
||||
@@ -607,6 +631,71 @@ async function initUI() {
|
||||
updateDiceDisplay();
|
||||
});
|
||||
|
||||
// Mobile FAB Widget toggles - simplified, no position saving (auto-positioned)
|
||||
$('#rpg-toggle-fab-widgets-enabled').on('change', function() {
|
||||
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||
extensionSettings.mobileFabWidgets.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFabWidgets();
|
||||
$('#rpg-fab-widget-options').toggle(extensionSettings.mobileFabWidgets.enabled);
|
||||
});
|
||||
|
||||
$('#rpg-toggle-fab-weather-icon').on('change', function() {
|
||||
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||
if (!extensionSettings.mobileFabWidgets.weatherIcon) extensionSettings.mobileFabWidgets.weatherIcon = {};
|
||||
extensionSettings.mobileFabWidgets.weatherIcon.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-fab-weather-desc').on('change', function() {
|
||||
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||
if (!extensionSettings.mobileFabWidgets.weatherDesc) extensionSettings.mobileFabWidgets.weatherDesc = {};
|
||||
extensionSettings.mobileFabWidgets.weatherDesc.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-fab-clock').on('change', function() {
|
||||
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||
if (!extensionSettings.mobileFabWidgets.clock) extensionSettings.mobileFabWidgets.clock = {};
|
||||
extensionSettings.mobileFabWidgets.clock.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-fab-date').on('change', function() {
|
||||
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||
if (!extensionSettings.mobileFabWidgets.date) extensionSettings.mobileFabWidgets.date = {};
|
||||
extensionSettings.mobileFabWidgets.date.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-fab-location').on('change', function() {
|
||||
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||
if (!extensionSettings.mobileFabWidgets.location) extensionSettings.mobileFabWidgets.location = {};
|
||||
extensionSettings.mobileFabWidgets.location.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-fab-stats').on('change', function() {
|
||||
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||
if (!extensionSettings.mobileFabWidgets.stats) extensionSettings.mobileFabWidgets.stats = {};
|
||||
extensionSettings.mobileFabWidgets.stats.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-fab-attributes').on('change', function() {
|
||||
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||
if (!extensionSettings.mobileFabWidgets.attributes) extensionSettings.mobileFabWidgets.attributes = {};
|
||||
extensionSettings.mobileFabWidgets.attributes.enabled = $(this).prop('checked');
|
||||
saveSettings();
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
$('#rpg-manual-update').on('click', async function() {
|
||||
if (!extensionSettings.enabled) {
|
||||
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
||||
@@ -784,6 +873,8 @@ async function initUI() {
|
||||
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
|
||||
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
|
||||
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
|
||||
$('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false);
|
||||
$('#rpg-toggle-cyoa').prop('checked', extensionSettings.enableCYOA ?? false);
|
||||
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
|
||||
|
||||
$('#rpg-toggle-dynamic-weather').prop('checked', extensionSettings.enableDynamicWeather);
|
||||
@@ -792,6 +883,8 @@ async function initUI() {
|
||||
// Feature toggle visibility settings
|
||||
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
|
||||
$('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true);
|
||||
$('#rpg-toggle-show-deception-toggle').prop('checked', extensionSettings.showDeceptionToggle ?? true);
|
||||
$('#rpg-toggle-show-cyoa-toggle').prop('checked', extensionSettings.showCYOAToggle ?? true);
|
||||
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true);
|
||||
$('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true);
|
||||
$('#rpg-toggle-show-narrator-mode').prop('checked', extensionSettings.showNarratorMode ?? true);
|
||||
@@ -824,6 +917,20 @@ async function initUI() {
|
||||
$('#rpg-toggle-auto-avatars-panel').prop('checked', extensionSettings.autoGenerateAvatars || false);
|
||||
|
||||
$('#rpg-toggle-dice-display').prop('checked', extensionSettings.showDiceDisplay);
|
||||
|
||||
// Initialize Mobile FAB Widget checkboxes
|
||||
const fabWidgets = extensionSettings.mobileFabWidgets || {};
|
||||
$('#rpg-toggle-fab-widgets-enabled').prop('checked', fabWidgets.enabled || false);
|
||||
$('#rpg-toggle-fab-weather-icon').prop('checked', fabWidgets.weatherIcon?.enabled || false);
|
||||
$('#rpg-toggle-fab-weather-desc').prop('checked', fabWidgets.weatherDesc?.enabled || false);
|
||||
$('#rpg-toggle-fab-clock').prop('checked', fabWidgets.clock?.enabled || false);
|
||||
$('#rpg-toggle-fab-date').prop('checked', fabWidgets.date?.enabled || false);
|
||||
$('#rpg-toggle-fab-location').prop('checked', fabWidgets.location?.enabled || false);
|
||||
$('#rpg-toggle-fab-stats').prop('checked', fabWidgets.stats?.enabled || false);
|
||||
$('#rpg-toggle-fab-attributes').prop('checked', fabWidgets.attributes?.enabled || false);
|
||||
// Toggle visibility of widget options based on master toggle
|
||||
$('#rpg-fab-widget-options').toggle(fabWidgets.enabled || false);
|
||||
|
||||
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
|
||||
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
|
||||
$('#rpg-theme-select').val(extensionSettings.theme);
|
||||
@@ -847,7 +954,6 @@ async function initUI() {
|
||||
|
||||
$('#rpg-generation-mode').val(extensionSettings.generationMode);
|
||||
$('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided);
|
||||
$('#rpg-save-tracker-history').prop('checked', extensionSettings.saveTrackerHistory);
|
||||
|
||||
updatePanelVisibility();
|
||||
updateSectionVisibility();
|
||||
@@ -968,6 +1074,8 @@ jQuery(async () => {
|
||||
// Load chat-specific data for current chat
|
||||
try {
|
||||
loadChatData();
|
||||
// Initialize FAB widgets with any loaded data
|
||||
updateFabWidgets();
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Chat data load failed, using defaults:', error);
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Marinara",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.2",
|
||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||
}
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
|
||||
v3.3.1
|
||||
v3.4.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+436
-3
@@ -100,6 +100,24 @@ export function loadSettings() {
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
// Migration to version 4: Enable FAB widgets by default
|
||||
if (currentVersion < 4) {
|
||||
// console.log('[RPG Companion] Migrating settings to version 4 (enabling FAB widgets)');
|
||||
if (!extensionSettings.mobileFabWidgets) {
|
||||
extensionSettings.mobileFabWidgets = {};
|
||||
}
|
||||
extensionSettings.mobileFabWidgets.enabled = true;
|
||||
extensionSettings.mobileFabWidgets.weatherIcon = { enabled: true };
|
||||
extensionSettings.mobileFabWidgets.weatherDesc = { enabled: true };
|
||||
extensionSettings.mobileFabWidgets.clock = { enabled: true };
|
||||
extensionSettings.mobileFabWidgets.date = { enabled: true };
|
||||
extensionSettings.mobileFabWidgets.location = { enabled: true };
|
||||
extensionSettings.mobileFabWidgets.stats = { enabled: true };
|
||||
extensionSettings.mobileFabWidgets.attributes = { enabled: true };
|
||||
extensionSettings.settingsVersion = 4;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
// Save migrated settings
|
||||
if (settingsChanged) {
|
||||
saveSettings();
|
||||
@@ -126,6 +144,9 @@ export function loadSettings() {
|
||||
migrateToTrackerConfig();
|
||||
saveSettings(); // Persist migration
|
||||
}
|
||||
|
||||
// Migrate to preset manager system if presets don't exist
|
||||
migrateToPresetManager();
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Error loading settings:', error);
|
||||
console.error('[RPG Companion] Error details:', error.message, error.stack);
|
||||
@@ -455,7 +476,7 @@ function migrateToTrackerConfig() {
|
||||
{ id: 'physicalState', label: 'Physical State', enabled: true, placeholder: 'Visible Physical State (up to three traits)' },
|
||||
{ id: 'demeanor', label: 'Demeanor Cue', enabled: true, placeholder: 'Observable Demeanor Cue (one trait)' },
|
||||
{ id: 'relationship', label: 'Relationship', enabled: true, type: 'relationship', placeholder: 'Enemy/Neutral/Friend/Lover' },
|
||||
{ id: 'internalMonologue', label: 'Internal Monologue', enabled: true, placeholder: 'Internal Monologue (in first person POV, up to three sentences long)' }
|
||||
{ id: 'internalMonologue', label: 'Internal Monologue', enabled: true, placeholder: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)' }
|
||||
],
|
||||
characterStats: {
|
||||
enabled: false,
|
||||
@@ -555,7 +576,7 @@ function migrateToTrackerConfig() {
|
||||
const thoughts = {
|
||||
enabled: thoughtsField ? (thoughtsField.enabled !== false) : true,
|
||||
name: 'Thoughts',
|
||||
description: thoughtsField?.placeholder || 'Internal monologue (in first person POV, up to three sentences long)'
|
||||
description: thoughtsField?.placeholder || 'Internal Monologue (in first person from character\'s POV, up to three sentences long)'
|
||||
};
|
||||
|
||||
// Update to new structure
|
||||
@@ -601,8 +622,420 @@ function migrateToTrackerConfig() {
|
||||
pc.thoughts = {
|
||||
enabled: true,
|
||||
name: 'Thoughts',
|
||||
description: 'Internal monologue (in first person POV, up to three sentences long)'
|
||||
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Preset Management Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Gets the entity key for the current character or group
|
||||
* @returns {string|null} Entity key in format "char_{id}" or "group_{id}", or null if no character selected
|
||||
*/
|
||||
export function getCurrentEntityKey() {
|
||||
const context = getContext();
|
||||
if (context.groupId) {
|
||||
return `group_${context.groupId}`;
|
||||
} else if (context.characterId !== undefined && context.characterId !== null) {
|
||||
return `char_${context.characterId}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name for the current character or group
|
||||
* @returns {string} Display name for the current entity
|
||||
*/
|
||||
export function getCurrentEntityName() {
|
||||
const context = getContext();
|
||||
if (context.groupId) {
|
||||
const group = context.groups?.find(g => g.id === context.groupId);
|
||||
return group?.name || 'Group Chat';
|
||||
} else if (context.characterId !== undefined && context.characterId !== null) {
|
||||
return context.name2 || 'Character';
|
||||
}
|
||||
return 'No Character';
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates existing trackerConfig to the preset system if presetManager doesn't exist
|
||||
* Creates a "Default" preset from the current trackerConfig
|
||||
*/
|
||||
export function migrateToPresetManager() {
|
||||
if (!extensionSettings.presetManager || Object.keys(extensionSettings.presetManager.presets || {}).length === 0) {
|
||||
// console.log('[RPG Companion] Migrating to preset manager system');
|
||||
|
||||
// Initialize presetManager if it doesn't exist
|
||||
if (!extensionSettings.presetManager) {
|
||||
extensionSettings.presetManager = {
|
||||
presets: {},
|
||||
characterAssociations: {},
|
||||
activePresetId: null,
|
||||
defaultPresetId: null
|
||||
};
|
||||
}
|
||||
|
||||
// Create default preset from existing trackerConfig
|
||||
const defaultPresetId = 'preset_default';
|
||||
extensionSettings.presetManager.presets[defaultPresetId] = {
|
||||
id: defaultPresetId,
|
||||
name: 'Default',
|
||||
trackerConfig: JSON.parse(JSON.stringify(extensionSettings.trackerConfig))
|
||||
};
|
||||
extensionSettings.presetManager.activePresetId = defaultPresetId;
|
||||
extensionSettings.presetManager.defaultPresetId = defaultPresetId;
|
||||
|
||||
// console.log('[RPG Companion] Created Default preset from existing trackerConfig');
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available presets
|
||||
* @returns {Object} Map of preset ID to preset data
|
||||
*/
|
||||
export function getPresets() {
|
||||
return extensionSettings.presetManager?.presets || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific preset by ID
|
||||
* @param {string} presetId - The preset ID
|
||||
* @returns {Object|null} The preset object or null if not found
|
||||
*/
|
||||
export function getPreset(presetId) {
|
||||
return extensionSettings.presetManager?.presets?.[presetId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently active preset ID
|
||||
* @returns {string|null} The active preset ID or null
|
||||
*/
|
||||
export function getActivePresetId() {
|
||||
return extensionSettings.presetManager?.activePresetId || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default preset ID
|
||||
* @returns {string|null} The default preset ID or null
|
||||
*/
|
||||
export function getDefaultPresetId() {
|
||||
return extensionSettings.presetManager?.defaultPresetId || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a preset as the default
|
||||
* @param {string} presetId - The preset ID to set as default
|
||||
*/
|
||||
export function setDefaultPreset(presetId) {
|
||||
if (extensionSettings.presetManager.presets[presetId]) {
|
||||
extensionSettings.presetManager.defaultPresetId = presetId;
|
||||
saveSettings();
|
||||
// console.log(`[RPG Companion] Set preset ${presetId} as default`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given preset is the default
|
||||
* @param {string} presetId - The preset ID to check
|
||||
* @returns {boolean} True if it's the default preset
|
||||
*/
|
||||
export function isDefaultPreset(presetId) {
|
||||
return extensionSettings.presetManager?.defaultPresetId === presetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new preset from the current trackerConfig
|
||||
* @param {string} name - Name for the new preset
|
||||
* @returns {string} The ID of the newly created preset
|
||||
*/
|
||||
export function createPreset(name) {
|
||||
const presetId = `preset_${Date.now()}`;
|
||||
extensionSettings.presetManager.presets[presetId] = {
|
||||
id: presetId,
|
||||
name: name,
|
||||
trackerConfig: JSON.parse(JSON.stringify(extensionSettings.trackerConfig)),
|
||||
historyPersistence: extensionSettings.historyPersistence
|
||||
? JSON.parse(JSON.stringify(extensionSettings.historyPersistence))
|
||||
: null
|
||||
};
|
||||
// Also set it as the active preset so edits go to the new preset
|
||||
extensionSettings.presetManager.activePresetId = presetId;
|
||||
saveSettings();
|
||||
// console.log(`[RPG Companion] Created preset "${name}" with ID ${presetId}`);
|
||||
return presetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current trackerConfig and historyPersistence to the specified preset
|
||||
* @param {string} presetId - The preset ID to save to
|
||||
*/
|
||||
export function saveToPreset(presetId) {
|
||||
const preset = extensionSettings.presetManager.presets[presetId];
|
||||
if (preset) {
|
||||
preset.trackerConfig = JSON.parse(JSON.stringify(extensionSettings.trackerConfig));
|
||||
preset.historyPersistence = extensionSettings.historyPersistence
|
||||
? JSON.parse(JSON.stringify(extensionSettings.historyPersistence))
|
||||
: null;
|
||||
saveSettings();
|
||||
// console.log(`[RPG Companion] Saved current config to preset "${preset.name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a preset's trackerConfig and historyPersistence as the active configuration
|
||||
* @param {string} presetId - The preset ID to load
|
||||
* @returns {boolean} True if loaded successfully, false otherwise
|
||||
*/
|
||||
export function loadPreset(presetId) {
|
||||
const preset = extensionSettings.presetManager.presets[presetId];
|
||||
if (preset && preset.trackerConfig) {
|
||||
extensionSettings.trackerConfig = JSON.parse(JSON.stringify(preset.trackerConfig));
|
||||
// Load historyPersistence if present, otherwise use defaults
|
||||
if (preset.historyPersistence) {
|
||||
extensionSettings.historyPersistence = JSON.parse(JSON.stringify(preset.historyPersistence));
|
||||
} else {
|
||||
// Default values for presets that don't have historyPersistence yet
|
||||
extensionSettings.historyPersistence = {
|
||||
enabled: false,
|
||||
messageCount: 5,
|
||||
injectionPosition: 'assistant_message_end',
|
||||
contextPreamble: ''
|
||||
};
|
||||
}
|
||||
extensionSettings.presetManager.activePresetId = presetId;
|
||||
saveSettings();
|
||||
// console.log(`[RPG Companion] Loaded preset "${preset.name}"`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a preset
|
||||
* @param {string} presetId - The preset ID to rename
|
||||
* @param {string} newName - The new name for the preset
|
||||
*/
|
||||
export function renamePreset(presetId, newName) {
|
||||
const preset = extensionSettings.presetManager.presets[presetId];
|
||||
if (preset) {
|
||||
preset.name = newName;
|
||||
saveSettings();
|
||||
// console.log(`[RPG Companion] Renamed preset to "${newName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a preset
|
||||
* @param {string} presetId - The preset ID to delete
|
||||
* @returns {boolean} True if deleted, false if it's the last preset (can't delete)
|
||||
*/
|
||||
export function deletePreset(presetId) {
|
||||
const presets = extensionSettings.presetManager.presets;
|
||||
const presetIds = Object.keys(presets);
|
||||
|
||||
// Don't delete if it's the last preset
|
||||
if (presetIds.length <= 1) {
|
||||
// console.warn('[RPG Companion] Cannot delete the last preset');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove any character associations using this preset
|
||||
const associations = extensionSettings.presetManager.characterAssociations;
|
||||
for (const entityKey of Object.keys(associations)) {
|
||||
if (associations[entityKey] === presetId) {
|
||||
delete associations[entityKey];
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the preset
|
||||
delete presets[presetId];
|
||||
|
||||
// If the deleted preset was active, switch to the first available preset
|
||||
if (extensionSettings.presetManager.activePresetId === presetId) {
|
||||
const remainingIds = Object.keys(presets);
|
||||
if (remainingIds.length > 0) {
|
||||
loadPreset(remainingIds[0]);
|
||||
}
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
// console.log(`[RPG Companion] Deleted preset ${presetId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the current preset with the current character/group
|
||||
*/
|
||||
export function associatePresetWithCurrentEntity() {
|
||||
const entityKey = getCurrentEntityKey();
|
||||
const activePresetId = extensionSettings.presetManager.activePresetId;
|
||||
|
||||
if (entityKey && activePresetId) {
|
||||
extensionSettings.presetManager.characterAssociations[entityKey] = activePresetId;
|
||||
saveSettings();
|
||||
// console.log(`[RPG Companion] Associated preset ${activePresetId} with ${entityKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the preset association for the current character/group
|
||||
*/
|
||||
export function removePresetAssociationForCurrentEntity() {
|
||||
const entityKey = getCurrentEntityKey();
|
||||
if (entityKey && extensionSettings.presetManager.characterAssociations[entityKey]) {
|
||||
delete extensionSettings.presetManager.characterAssociations[entityKey];
|
||||
saveSettings();
|
||||
// console.log(`[RPG Companion] Removed preset association for ${entityKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the preset ID associated with the current character/group
|
||||
* @returns {string|null} The associated preset ID or null
|
||||
*/
|
||||
export function getPresetForCurrentEntity() {
|
||||
const entityKey = getCurrentEntityKey();
|
||||
if (entityKey) {
|
||||
return extensionSettings.presetManager.characterAssociations[entityKey] || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current character/group has a preset association
|
||||
* @returns {boolean} True if there's an association
|
||||
*/
|
||||
export function hasPresetAssociation() {
|
||||
const entityKey = getCurrentEntityKey();
|
||||
return entityKey && extensionSettings.presetManager.characterAssociations[entityKey] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-switches to the preset associated with the current character/group
|
||||
* Called when character changes. Falls back to default preset if no association.
|
||||
* @returns {boolean} True if a preset was switched, false otherwise
|
||||
*/
|
||||
export function autoSwitchPresetForEntity() {
|
||||
const associatedPresetId = getPresetForCurrentEntity();
|
||||
|
||||
// If there's a character-specific preset, use it
|
||||
if (associatedPresetId && associatedPresetId !== extensionSettings.presetManager.activePresetId) {
|
||||
// Check if the preset still exists
|
||||
if (extensionSettings.presetManager.presets[associatedPresetId]) {
|
||||
return loadPreset(associatedPresetId);
|
||||
} else {
|
||||
// Preset was deleted, remove the stale association
|
||||
removePresetAssociationForCurrentEntity();
|
||||
}
|
||||
}
|
||||
|
||||
// No character association - fall back to default preset if set
|
||||
if (!associatedPresetId) {
|
||||
const defaultPresetId = extensionSettings.presetManager.defaultPresetId;
|
||||
if (defaultPresetId &&
|
||||
defaultPresetId !== extensionSettings.presetManager.activePresetId &&
|
||||
extensionSettings.presetManager.presets[defaultPresetId]) {
|
||||
return loadPreset(defaultPresetId);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports presets for sharing (without character associations)
|
||||
* @param {string[]} presetIds - Array of preset IDs to export, or empty for all
|
||||
* @returns {Object} Export data object
|
||||
*/
|
||||
export function exportPresets(presetIds = []) {
|
||||
const presetsToExport = {};
|
||||
const allPresets = extensionSettings.presetManager.presets;
|
||||
|
||||
// If no specific IDs provided, export all
|
||||
const idsToExport = presetIds.length > 0 ? presetIds : Object.keys(allPresets);
|
||||
|
||||
for (const id of idsToExport) {
|
||||
if (allPresets[id]) {
|
||||
presetsToExport[id] = {
|
||||
id: allPresets[id].id,
|
||||
name: allPresets[id].name,
|
||||
trackerConfig: allPresets[id].trackerConfig
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
presets: presetsToExport
|
||||
// Note: characterAssociations are intentionally NOT exported
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports presets from an export file
|
||||
* @param {Object} importData - The imported data object
|
||||
* @param {boolean} overwrite - If true, overwrites existing presets with same name
|
||||
* @returns {number} Number of presets imported
|
||||
*/
|
||||
export function importPresets(importData, overwrite = false) {
|
||||
if (!importData.presets || typeof importData.presets !== 'object') {
|
||||
throw new Error('Invalid import data: missing presets');
|
||||
}
|
||||
|
||||
let importCount = 0;
|
||||
const existingNames = new Set(
|
||||
Object.values(extensionSettings.presetManager.presets).map(p => p.name.toLowerCase())
|
||||
);
|
||||
|
||||
for (const [originalId, preset] of Object.entries(importData.presets)) {
|
||||
if (!preset.name || !preset.trackerConfig) {
|
||||
continue; // Skip invalid presets
|
||||
}
|
||||
|
||||
let name = preset.name;
|
||||
const nameLower = name.toLowerCase();
|
||||
|
||||
// Check for name collision
|
||||
if (existingNames.has(nameLower)) {
|
||||
if (overwrite) {
|
||||
// Find and delete the existing preset with this name
|
||||
for (const [existingId, existingPreset] of Object.entries(extensionSettings.presetManager.presets)) {
|
||||
if (existingPreset.name.toLowerCase() === nameLower) {
|
||||
delete extensionSettings.presetManager.presets[existingId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Generate a unique name
|
||||
let counter = 1;
|
||||
while (existingNames.has(`${nameLower} (${counter})`)) {
|
||||
counter++;
|
||||
}
|
||||
name = `${preset.name} (${counter})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new preset with new ID
|
||||
const newId = `preset_${Date.now()}_${importCount}`;
|
||||
extensionSettings.presetManager.presets[newId] = {
|
||||
id: newId,
|
||||
name: name,
|
||||
trackerConfig: JSON.parse(JSON.stringify(preset.trackerConfig))
|
||||
};
|
||||
existingNames.add(name.toLowerCase());
|
||||
importCount++;
|
||||
}
|
||||
|
||||
if (importCount > 0) {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
return importCount;
|
||||
}
|
||||
|
||||
|
||||
+77
-25
@@ -10,7 +10,7 @@
|
||||
* Extension settings - persisted to SillyTavern settings
|
||||
*/
|
||||
export let extensionSettings = {
|
||||
settingsVersion: 3, // Version number for settings migrations (v3 = JSON format)
|
||||
settingsVersion: 4, // Version number for settings migrations (v4 = FAB widgets enabled by default)
|
||||
enabled: true,
|
||||
autoUpdate: false,
|
||||
updateDepth: 4, // How many messages to include in the context
|
||||
@@ -27,6 +27,10 @@ export let extensionSettings = {
|
||||
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
||||
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
|
||||
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
|
||||
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
|
||||
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
|
||||
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
|
||||
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
|
||||
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
||||
|
||||
@@ -34,6 +38,8 @@ export let extensionSettings = {
|
||||
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
|
||||
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
|
||||
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
|
||||
showDeceptionToggle: true, // Show Deception System toggle in main panel
|
||||
showCYOAToggle: true, // Show CYOA toggle in main panel
|
||||
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
|
||||
|
||||
showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel
|
||||
@@ -42,7 +48,13 @@ export let extensionSettings = {
|
||||
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
|
||||
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
|
||||
enableNaturalPlot: true, // Show natural plot progression button above chat input
|
||||
saveTrackerHistory: false, // Save tracker data in chat history for each message
|
||||
// History persistence settings - inject selected tracker data into historical messages
|
||||
historyPersistence: {
|
||||
enabled: false, // Master toggle for history persistence feature
|
||||
messageCount: 5, // Number of messages to include (0 = all available)
|
||||
injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message'
|
||||
contextPreamble: '' // Optional custom preamble text (empty = use default short one)
|
||||
},
|
||||
panelPosition: 'right', // 'left', 'right', or 'top'
|
||||
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
||||
customColors: {
|
||||
@@ -58,6 +70,17 @@ export let extensionSettings = {
|
||||
top: 'calc(var(--topBarBlockSize) + 60px)',
|
||||
right: '12px'
|
||||
}, // Saved position for mobile FAB button
|
||||
// Mobile FAB widget display options (8-position system around the button)
|
||||
mobileFabWidgets: {
|
||||
enabled: true, // Master toggle for FAB widgets
|
||||
weatherIcon: { enabled: true, position: 0 }, // Weather emoji (☀️, 🌧️, etc.)
|
||||
weatherDesc: { enabled: true, position: 1 }, // Weather description text
|
||||
clock: { enabled: true, position: 2 }, // Current time display
|
||||
date: { enabled: true, position: 3 }, // Date display
|
||||
location: { enabled: true, position: 4 }, // Location name
|
||||
stats: { enabled: true, position: 5 }, // All stats as compact numbers
|
||||
attributes: { enabled: true, position: 6 } // Compact RPG attributes display
|
||||
},
|
||||
userStats: JSON.stringify({
|
||||
stats: [
|
||||
{ id: 'health', name: 'Health', value: 100 },
|
||||
@@ -91,45 +114,51 @@ export let extensionSettings = {
|
||||
userStats: {
|
||||
// Array of custom stats (allows add/remove/rename)
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false },
|
||||
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false }
|
||||
],
|
||||
// RPG Attributes (customizable D&D-style attributes)
|
||||
showRPGAttributes: true,
|
||||
showLevel: true, // Show/hide level in UI and prompts
|
||||
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
|
||||
rpgAttributes: [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
|
||||
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
|
||||
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
|
||||
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
|
||||
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
|
||||
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
|
||||
],
|
||||
// Status section config
|
||||
statusSection: {
|
||||
enabled: true,
|
||||
showMoodEmoji: true,
|
||||
customFields: ['Conditions'] // User can edit what to track
|
||||
customFields: ['Conditions'], // User can edit what to track
|
||||
persistInHistory: false // Persist status in historical messages
|
||||
},
|
||||
// Optional skills field
|
||||
skillsSection: {
|
||||
enabled: false,
|
||||
label: 'Skills', // User-editable
|
||||
customFields: [] // Array of skill names
|
||||
}
|
||||
customFields: [], // Array of skill names
|
||||
persistInHistory: false // Persist skills in historical messages
|
||||
},
|
||||
// Inventory persistence
|
||||
inventoryPersistInHistory: false, // Persist inventory in historical messages
|
||||
// Quests persistence
|
||||
questsPersistInHistory: false // Persist quests in historical messages
|
||||
},
|
||||
infoBox: {
|
||||
widgets: {
|
||||
date: { enabled: true, format: 'Weekday, Month, Year' }, // Format options in UI
|
||||
weather: { enabled: true },
|
||||
temperature: { enabled: true, unit: 'C' }, // 'C' or 'F'
|
||||
time: { enabled: true },
|
||||
location: { enabled: true },
|
||||
recentEvents: { enabled: true }
|
||||
date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, // Date enabled by default for history
|
||||
weather: { enabled: true, persistInHistory: true }, // Weather enabled by default for history
|
||||
temperature: { enabled: true, unit: 'C', persistInHistory: false }, // 'C' or 'F'
|
||||
time: { enabled: true, persistInHistory: true }, // Time enabled by default for history
|
||||
location: { enabled: true, persistInHistory: true }, // Location enabled by default for history
|
||||
recentEvents: { enabled: true, persistInHistory: false }
|
||||
}
|
||||
},
|
||||
presentCharacters: {
|
||||
@@ -159,14 +188,15 @@ export let extensionSettings = {
|
||||
},
|
||||
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
|
||||
customFields: [
|
||||
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' },
|
||||
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state' }
|
||||
{ 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 configuration (separate line)
|
||||
thoughts: {
|
||||
enabled: true,
|
||||
name: 'Thoughts',
|
||||
description: 'Internal monologue (in first person POV, up to three sentences long)'
|
||||
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)',
|
||||
persistInHistory: false
|
||||
},
|
||||
// Character stats toggle (optional feature)
|
||||
characterStats: {
|
||||
@@ -250,6 +280,18 @@ export let extensionSettings = {
|
||||
recentEvents: false // Boolean for recent events widget lock
|
||||
},
|
||||
characters: {} // Object mapping character names to their locked fields (e.g., {"Sarah": {relationship: true, thoughts: false}})
|
||||
},
|
||||
// Preset management for tracker configurations
|
||||
presetManager: {
|
||||
// Map of preset ID to preset data (contains name and trackerConfig)
|
||||
presets: {},
|
||||
// Map of character/group entity to preset ID (e.g., "char_0": "preset_123", "group_abc": "preset_456")
|
||||
// Note: This is stored separately and NOT exported with presets
|
||||
characterAssociations: {},
|
||||
// Currently active preset ID
|
||||
activePresetId: null,
|
||||
// Default preset ID (used when no character association exists)
|
||||
defaultPresetId: null
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,6 +350,12 @@ export let isGenerating = false;
|
||||
*/
|
||||
export let isPlotProgression = false;
|
||||
|
||||
/**
|
||||
* Flag indicating if we're actively expecting a new message from generation
|
||||
* (as opposed to loading chat history)
|
||||
*/
|
||||
export let isAwaitingNewMessage = false;
|
||||
|
||||
/**
|
||||
* Temporary storage for pending dice roll (not saved until user clicks "Save Roll")
|
||||
*/
|
||||
@@ -408,6 +456,10 @@ export function setIsPlotProgression(value) {
|
||||
isPlotProgression = value;
|
||||
}
|
||||
|
||||
export function setIsAwaitingNewMessage(value) {
|
||||
isAwaitingNewMessage = value;
|
||||
}
|
||||
|
||||
export function setPendingDiceRoll(roll) {
|
||||
pendingDiceRoll = roll;
|
||||
}
|
||||
|
||||
+2
-4
@@ -26,15 +26,13 @@
|
||||
"template.settingsModal.display.panelPositionOptions.right": "Right Sidebar",
|
||||
"template.settingsModal.display.panelPositionOptions.left": "Left Sidebar",
|
||||
"template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages",
|
||||
"template.settingsModal.display.showUserStats": "Show User Stats",
|
||||
"template.settingsModal.display.toggleAutoUpdateNote": "Automatically refresh RPG info after each message.",
|
||||
"template.settingsModal.display.showUserStats": "Show User Stats",
|
||||
"template.settingsModal.display.showUserStatsNote": "Enable User Stats that track your persona's statistics, mood, attributes, skills, etc.",
|
||||
"template.settingsModal.display.showInfoBox": "Show Info Box",
|
||||
"template.settingsModal.display.showInfoBoxNote": "Display location, time, weather, and recent events.",
|
||||
"template.settingsModal.display.showPresentCharacters": "Show Present Characters",
|
||||
"template.settingsModal.display.showPresentCharactersNote": "Display character portraits with their current thoughts and status.",
|
||||
"template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages",
|
||||
"template.settingsModal.display.toggleAutoUpdateNote": "Automatically refresh RPG info after each message.",
|
||||
"template.settingsModal.display.narratorMode": "Narrator Mode",
|
||||
"template.settingsModal.display.narratorModeNote": "Use character card as narrator. Infer characters from context instead of using fixed character references.",
|
||||
"template.settingsModal.display.showInventory": "Show Inventory",
|
||||
@@ -76,7 +74,7 @@
|
||||
"template.settingsModal.advanced.encounterHistoryDepthNote": "Number of recent messages to include in combat initialization.",
|
||||
"template.settingsModal.advanced.autoSaveCombatLogs": "Auto-save Combat Logs",
|
||||
"template.settingsModal.advanced.autoSaveCombatLogsNote": "Save detailed combat logs to file for future reference and analysis.",
|
||||
"template.settingsModal.advanced.clearCacheNote": "Clears all cached data including tracker history and temporary files.",
|
||||
"template.settingsModal.advanced.clearCacheNote": "Clears committed and displayed tracker data for your currently active chat.",
|
||||
"template.settingsModal.advanced.generationMode": "Generation Mode:",
|
||||
"template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation",
|
||||
"template.settingsModal.advanced.generationModeOptions.separate": "Separate Generation",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
$userStatsContainer
|
||||
} from '../../core/state.js';
|
||||
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||
import { updateFabWidgets } from '../ui/mobile.js';
|
||||
|
||||
/**
|
||||
* Sets up event listeners for classic stat +/- buttons using delegation.
|
||||
@@ -25,6 +26,7 @@ export function setupClassicStatsButtons() {
|
||||
saveChatData();
|
||||
// Update only the specific stat value, not the entire stats panel
|
||||
$(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]);
|
||||
updateFabWidgets();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -37,6 +39,7 @@ export function setupClassicStatsButtons() {
|
||||
saveChatData();
|
||||
// Update only the specific stat value, not the entire stats panel
|
||||
$(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]);
|
||||
updateFabWidgets();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { togglePlotButtons } from '../ui/layout.js';
|
||||
import { extensionSettings, setIsPlotProgression } from '../../core/state.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT } from '../generation/promptBuilder.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_CYOA_PROMPT } from '../generation/promptBuilder.js';
|
||||
import { Generate } from '../../../../../../../script.js';
|
||||
|
||||
/**
|
||||
@@ -121,6 +121,20 @@ export async function sendPlotProgression(type) {
|
||||
prompt += '\n\n' + dialogueColoringPromptText;
|
||||
}
|
||||
|
||||
// Add Deception System prompt if enabled
|
||||
if (extensionSettings.enableDeceptionSystem) {
|
||||
// Use custom Deception prompt if set, otherwise use default
|
||||
const deceptionPromptText = extensionSettings.customDeceptionPrompt || DEFAULT_DECEPTION_PROMPT;
|
||||
prompt += '\n\n' + deceptionPromptText;
|
||||
}
|
||||
|
||||
// Add CYOA prompt if enabled
|
||||
if (extensionSettings.enableCYOA) {
|
||||
// Use custom CYOA prompt if set, otherwise use default
|
||||
const cyoaPromptText = extensionSettings.customCYOAPrompt || DEFAULT_CYOA_PROMPT;
|
||||
prompt += '\n\n' + cyoaPromptText;
|
||||
}
|
||||
|
||||
// Set flag to indicate we're doing plot progression
|
||||
// This will be used by onMessageReceived to clear the prompt after generation completes
|
||||
setIsPlotProgression(true);
|
||||
|
||||
@@ -34,6 +34,7 @@ import { renderQuests } from '../rendering/quests.js';
|
||||
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
|
||||
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||||
|
||||
// Store the original preset name to restore after tracker generation
|
||||
let originalPresetName = null;
|
||||
@@ -235,6 +236,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
setFabLoadingState(true); // Show spinning FAB on mobile
|
||||
|
||||
// Update button to show "Updating..." state
|
||||
const $updateBtn = $('#rpg-manual-update');
|
||||
@@ -302,21 +304,6 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
||||
}
|
||||
|
||||
// When saveTrackerHistory is enabled, store tracker data on the user's message too
|
||||
// This allows scrolling through history and seeing trackers at each point
|
||||
if (extensionSettings.saveTrackerHistory && lastMessage && lastMessage.is_user) {
|
||||
if (!lastMessage.extra) {
|
||||
lastMessage.extra = {};
|
||||
}
|
||||
lastMessage.extra.rpg_companion_data = {
|
||||
userStats: parsedData.userStats,
|
||||
infoBox: parsedData.infoBox,
|
||||
characterThoughts: parsedData.characterThoughts,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
// console.log('[RPG Companion] 💾 Stored tracker data on user message for history');
|
||||
}
|
||||
|
||||
// Also store on assistant message if present (existing behavior)
|
||||
if (lastMessage && !lastMessage.is_user) {
|
||||
if (!lastMessage.extra) {
|
||||
@@ -391,6 +378,8 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
}
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setFabLoadingState(false); // Stop spinning FAB on mobile
|
||||
updateFabWidgets(); // Update FAB widgets with new data
|
||||
|
||||
// Restore button to original state
|
||||
const $updateBtn = $('#rpg-manual-update');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles } from '../../../../../../../script.js';
|
||||
import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles, eventSource, event_types } from '../../../../../../../script.js';
|
||||
import {
|
||||
extensionSettings,
|
||||
committedTrackerData,
|
||||
@@ -20,19 +20,227 @@ import {
|
||||
generateTrackerExample,
|
||||
generateTrackerInstructions,
|
||||
generateContextualSummary,
|
||||
formatHistoricalTrackerData,
|
||||
DEFAULT_HTML_PROMPT,
|
||||
DEFAULT_DIALOGUE_COLORING_PROMPT,
|
||||
DEFAULT_DECEPTION_PROMPT,
|
||||
DEFAULT_CYOA_PROMPT,
|
||||
DEFAULT_SPOTIFY_PROMPT,
|
||||
SPOTIFY_FORMAT_INSTRUCTION
|
||||
} from './promptBuilder.js';
|
||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||
|
||||
// Track suppression state for event handler
|
||||
let currentSuppressionState = false;
|
||||
|
||||
// Type imports
|
||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||
|
||||
// Track last chat length we committed at to prevent duplicate commits from streaming
|
||||
let lastCommittedChatLength = -1;
|
||||
|
||||
// Store original message content for restoration after generation
|
||||
// Map of message index -> original mes content
|
||||
let originalMessageContent = new Map();
|
||||
|
||||
/**
|
||||
* Builds a map of historical context data from ST chat messages with rpg_companion_swipes data.
|
||||
* Returns a map keyed by message index with formatted context strings.
|
||||
* The index stored depends on the injection position setting.
|
||||
*
|
||||
* @returns {Map<number, string>} Map of target message index to formatted context string
|
||||
*/
|
||||
function buildHistoricalContextMap() {
|
||||
const historyPersistence = extensionSettings.historyPersistence;
|
||||
if (!historyPersistence || !historyPersistence.enabled) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
if (!chat || chat.length < 2) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
const userName = context.name1;
|
||||
const position = historyPersistence.injectionPosition || 'assistant_message_end';
|
||||
const contextMap = new Map();
|
||||
|
||||
// Determine how many messages to include (0 = all available)
|
||||
const messageCount = historyPersistence.messageCount || 0;
|
||||
const maxMessages = messageCount === 0 ? chat.length : Math.min(messageCount, chat.length);
|
||||
|
||||
// Find the last assistant message - this is the one that gets current context via setExtensionPrompt
|
||||
// We should NOT add historical context to it
|
||||
let lastAssistantIndex = -1;
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
if (!chat[i].is_user && !chat[i].is_system) {
|
||||
lastAssistantIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through messages to find those with tracker data
|
||||
// Start from before the last assistant message
|
||||
let processedCount = 0;
|
||||
const startIndex = lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2;
|
||||
|
||||
for (let i = startIndex; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) {
|
||||
const message = chat[i];
|
||||
|
||||
// Skip system messages
|
||||
if (message.is_system) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only assistant messages have rpg_companion_swipes data
|
||||
if (message.is_user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the rpg_companion_swipes data for current swipe
|
||||
// Data can be in two places:
|
||||
// 1. message.extra.rpg_companion_swipes (current session, before save)
|
||||
// 2. message.swipe_info[swipeId].extra.rpg_companion_swipes (loaded from file)
|
||||
const currentSwipeId = message.swipe_id || 0;
|
||||
let swipeData = message.extra?.rpg_companion_swipes;
|
||||
|
||||
// If not in message.extra, check swipe_info
|
||||
if (!swipeData && message.swipe_info && message.swipe_info[currentSwipeId]) {
|
||||
swipeData = message.swipe_info[currentSwipeId].extra?.rpg_companion_swipes;
|
||||
}
|
||||
|
||||
if (!swipeData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const trackerData = swipeData[currentSwipeId];
|
||||
if (!trackerData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Format the historical tracker data using the shared function
|
||||
const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName);
|
||||
if (!formattedContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build the context wrapper
|
||||
const preamble = historyPersistence.contextPreamble || 'Context for that moment:';
|
||||
const wrappedContext = `\n${preamble}\n${formattedContext}`;
|
||||
|
||||
// Determine which message index to store based on injection position
|
||||
let targetIndex = i; // Default: the assistant message itself
|
||||
|
||||
if (position === 'user_message_end') {
|
||||
// Find the next user message after this assistant message
|
||||
for (let j = i + 1; j < chat.length; j++) {
|
||||
if (chat[j].is_user && !chat[j].is_system) {
|
||||
targetIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If no user message found after, skip this one
|
||||
if (targetIndex === i) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// For assistant_message_end, extra_user_message, extra_assistant_message:
|
||||
// We inject into the assistant message itself (for now - extra messages handled differently)
|
||||
|
||||
// Store the context keyed by target index
|
||||
// If multiple assistant messages map to the same user message, append
|
||||
if (contextMap.has(targetIndex)) {
|
||||
contextMap.set(targetIndex, contextMap.get(targetIndex) + wrappedContext);
|
||||
} else {
|
||||
contextMap.set(targetIndex, wrappedContext);
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
return contextMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects historical context into chat messages by modifying them in-place.
|
||||
* Stores original content for restoration after generation.
|
||||
* This approach works for ALL API types (text completion and chat completion).
|
||||
*/
|
||||
function injectHistoricalContextIntoChat() {
|
||||
const historyPersistence = extensionSettings.historyPersistence;
|
||||
if (!historyPersistence || !historyPersistence.enabled) {
|
||||
// console.log('[RPG Companion] History persistence not enabled, skipping injection');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSuppressionState || !extensionSettings.enabled) {
|
||||
// console.log('[RPG Companion] Skipping history injection: suppressed or disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
if (!chat || chat.length < 2) {
|
||||
// console.log('[RPG Companion] Chat too short, skipping history injection');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the context map
|
||||
const contextMap = buildHistoricalContextMap();
|
||||
if (contextMap.size === 0) {
|
||||
// console.log('[RPG Companion] No historical context to inject');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`[RPG Companion] Injecting historical context into ${contextMap.size} messages`);
|
||||
|
||||
// Clear any previous stored content
|
||||
originalMessageContent.clear();
|
||||
|
||||
let injectedCount = 0;
|
||||
for (const [msgIdx, ctxContent] of contextMap) {
|
||||
const message = chat[msgIdx];
|
||||
if (!message || typeof message.mes !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store original content for restoration
|
||||
originalMessageContent.set(msgIdx, message.mes);
|
||||
|
||||
// Modify the message in-place
|
||||
message.mes = message.mes + ctxContent;
|
||||
injectedCount++;
|
||||
// console.log(`[RPG Companion] Injected context into message ${msgIdx}`);
|
||||
}
|
||||
|
||||
// console.log(`[RPG Companion] Successfully injected historical context into ${injectedCount} messages`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores original message content after generation completes.
|
||||
* This ensures the injected context doesn't persist in the actual chat data.
|
||||
*/
|
||||
function restoreOriginalMessageContent() {
|
||||
if (originalMessageContent.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
|
||||
// console.log(`[RPG Companion] Restoring ${originalMessageContent.size} messages to original content`);
|
||||
|
||||
for (const [msgIdx, originalContent] of originalMessageContent) {
|
||||
if (chat[msgIdx]) {
|
||||
chat[msgIdx].mes = originalContent;
|
||||
}
|
||||
}
|
||||
|
||||
originalMessageContent.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for generation start.
|
||||
* Manages tracker data commitment and prompt injection based on generation mode.
|
||||
@@ -55,8 +263,8 @@ export async function onGenerationStarted(type, data, dryRun) {
|
||||
// console.log('[RPG Companion] Committed Prompt:', committedTrackerData);
|
||||
|
||||
// Skip tracker injection for image generation requests
|
||||
if (data?.quietImage) {
|
||||
// console.log('[RPG Companion] Detected image generation (quietImage=true), skipping tracker injection');
|
||||
if (data?.quietImage || data?.quiet_image || data?.isImageGeneration) {
|
||||
// console.log('[RPG Companion] Detected image generation, skipping tracker injection');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -261,7 +469,7 @@ export async function onGenerationStarted(type, data, dryRun) {
|
||||
if (extensionSettings.enableHtmlPrompt && !shouldSuppress) {
|
||||
// Use custom HTML prompt if set, otherwise use default
|
||||
const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
|
||||
const htmlPrompt = `\n${htmlPromptText}`;
|
||||
const htmlPrompt = `\n- ${htmlPromptText}\n`;
|
||||
|
||||
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');
|
||||
@@ -274,7 +482,7 @@ export async function onGenerationStarted(type, data, dryRun) {
|
||||
if (extensionSettings.enableDialogueColoring && !shouldSuppress) {
|
||||
// Use custom Dialogue Coloring prompt if set, otherwise use default
|
||||
const dialogueColoringPromptText = extensionSettings.customDialogueColoringPrompt || DEFAULT_DIALOGUE_COLORING_PROMPT;
|
||||
const dialogueColoringPrompt = `\n${dialogueColoringPromptText}`;
|
||||
const dialogueColoringPrompt = `\n- ${dialogueColoringPromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-dialogue-coloring', dialogueColoringPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Dialogue Coloring prompt at depth 0 for together mode');
|
||||
@@ -283,11 +491,24 @@ export async function onGenerationStarted(type, data, dryRun) {
|
||||
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Deception System prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableDeceptionSystem && !shouldSuppress) {
|
||||
// Use custom Deception prompt if set, otherwise use default
|
||||
const deceptionPromptText = extensionSettings.customDeceptionPrompt || DEFAULT_DECEPTION_PROMPT;
|
||||
const deceptionPrompt = `\n- ${deceptionPromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-deception', deceptionPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Deception System prompt at depth 0 for together mode');
|
||||
} else {
|
||||
// Clear Deception System prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Spotify prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||
// Use custom Spotify prompt if set, otherwise use default
|
||||
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
||||
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
|
||||
const spotifyPrompt = `\n- ${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for together mode');
|
||||
@@ -295,6 +516,20 @@ export async function onGenerationStarted(type, data, dryRun) {
|
||||
// Clear Spotify prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject CYOA prompt separately at depth 0 if enabled (injected last to appear last in prompt)
|
||||
if (extensionSettings.enableCYOA && !shouldSuppress) {
|
||||
// Use custom CYOA prompt if set, otherwise use default
|
||||
const cyoaPromptText = extensionSettings.customCYOAPrompt || DEFAULT_CYOA_PROMPT;
|
||||
const cyoaPrompt = `\n- ${cyoaPromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-zzz-cyoa', cyoaPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected CYOA prompt at depth 0 for together mode');
|
||||
} else {
|
||||
// Clear CYOA prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
} else if (extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') {
|
||||
// In SEPARATE and EXTERNAL modes, inject the contextual summary for main roleplay generation
|
||||
const contextSummary = generateContextualSummary();
|
||||
@@ -322,7 +557,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
if (extensionSettings.enableHtmlPrompt && !shouldSuppress) {
|
||||
// Use custom HTML prompt if set, otherwise use default
|
||||
const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
|
||||
const htmlPrompt = `\n${htmlPromptText}`;
|
||||
const htmlPrompt = `\n- ${htmlPromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for separate/external mode');
|
||||
@@ -331,11 +566,37 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Dialogue Coloring prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableDialogueColoring && !shouldSuppress) {
|
||||
// Use custom Dialogue Coloring prompt if set, otherwise use default
|
||||
const dialogueColoringPromptText = extensionSettings.customDialogueColoringPrompt || DEFAULT_DIALOGUE_COLORING_PROMPT;
|
||||
const dialogueColoringPrompt = `\n- ${dialogueColoringPromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-dialogue-coloring', dialogueColoringPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Dialogue Coloring prompt at depth 0 for separate/external mode');
|
||||
} else {
|
||||
// Clear Dialogue Coloring prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Deception System prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableDeceptionSystem && !shouldSuppress) {
|
||||
// Use custom Deception prompt if set, otherwise use default
|
||||
const deceptionPromptText = extensionSettings.customDeceptionPrompt || DEFAULT_DECEPTION_PROMPT;
|
||||
const deceptionPrompt = `\n- ${deceptionPromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-deception', deceptionPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Deception System prompt at depth 0 for separate/external mode');
|
||||
} else {
|
||||
// Clear Deception System prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Spotify prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||
// Use custom Spotify prompt if set, otherwise use default
|
||||
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
||||
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
|
||||
const spotifyPrompt = `\n- ${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for separate/external mode');
|
||||
@@ -344,6 +605,19 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject CYOA prompt separately at depth 0 if enabled (injected last to appear last in prompt)
|
||||
if (extensionSettings.enableCYOA && !shouldSuppress) {
|
||||
// Use custom CYOA prompt if set, otherwise use default
|
||||
const cyoaPromptText = extensionSettings.customCYOAPrompt || DEFAULT_CYOA_PROMPT;
|
||||
const cyoaPrompt = `\n- ${cyoaPromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-zzz-cyoa', cyoaPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected CYOA prompt at depth 0 for separate/external mode');
|
||||
} else {
|
||||
// Clear CYOA prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Clear together mode injections
|
||||
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
@@ -353,6 +627,31 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Set suppression state for the historical context injection
|
||||
currentSuppressionState = shouldSuppress;
|
||||
|
||||
// Inject historical context directly into chat messages
|
||||
// This temporarily modifies messages and will be restored after generation
|
||||
injectHistoricalContextIntoChat();
|
||||
|
||||
// Register a one-time listener to restore messages after prompt is built
|
||||
// Using .once() so it auto-removes after firing
|
||||
eventSource.once(event_types.GENERATE_AFTER_COMBINE_PROMPTS, () => {
|
||||
restoreOriginalMessageContent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when generation ends to restore original message content.
|
||||
* This should be called from the GENERATION_ENDED event handler.
|
||||
*/
|
||||
export function onGenerationEndedCleanup() {
|
||||
restoreOriginalMessageContent();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,16 @@ export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, an
|
||||
*/
|
||||
export const DEFAULT_DIALOGUE_COLORING_PROMPT = `Wrap all character/NPC "dialogues" in unique <font color=######>tags</font>, exemplary: <font color=#abc123>"You're pretty good."</font> Assign a distinct color to each speaker and reuse it whenever they speak again.`;
|
||||
|
||||
/**
|
||||
* Default Deception System prompt text
|
||||
*/
|
||||
export const DEFAULT_DECEPTION_PROMPT = `When a character is lying or deceiving, you should follow up that line with the <lie> tag, containing a brief description of the truth and the lie's reason, using the template below (replace placeholders in brackets). This will be hidden from the user's view, but not to you, making it useful for future consequences: <lie>[Character] is [lying/deceiving/omitting], the truth is [truth]. Reason: [reason].</lie>`;
|
||||
|
||||
/**
|
||||
* Default CYOA prompt text
|
||||
*/
|
||||
export const DEFAULT_CYOA_PROMPT = `Since this is a "Choose Your Own Adventure" type of game, you must finish your response by creating a numbered list of 5 different possible action or dialogue options (depending on the scene) for the user to choose from. Make sure they all fit their persona well. They will respond with their choice on how to progress.`;
|
||||
|
||||
/**
|
||||
* Default Spotify music prompt text (customizable by users)
|
||||
*/
|
||||
@@ -229,7 +239,6 @@ function buildAttributesString() {
|
||||
*/
|
||||
export function generateTrackerExample() {
|
||||
let example = '';
|
||||
const useXmlTags = extensionSettings.saveTrackerHistory;
|
||||
|
||||
// Use COMMITTED data for generation context, not displayed data
|
||||
// Apply locks before sending to AI (for JSON format only)
|
||||
@@ -310,26 +319,18 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
|
||||
// Only add tracker instructions if at least one tracker is enabled
|
||||
if (hasAnyTrackers) {
|
||||
// Determine format based on saveTrackerHistory setting
|
||||
const useXmlTags = extensionSettings.saveTrackerHistory;
|
||||
const openTag = useXmlTags ? '<trackers>\n' : '';
|
||||
const closeTag = useXmlTags ? '\n</trackers>' : '';
|
||||
const codeBlockMarker = '';
|
||||
const endCodeBlockMarker = '';
|
||||
|
||||
// Universal instruction header
|
||||
if (useXmlTags) {
|
||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the JSON format shown below, enclosed in <trackers></trackers> XML tags. `;
|
||||
} else {
|
||||
instructions += '\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the JSON format shown below as a single unified JSON object containing all enabled tracker fields. ';
|
||||
}
|
||||
instructions += '\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the JSON format shown below as a single unified JSON object containing all enabled tracker fields. ';
|
||||
|
||||
// Append custom instruction portion if available
|
||||
const customPrompt = extensionSettings.customTrackerInstructionsPrompt;
|
||||
if (customPrompt) {
|
||||
instructions += customPrompt.replace(/{userName}/g, userName);
|
||||
} else {
|
||||
instructions += `Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that ${userName} perceives about the current scene and the present characters. For example: "Location" becomes "Forest Clearing", "Mood Emoji" becomes "😊". `;
|
||||
instructions += `Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that ${userName} perceives about the current scene and the present characters. For example: "Location" becomes "Forest Clearing", "Mood Emoji" becomes "😊". DO NOT include ${userName} in the characters section, only NPCs. `;
|
||||
instructions += `Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences.`;
|
||||
}
|
||||
|
||||
@@ -728,6 +729,216 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats historical tracker data from a message's rpg_companion_swipes data.
|
||||
* Only includes tracker fields that have persistInHistory enabled in trackerConfig.
|
||||
* Uses the same formatting as formatTrackerDataForContext but filtered by persistence settings.
|
||||
*
|
||||
* @param {Object} trackerData - The tracker data from message.extra.rpg_companion_swipes[swipeId]
|
||||
* @param {Object} trackerConfig - The tracker configuration from extensionSettings.trackerConfig
|
||||
* @param {string} userName - The user's name for personalization
|
||||
* @returns {string} Formatted historical context or empty string if nothing to include
|
||||
*/
|
||||
export function formatHistoricalTrackerData(trackerData, trackerConfig, userName) {
|
||||
if (!trackerData || !trackerConfig) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let formatted = '';
|
||||
|
||||
// Helper to safely get values
|
||||
const getValue = (field) => {
|
||||
if (field === null || field === undefined) return '';
|
||||
if (field && typeof field === 'object' && !Array.isArray(field) && 'value' in field) {
|
||||
return getValue(field.value);
|
||||
}
|
||||
if (typeof field !== 'object') {
|
||||
return String(field);
|
||||
}
|
||||
if (Array.isArray(field)) {
|
||||
return field.map(item => getValue(item)).filter(Boolean).join(', ');
|
||||
}
|
||||
if (field && typeof field === 'object') {
|
||||
if ('start' in field && 'end' in field) {
|
||||
return `${getValue(field.start)} - ${getValue(field.end)}`;
|
||||
}
|
||||
if ('emoji' in field && 'forecast' in field) {
|
||||
return `${getValue(field.emoji)} ${getValue(field.forecast)}`;
|
||||
}
|
||||
if ('name' in field) {
|
||||
const name = getValue(field.name);
|
||||
if ('quantity' in field && field.quantity > 1) {
|
||||
return `${name} (x${field.quantity})`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
if ('title' in field) {
|
||||
return getValue(field.title);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
try {
|
||||
// Process userStats if present and has persistence-enabled fields
|
||||
if (trackerData.userStats) {
|
||||
const userStatsConfig = trackerConfig.userStats;
|
||||
const userStatsData = typeof trackerData.userStats === 'string'
|
||||
? JSON.parse(trackerData.userStats)
|
||||
: trackerData.userStats;
|
||||
|
||||
let statsFormatted = '';
|
||||
|
||||
// Custom stats with persistInHistory enabled
|
||||
if (userStatsData.stats && Array.isArray(userStatsData.stats) && userStatsConfig.customStats) {
|
||||
for (const stat of userStatsData.stats) {
|
||||
const configStat = userStatsConfig.customStats.find(s => s.id === stat.id);
|
||||
if (configStat?.persistInHistory && stat.value !== undefined) {
|
||||
const statName = stat.name || configStat.name || stat.id;
|
||||
statsFormatted += `${statName}: ${stat.value}, `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status section
|
||||
if (userStatsConfig.statusSection?.persistInHistory && userStatsData.status) {
|
||||
const mood = getValue(userStatsData.status.mood || userStatsData.status);
|
||||
const conditions = getValue(userStatsData.status.conditions);
|
||||
if (mood) statsFormatted += `Mood: ${mood}, `;
|
||||
if (conditions && conditions !== 'None') statsFormatted += `Conditions: ${conditions}, `;
|
||||
}
|
||||
|
||||
// Skills section
|
||||
if (userStatsConfig.skillsSection?.persistInHistory && userStatsData.skills) {
|
||||
const skillsList = Array.isArray(userStatsData.skills)
|
||||
? userStatsData.skills.map(s => getValue(s)).filter(s => s).join(', ')
|
||||
: getValue(userStatsData.skills);
|
||||
if (skillsList) statsFormatted += `Skills: ${skillsList}, `;
|
||||
}
|
||||
|
||||
// Inventory
|
||||
if (userStatsConfig.inventoryPersistInHistory && userStatsData.inventory) {
|
||||
const inv = userStatsData.inventory;
|
||||
if (inv.onPerson && Array.isArray(inv.onPerson) && inv.onPerson.length > 0) {
|
||||
const items = inv.onPerson.map(i => getValue(i)).filter(i => i);
|
||||
if (items.length > 0) statsFormatted += `On Person: ${items.join(', ')}, `;
|
||||
}
|
||||
if (inv.clothing && Array.isArray(inv.clothing) && inv.clothing.length > 0) {
|
||||
const items = inv.clothing.map(i => getValue(i)).filter(i => i);
|
||||
if (items.length > 0) statsFormatted += `Clothing: ${items.join(', ')}, `;
|
||||
}
|
||||
}
|
||||
|
||||
// Quests
|
||||
if (userStatsConfig.questsPersistInHistory && userStatsData.quests) {
|
||||
const quests = userStatsData.quests;
|
||||
if (quests.main) {
|
||||
const mainQuest = getValue(quests.main);
|
||||
if (mainQuest && mainQuest !== 'None') statsFormatted += `Quest: ${mainQuest}, `;
|
||||
}
|
||||
}
|
||||
|
||||
if (statsFormatted) {
|
||||
formatted += `${userName}: ${statsFormatted.slice(0, -2)}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Process infoBox if present and has persistence-enabled widgets
|
||||
if (trackerData.infoBox) {
|
||||
const infoBoxConfig = trackerConfig.infoBox;
|
||||
const infoBoxData = typeof trackerData.infoBox === 'string'
|
||||
? JSON.parse(trackerData.infoBox)
|
||||
: trackerData.infoBox;
|
||||
|
||||
let infoFormatted = '';
|
||||
|
||||
// Date
|
||||
if (infoBoxConfig.widgets.date?.persistInHistory && infoBoxData.date) {
|
||||
const date = getValue(infoBoxData.date);
|
||||
if (date) infoFormatted += `Date: ${date}, `;
|
||||
}
|
||||
|
||||
// Time
|
||||
if (infoBoxConfig.widgets.time?.persistInHistory && infoBoxData.time) {
|
||||
const time = getValue(infoBoxData.time);
|
||||
if (time) infoFormatted += `Time: ${time}, `;
|
||||
}
|
||||
|
||||
// Weather
|
||||
if (infoBoxConfig.widgets.weather?.persistInHistory && infoBoxData.weather) {
|
||||
const weather = getValue(infoBoxData.weather);
|
||||
if (weather) infoFormatted += `Weather: ${weather}, `;
|
||||
}
|
||||
|
||||
// Temperature
|
||||
if (infoBoxConfig.widgets.temperature?.persistInHistory && infoBoxData.temperature) {
|
||||
const temp = getValue(infoBoxData.temperature);
|
||||
if (temp) infoFormatted += `Temp: ${temp}, `;
|
||||
}
|
||||
|
||||
// Location
|
||||
if (infoBoxConfig.widgets.location?.persistInHistory && infoBoxData.location) {
|
||||
const location = getValue(infoBoxData.location);
|
||||
if (location) infoFormatted += `Location: ${location}, `;
|
||||
}
|
||||
|
||||
// Recent Events
|
||||
if (infoBoxConfig.widgets.recentEvents?.persistInHistory && infoBoxData.recentEvents) {
|
||||
const events = getValue(infoBoxData.recentEvents);
|
||||
if (events) infoFormatted += `Events: ${events}, `;
|
||||
}
|
||||
|
||||
if (infoFormatted) {
|
||||
formatted += infoFormatted.slice(0, -2) + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Process characterThoughts if present and has persistence-enabled fields
|
||||
if (trackerData.characterThoughts) {
|
||||
const charsConfig = trackerConfig.presentCharacters;
|
||||
const charsData = typeof trackerData.characterThoughts === 'string'
|
||||
? JSON.parse(trackerData.characterThoughts)
|
||||
: trackerData.characterThoughts;
|
||||
|
||||
// Characters can be an array or wrapped in an object
|
||||
const characters = Array.isArray(charsData) ? charsData : (charsData.characters || []);
|
||||
|
||||
for (const char of characters) {
|
||||
if (!char || !char.name) continue;
|
||||
|
||||
let charFormatted = '';
|
||||
|
||||
// Custom fields (appearance, demeanor, etc.)
|
||||
if (char.details && typeof char.details === 'object') {
|
||||
for (const field of charsConfig.customFields) {
|
||||
if (field.persistInHistory && char.details[field.id]) {
|
||||
const value = getValue(char.details[field.id]);
|
||||
if (value) charFormatted += `${field.name}: ${value}, `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thoughts
|
||||
if (charsConfig.thoughts?.persistInHistory && char.thoughts) {
|
||||
const thoughts = typeof char.thoughts === 'object' && char.thoughts.content
|
||||
? getValue(char.thoughts.content)
|
||||
: getValue(char.thoughts);
|
||||
if (thoughts) charFormatted += `Thinking: ${thoughts}, `;
|
||||
}
|
||||
|
||||
if (charFormatted) {
|
||||
formatted += `${getValue(char.name)}: ${charFormatted.slice(0, -2)}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formatted.trim();
|
||||
} catch (e) {
|
||||
console.warn('[RPG Companion] Failed to format historical tracker data:', e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formatted contextual summary for SEPARATE mode injection.
|
||||
* Includes the full tracker data in original format (without code fences and separators).
|
||||
@@ -883,6 +1094,8 @@ export function generateRPGPromptText() {
|
||||
export async function generateSeparateUpdatePrompt() {
|
||||
const depth = extensionSettings.updateDepth;
|
||||
const userName = getContext().name1;
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
const historyPersistence = extensionSettings.historyPersistence;
|
||||
|
||||
const messages = [];
|
||||
|
||||
@@ -899,6 +1112,7 @@ export async function generateSeparateUpdatePrompt() {
|
||||
systemMessage += `Here is the description of the protagonist for reference:\n`;
|
||||
systemMessage += `<protagonist>\n{{persona}}\n</protagonist>\n`;
|
||||
systemMessage += `\n`;
|
||||
|
||||
systemMessage += `Here are the last few messages in the conversation history (between the user and the roleplayer assistant) you should reference when responding:\n<history>`;
|
||||
|
||||
messages.push({
|
||||
@@ -907,13 +1121,98 @@ export async function generateSeparateUpdatePrompt() {
|
||||
});
|
||||
|
||||
// /hide command automatically handles checkpoint filtering
|
||||
// Add chat history as separate user/assistant messages
|
||||
// Add chat history as separate user/assistant messages with per-message historical context
|
||||
const recentMessages = chat.slice(-depth);
|
||||
const startIndex = chat.length - depth;
|
||||
const position = historyPersistence?.injectionPosition || 'assistant_message_end';
|
||||
|
||||
// Build a map of which messages should get context based on position setting
|
||||
// Key: message index in recentMessages, Value: context string
|
||||
const contextInjectionMap = new Map();
|
||||
|
||||
if (historyPersistence?.enabled) {
|
||||
// Find the last assistant message index (in recentMessages)
|
||||
let lastAssistantIdx = -1;
|
||||
for (let i = recentMessages.length - 1; i >= 0; i--) {
|
||||
if (!recentMessages[i].is_user && !recentMessages[i].is_system) {
|
||||
lastAssistantIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through assistant messages to find tracker data
|
||||
for (let i = 0; i < recentMessages.length; i++) {
|
||||
const message = recentMessages[i];
|
||||
|
||||
// Skip user and system messages - only assistant messages have tracker data
|
||||
if (message.is_user || message.is_system) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the last assistant message - it gets current context elsewhere
|
||||
if (i === lastAssistantIdx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const swipeData = message.extra?.rpg_companion_swipes;
|
||||
if (!swipeData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentSwipeId = message.swipe_id || 0;
|
||||
const trackerData = swipeData[currentSwipeId];
|
||||
if (!trackerData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName);
|
||||
if (!formattedContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const preamble = historyPersistence.contextPreamble || 'Context for that moment:';
|
||||
const wrappedContext = `\n${preamble}\n${formattedContext}`;
|
||||
|
||||
// Determine target message based on position
|
||||
let targetIdx = i;
|
||||
|
||||
if (position === 'user_message_end') {
|
||||
// Find next user message after this assistant message
|
||||
for (let j = i + 1; j < recentMessages.length; j++) {
|
||||
if (recentMessages[j].is_user && !recentMessages[j].is_system) {
|
||||
targetIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If no user message found, skip
|
||||
if (targetIdx === i) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// For assistant_message_end: inject into the assistant message itself
|
||||
|
||||
// Append to existing or create new entry
|
||||
if (contextInjectionMap.has(targetIdx)) {
|
||||
contextInjectionMap.set(targetIdx, contextInjectionMap.get(targetIdx) + wrappedContext);
|
||||
} else {
|
||||
contextInjectionMap.set(targetIdx, wrappedContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now build the messages array with injected context
|
||||
for (let i = 0; i < recentMessages.length; i++) {
|
||||
const message = recentMessages[i];
|
||||
let content = message.mes;
|
||||
|
||||
// Add historical context if this message is a target
|
||||
if (contextInjectionMap.has(i)) {
|
||||
content += contextInjectionMap.get(i);
|
||||
}
|
||||
|
||||
for (const message of recentMessages) {
|
||||
messages.push({
|
||||
role: message.is_user ? 'user' : 'assistant',
|
||||
content: message.mes
|
||||
content: content
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,16 @@ import {
|
||||
committedTrackerData,
|
||||
lastActionWasSwipe,
|
||||
isPlotProgression,
|
||||
isAwaitingNewMessage,
|
||||
setLastActionWasSwipe,
|
||||
setIsPlotProgression,
|
||||
setIsGenerating,
|
||||
setIsAwaitingNewMessage,
|
||||
updateLastGeneratedData,
|
||||
updateCommittedTrackerData,
|
||||
$musicPlayerContainer
|
||||
} from '../../core/state.js';
|
||||
import { saveChatData, loadChatData } from '../../core/persistence.js';
|
||||
import { saveChatData, loadChatData, autoSwitchPresetForEntity } from '../../core/persistence.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
|
||||
// Generation & Parsing
|
||||
@@ -28,7 +30,7 @@ import { parseResponse, parseUserStats } from '../generation/parser.js';
|
||||
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
||||
import { updateRPGData } from '../generation/apiClient.js';
|
||||
import { removeLocks } from '../generation/lockManager.js';
|
||||
import { onGenerationStarted } from '../generation/injector.js';
|
||||
import { onGenerationStarted, onGenerationEndedCleanup } from '../generation/injector.js';
|
||||
|
||||
// Rendering
|
||||
import { renderUserStats } from '../rendering/userStats.js';
|
||||
@@ -41,6 +43,9 @@ import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||
// Utils
|
||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||
|
||||
// UI
|
||||
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||||
|
||||
// Chapter checkpoint
|
||||
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||
@@ -105,6 +110,15 @@ export function onMessageSent() {
|
||||
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent (after placeholder check)');
|
||||
// console.log('[RPG Companion] 🟢 NOTE: lastActionWasSwipe will be reset in onMessageReceived after generation completes');
|
||||
|
||||
// Set flag to indicate we're expecting a new message from generation
|
||||
// This allows auto-update to distinguish between new generations and loading chat history
|
||||
setIsAwaitingNewMessage(true);
|
||||
|
||||
// Show FAB loading state for together mode (starts spinning)
|
||||
if (extensionSettings.generationMode === 'together') {
|
||||
setFabLoadingState(true);
|
||||
}
|
||||
|
||||
// For separate mode with auto-update disabled, commit displayed tracker
|
||||
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
||||
if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) {
|
||||
@@ -190,20 +204,16 @@ export async function onMessageReceived(data) {
|
||||
// Remove the tracker code blocks from the visible message
|
||||
let cleanedMessage = responseText;
|
||||
|
||||
// Only remove trackers if saveTrackerHistory is disabled
|
||||
// When enabled, trackers are in <trackers> XML tags which SillyTavern auto-hides
|
||||
if (!extensionSettings.saveTrackerHistory) {
|
||||
// Note: JSON code blocks are hidden from display by regex script (but preserved in message data)
|
||||
// Note: JSON code blocks are hidden from display by regex script (but preserved in message data)
|
||||
|
||||
// Remove old text format code blocks (legacy support)
|
||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||
// Remove any stray "---" dividers that might appear after the code blocks
|
||||
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, '');
|
||||
// Clean up multiple consecutive newlines
|
||||
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
// Remove old text format code blocks (legacy support)
|
||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||
// Remove any stray "---" dividers that might appear after the code blocks
|
||||
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, '');
|
||||
// Clean up multiple consecutive newlines
|
||||
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
|
||||
// Note: <trackers> XML tags are automatically hidden by SillyTavern
|
||||
// Note: <Song - Artist/> tags are also automatically hidden by SillyTavern
|
||||
|
||||
@@ -250,13 +260,20 @@ export async function onMessageReceived(data) {
|
||||
}
|
||||
|
||||
// Trigger auto-update if enabled (for both separate and external modes)
|
||||
if (extensionSettings.autoUpdate) {
|
||||
// Only trigger if this is a newly generated message, not loading chat history
|
||||
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
||||
setTimeout(async () => {
|
||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||
// Update FAB widgets after separate/external mode update completes
|
||||
setFabLoadingState(false);
|
||||
updateFabWidgets();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the awaiting flag after processing the message
|
||||
setIsAwaitingNewMessage(false);
|
||||
|
||||
// Reset the swipe flag after generation completes
|
||||
// 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)
|
||||
@@ -272,6 +289,10 @@ export async function onMessageReceived(data) {
|
||||
// console.log('[RPG Companion] Plot progression generation completed');
|
||||
}
|
||||
|
||||
// Stop FAB loading state and update widgets
|
||||
setFabLoadingState(false);
|
||||
updateFabWidgets();
|
||||
|
||||
// Re-apply checkpoint in case SillyTavern unhid messages during generation
|
||||
await restoreCheckpointOnLoad();
|
||||
}
|
||||
@@ -287,6 +308,12 @@ export function onCharacterChanged() {
|
||||
$(window).off('resize.thoughtPanel');
|
||||
$(document).off('click.thoughtPanel');
|
||||
|
||||
// Auto-switch to the preset associated with this character/group (if any)
|
||||
const presetSwitched = autoSwitchPresetForEntity();
|
||||
// if (presetSwitched) {
|
||||
// console.log('[RPG Companion] Auto-switched preset for character');
|
||||
// }
|
||||
|
||||
// Load chat-specific data when switching chats
|
||||
loadChatData();
|
||||
|
||||
@@ -303,6 +330,9 @@ export function onCharacterChanged() {
|
||||
renderQuests();
|
||||
renderMusicPlayer($musicPlayerContainer[0]);
|
||||
|
||||
// Update FAB widgets with loaded data
|
||||
updateFabWidgets();
|
||||
|
||||
// Update chat thought overlays
|
||||
updateChatThoughts();
|
||||
|
||||
@@ -340,6 +370,7 @@ export function onMessageSwiped(messageIndex) {
|
||||
if (!isExistingSwipe) {
|
||||
// This is a NEW swipe that will trigger generation
|
||||
setLastActionWasSwipe(true);
|
||||
setIsAwaitingNewMessage(true);
|
||||
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
|
||||
} else {
|
||||
// This is navigating to an EXISTING swipe - don't change the flag
|
||||
@@ -436,6 +467,9 @@ export function clearExtensionPrompts() {
|
||||
export async function onGenerationEnded() {
|
||||
// console.log('[RPG Companion] 🏁 onGenerationEnded called');
|
||||
|
||||
// Restore original message content that was modified for historical context injection
|
||||
onGenerationEndedCleanup();
|
||||
|
||||
// Note: isGenerating flag is cleared in onMessageReceived after parsing (together mode)
|
||||
// or in apiClient.js after separate generation completes (separate mode)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { saveChatData } from '../../core/persistence.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { isItemLocked } from '../generation/lockManager.js';
|
||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||
import { updateFabWidgets } from '../ui/mobile.js';
|
||||
|
||||
/**
|
||||
* Helper to generate lock icon HTML if setting is enabled
|
||||
@@ -615,6 +616,9 @@ export function renderInfoBox() {
|
||||
} else {
|
||||
updateInfoBoxField(field, value);
|
||||
}
|
||||
|
||||
// Update FAB widgets to reflect changes
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
// Update location size on input as well (real-time)
|
||||
|
||||
@@ -3,10 +3,35 @@
|
||||
* Handles UI rendering for quests system (main and optional quests)
|
||||
*/
|
||||
|
||||
import { extensionSettings, $questsContainer } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { extensionSettings, $questsContainer, committedTrackerData, lastGeneratedData } from '../../core/state.js';
|
||||
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||
|
||||
/**
|
||||
* Syncs the current extensionSettings.quests to committedTrackerData.userStats
|
||||
* This ensures quest changes made via UI are reflected in the data sent to AI
|
||||
*/
|
||||
function syncQuestsToCommittedData() {
|
||||
const currentData = committedTrackerData.userStats || lastGeneratedData.userStats;
|
||||
if (!currentData) return;
|
||||
|
||||
const trimmed = currentData.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try {
|
||||
const jsonData = JSON.parse(currentData);
|
||||
if (jsonData && typeof jsonData === 'object') {
|
||||
// Update quests in the JSON data
|
||||
jsonData.quests = extensionSettings.quests || { main: 'None', optional: [] };
|
||||
const updatedJSON = JSON.stringify(jsonData, null, 2);
|
||||
committedTrackerData.userStats = updatedJSON;
|
||||
lastGeneratedData.userStats = updatedJSON;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG Quests] Failed to sync quests to committed data:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to generate lock icon HTML if setting is enabled
|
||||
* @param {string} tracker - Tracker name
|
||||
@@ -250,7 +275,10 @@ function attachQuestEventHandlers() {
|
||||
}
|
||||
extensionSettings.quests.optional.push(questTitle);
|
||||
}
|
||||
// Sync quest changes to committedTrackerData so AI sees the addition
|
||||
syncQuestsToCommittedData();
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
renderQuests();
|
||||
}
|
||||
});
|
||||
@@ -278,7 +306,10 @@ function attachQuestEventHandlers() {
|
||||
|
||||
if (questTitle) {
|
||||
extensionSettings.quests.main = questTitle;
|
||||
// Sync quest changes to committedTrackerData so AI sees the edit
|
||||
syncQuestsToCommittedData();
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
renderQuests();
|
||||
}
|
||||
});
|
||||
@@ -293,7 +324,10 @@ function attachQuestEventHandlers() {
|
||||
} else {
|
||||
extensionSettings.quests.optional.splice(index, 1);
|
||||
}
|
||||
// Sync quest changes to committedTrackerData so AI sees the removal
|
||||
syncQuestsToCommittedData();
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
renderQuests();
|
||||
});
|
||||
|
||||
@@ -306,7 +340,10 @@ function attachQuestEventHandlers() {
|
||||
|
||||
if (newTitle && field === 'optional' && index !== undefined) {
|
||||
extensionSettings.quests.optional[index] = newTitle;
|
||||
// Sync quest changes to committedTrackerData so AI sees the edit
|
||||
syncQuestsToCommittedData();
|
||||
saveSettings();
|
||||
saveChatData();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -391,50 +391,10 @@ export function renderThoughts() {
|
||||
debugLog('[RPG Thoughts] ==================== BUILDING HTML ====================');
|
||||
debugLog('[RPG Thoughts] Starting HTML generation for', presentCharacters.length + ' characters');
|
||||
|
||||
// If no characters parsed, show a placeholder editable card
|
||||
// If no characters parsed, show empty state (no placeholder)
|
||||
if (presentCharacters.length === 0) {
|
||||
debugLog('[RPG Thoughts] ⚠ No characters parsed - showing placeholder card');
|
||||
// Get default character portrait
|
||||
let defaultPortrait = FALLBACK_AVATAR_DATA_URI;
|
||||
let defaultName = 'Character';
|
||||
|
||||
if (this_chid !== undefined && characters[this_chid]) {
|
||||
if (characters[this_chid].avatar && characters[this_chid].avatar !== 'none') {
|
||||
const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar);
|
||||
if (thumbnailUrl) {
|
||||
defaultPortrait = thumbnailUrl;
|
||||
}
|
||||
}
|
||||
defaultName = characters[this_chid].name || 'Character';
|
||||
}
|
||||
|
||||
html += '<div class="rpg-thoughts-content">';
|
||||
html += `
|
||||
<div class="rpg-character-card" data-character-name="${defaultName}">
|
||||
<div class="rpg-character-avatar">
|
||||
<img src="${defaultPortrait}" alt="${defaultName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">⚖️</div>
|
||||
</div>
|
||||
<div class="rpg-character-info">
|
||||
<div class="rpg-character-header">
|
||||
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="emoji" title="Click to edit emoji">😊</span>
|
||||
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="name" title="Click to edit name">${defaultName}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add custom fields dynamically
|
||||
for (const field of enabledFields) {
|
||||
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
|
||||
html += `
|
||||
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="${field.name}" title="Click to edit ${field.name}"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
html += '</div>';
|
||||
debugLog('[RPG Thoughts] ⚠ No characters parsed - showing empty state');
|
||||
html += '<div class="rpg-thoughts-content"></div>';
|
||||
} else {
|
||||
html += '<div class="rpg-thoughts-content">';
|
||||
|
||||
@@ -540,6 +500,7 @@ export function renderThoughts() {
|
||||
<div class="rpg-character-header">
|
||||
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
|
||||
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
|
||||
<button class="rpg-character-remove" data-character="${char.name}" title="Remove character">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -650,6 +611,15 @@ export function renderThoughts() {
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
// Add event listener for character remove button
|
||||
$thoughtsContainer.find('.rpg-character-remove').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const characterName = $(this).data('character');
|
||||
removeCharacter(characterName);
|
||||
});
|
||||
|
||||
// Add event listener for avatar upload clicks
|
||||
$thoughtsContainer.find('.rpg-avatar-upload').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
@@ -703,6 +673,121 @@ export function renderThoughts() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a character from Present Characters data and re-renders.
|
||||
*
|
||||
* @param {string} characterName - Name of the character to remove
|
||||
*/
|
||||
export function removeCharacter(characterName) {
|
||||
if (!lastGeneratedData.characterThoughts) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if data is in JSON format
|
||||
let isJSON = false;
|
||||
let parsedData = null;
|
||||
|
||||
try {
|
||||
parsedData = typeof lastGeneratedData.characterThoughts === 'string'
|
||||
? JSON.parse(lastGeneratedData.characterThoughts)
|
||||
: lastGeneratedData.characterThoughts;
|
||||
|
||||
if (Array.isArray(parsedData) || (parsedData && parsedData.characters)) {
|
||||
isJSON = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, treat as text format
|
||||
}
|
||||
|
||||
if (isJSON) {
|
||||
// JSON format - remove character from array
|
||||
let characters = Array.isArray(parsedData) ? parsedData : parsedData.characters;
|
||||
characters = characters.filter(char => char.name !== characterName);
|
||||
|
||||
if (Array.isArray(parsedData)) {
|
||||
parsedData = characters;
|
||||
} else {
|
||||
parsedData.characters = characters;
|
||||
}
|
||||
|
||||
const updatedJSON = JSON.stringify(parsedData, null, 2);
|
||||
lastGeneratedData.characterThoughts = updatedJSON;
|
||||
committedTrackerData.characterThoughts = updatedJSON;
|
||||
} else {
|
||||
// Text format - remove character block
|
||||
const lines = lastGeneratedData.characterThoughts.split('\n');
|
||||
const dividerIndex = lines.findIndex(line => line.includes('---'));
|
||||
|
||||
if (dividerIndex === -1) return;
|
||||
|
||||
// Find the character block to remove
|
||||
let startLineIndex = -1;
|
||||
let endLineIndex = -1;
|
||||
|
||||
for (let i = dividerIndex + 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// Check if this is the start of the character block
|
||||
if (line.startsWith('Name:')) {
|
||||
const nameMatch = line.match(/^Name:\s*(.+)/);
|
||||
if (nameMatch && nameMatch[1].trim() === characterName) {
|
||||
startLineIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found the start, look for the end
|
||||
if (startLineIndex !== -1 && i > startLineIndex) {
|
||||
// End of block is either another "Name:" line or end of content
|
||||
if (line.startsWith('Name:') || i === lines.length - 1) {
|
||||
endLineIndex = line.startsWith('Name:') ? i - 1 : i;
|
||||
|
||||
// Remove empty lines at the end of the block
|
||||
while (endLineIndex > startLineIndex && !lines[endLineIndex].trim()) {
|
||||
endLineIndex--;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the character block
|
||||
if (startLineIndex !== -1 && endLineIndex !== -1) {
|
||||
lines.splice(startLineIndex, endLineIndex - startLineIndex + 1);
|
||||
|
||||
// Remove empty lines after removal to keep formatting clean
|
||||
let i = startLineIndex;
|
||||
while (i < lines.length && !lines[i].trim()) {
|
||||
lines.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
lastGeneratedData.characterThoughts = lines.join('\n');
|
||||
committedTrackerData.characterThoughts = lines.join('\n');
|
||||
}
|
||||
|
||||
// Update message swipe data
|
||||
const chat = getContext().chat;
|
||||
if (chat && chat.length > 0) {
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
const message = chat[i];
|
||||
if (!message.is_user) {
|
||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||
const swipeId = message.swipe_id || 0;
|
||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lastGeneratedData.characterThoughts;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveChatData();
|
||||
|
||||
// Re-render to show updated character list
|
||||
renderThoughts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a specific character field in Present Characters data and re-renders.
|
||||
* Works with the new multi-line format.
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||
import { updateFabWidgets } from '../ui/mobile.js';
|
||||
|
||||
/**
|
||||
* Builds the user stats text string using custom stat names
|
||||
@@ -424,8 +425,9 @@ export function renderUserStats() {
|
||||
saveChatData();
|
||||
updateMessageSwipeData();
|
||||
|
||||
// Re-render to update the bar
|
||||
// Re-render to update the bar and FAB widgets
|
||||
renderUserStats();
|
||||
updateFabWidgets();
|
||||
});
|
||||
|
||||
// Add event listeners for mood/conditions editing
|
||||
|
||||
+404
-3
@@ -3,7 +3,7 @@
|
||||
* Handles mobile-specific UI functionality: FAB dragging, tabs, keyboard handling
|
||||
*/
|
||||
|
||||
import { extensionSettings } from '../../core/state.js';
|
||||
import { extensionSettings, committedTrackerData, lastGeneratedData } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
|
||||
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
||||
@@ -106,6 +106,14 @@ export function setupMobileToggle() {
|
||||
right: 'auto',
|
||||
bottom: 'auto'
|
||||
});
|
||||
// Also update widget container position during drag
|
||||
const $container = $('#rpg-fab-widget-container');
|
||||
if ($container.length > 0) {
|
||||
$container.css({
|
||||
top: pendingY + 'px',
|
||||
left: pendingX + 'px'
|
||||
});
|
||||
}
|
||||
pendingX = null;
|
||||
pendingY = null;
|
||||
}
|
||||
@@ -253,7 +261,10 @@ export function setupMobileToggle() {
|
||||
// console.log('[RPG Mobile] Saved new FAB position (mouse):', newPosition);
|
||||
|
||||
// Constrain to viewport bounds (now that position is saved)
|
||||
setTimeout(() => constrainFabToViewport(), 10);
|
||||
setTimeout(() => {
|
||||
constrainFabToViewport();
|
||||
updateFabWidgetPosition(); // Update widget container position
|
||||
}, 10);
|
||||
|
||||
// Re-enable transitions with smooth animation
|
||||
setTimeout(() => {
|
||||
@@ -294,7 +305,10 @@ export function setupMobileToggle() {
|
||||
// console.log('[RPG Mobile] Saved new FAB position:', newPosition);
|
||||
|
||||
// Constrain to viewport bounds (now that position is saved)
|
||||
setTimeout(() => constrainFabToViewport(), 10);
|
||||
setTimeout(() => {
|
||||
constrainFabToViewport();
|
||||
updateFabWidgetPosition(); // Update widget container position
|
||||
}, 10);
|
||||
|
||||
// Re-enable transitions with smooth animation
|
||||
setTimeout(() => {
|
||||
@@ -1230,3 +1244,390 @@ export function setupDebugButtonDrag() {
|
||||
isDragging = false;
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FAB WIDGETS - Info display around FAB button
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Updates the FAB widgets display based on current tracker data and settings.
|
||||
* Widgets are positioned in 8 positions around the FAB (N, NE, E, SE, S, SW, W, NW).
|
||||
*/
|
||||
export function updateFabWidgets() {
|
||||
const $fab = $('#rpg-mobile-toggle');
|
||||
if ($fab.length === 0) return;
|
||||
|
||||
// Remove existing widget container and clean up event listeners
|
||||
$('#rpg-fab-widget-container').remove();
|
||||
$(document).off('click.fabWidgets touchstart.fabWidgets');
|
||||
|
||||
// Check if widgets are enabled
|
||||
const widgetSettings = extensionSettings.mobileFabWidgets;
|
||||
if (!widgetSettings || !widgetSettings.enabled) return;
|
||||
|
||||
// Don't show widgets on desktop or when panel is open
|
||||
if (window.innerWidth > 1000) return;
|
||||
|
||||
// Get tracker data - prefer lastGeneratedData (most recent) over committedTrackerData
|
||||
const infoBox = lastGeneratedData?.infoBox || committedTrackerData?.infoBox;
|
||||
const userStats = lastGeneratedData?.userStats || committedTrackerData?.userStats;
|
||||
|
||||
// Parse infoBox if it's a string
|
||||
let infoData = null;
|
||||
if (infoBox) {
|
||||
try {
|
||||
infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox;
|
||||
} catch (e) {
|
||||
console.warn('[RPG FAB Widgets] Failed to parse infoBox:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse userStats if it's a string
|
||||
let statsData = null;
|
||||
if (userStats) {
|
||||
try {
|
||||
statsData = typeof userStats === 'string' ? JSON.parse(userStats) : userStats;
|
||||
} catch (e) {
|
||||
console.warn('[RPG FAB Widgets] Failed to parse userStats:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Create widget container positioned at FAB location
|
||||
const fabOffset = $fab.offset();
|
||||
const fabWidth = $fab.outerWidth();
|
||||
const fabHeight = $fab.outerHeight();
|
||||
|
||||
const $container = $('<div id="rpg-fab-widget-container" class="rpg-fab-widget-container"></div>');
|
||||
$container.css({
|
||||
top: fabOffset.top + 'px',
|
||||
left: fabOffset.left + 'px',
|
||||
width: fabWidth + 'px',
|
||||
height: fabHeight + 'px'
|
||||
});
|
||||
|
||||
// Build widgets based on settings - auto-assign positions sequentially
|
||||
const widgets = [];
|
||||
|
||||
// Collect enabled widgets in display priority order
|
||||
// Large widgets (Stats, Attributes) go to West/Northwest
|
||||
// Small widgets spread around other positions
|
||||
|
||||
// Weather Icon (small)
|
||||
if (widgetSettings.weatherIcon?.enabled && infoData?.weather?.emoji) {
|
||||
widgets.push({
|
||||
type: 'small',
|
||||
html: `<div class="rpg-fab-widget rpg-fab-widget-weather-icon" title="Weather">${infoData.weather.emoji}</div>`
|
||||
});
|
||||
}
|
||||
|
||||
// Weather Description (small)
|
||||
if (widgetSettings.weatherDesc?.enabled && infoData?.weather?.forecast) {
|
||||
const desc = infoData.weather.forecast.length > 15 ? infoData.weather.forecast.substring(0, 13) + '…' : infoData.weather.forecast;
|
||||
widgets.push({
|
||||
type: 'small',
|
||||
html: `<div class="rpg-fab-widget rpg-fab-widget-weather-desc" title="${infoData.weather.forecast}">${desc}</div>`
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to create expandable text widget HTML
|
||||
const createExpandableText = (fullText, maxLen, emoji) => {
|
||||
if (fullText.length <= maxLen) {
|
||||
return `${emoji} ${fullText}`;
|
||||
}
|
||||
const truncated = fullText.substring(0, maxLen - 2) + '…';
|
||||
return `${emoji} <span class="rpg-truncated">${truncated}</span><span class="rpg-full-text">${fullText}</span>`;
|
||||
};
|
||||
|
||||
// Check if text needs truncation for data attribute
|
||||
const needsExpand = (text, maxLen) => text.length > maxLen;
|
||||
|
||||
// Helper to parse time string and calculate clock hand angles
|
||||
const parseTimeForClock = (timeStr) => {
|
||||
const timeMatch = timeStr.match(/(\d+):(\d+)/);
|
||||
if (timeMatch) {
|
||||
const hours = parseInt(timeMatch[1]);
|
||||
const minutes = parseInt(timeMatch[2]);
|
||||
const hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
|
||||
const minuteAngle = minutes * 6; // 6° per minute
|
||||
return { hourAngle, minuteAngle };
|
||||
}
|
||||
return { hourAngle: 0, minuteAngle: 0 };
|
||||
};
|
||||
|
||||
// Clock/Time (bottom position with animated clock face)
|
||||
if (widgetSettings.clock?.enabled && infoData?.time) {
|
||||
const timeStr = infoData.time.end || infoData.time.value || infoData.time.start || '';
|
||||
if (timeStr) {
|
||||
const { hourAngle, minuteAngle } = parseTimeForClock(timeStr);
|
||||
widgets.push({
|
||||
type: 'bottom', // Special type for bottom position
|
||||
html: `<div class="rpg-fab-widget rpg-fab-widget-clock" title="${timeStr}">
|
||||
<div class="rpg-fab-clock-face">
|
||||
<div class="rpg-fab-clock-hour" style="transform: rotate(${hourAngle}deg)"></div>
|
||||
<div class="rpg-fab-clock-minute" style="transform: rotate(${minuteAngle}deg)"></div>
|
||||
<div class="rpg-fab-clock-center"></div>
|
||||
</div>
|
||||
<span class="rpg-fab-clock-time">${timeStr}</span>
|
||||
</div>`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Date (small)
|
||||
if (widgetSettings.date?.enabled && infoData?.date?.value) {
|
||||
const dateVal = infoData.date.value;
|
||||
const expandAttr = needsExpand(dateVal, 12) ? ' data-full-text="true"' : '';
|
||||
widgets.push({
|
||||
type: 'small',
|
||||
html: `<div class="rpg-fab-widget rpg-fab-widget-date"${expandAttr} title="${dateVal}">${createExpandableText(dateVal, 12, '📅')}</div>`
|
||||
});
|
||||
}
|
||||
|
||||
// Location (small)
|
||||
if (widgetSettings.location?.enabled && infoData?.location?.value) {
|
||||
const loc = infoData.location.value;
|
||||
const expandAttr = needsExpand(loc, 14) ? ' data-full-text="true"' : '';
|
||||
widgets.push({
|
||||
type: 'small',
|
||||
html: `<div class="rpg-fab-widget rpg-fab-widget-location"${expandAttr} title="${loc}">${createExpandableText(loc, 14, '📍')}</div>`
|
||||
});
|
||||
}
|
||||
|
||||
// Stats (large - goes to West) - respects trackerConfig.userStats.customStats
|
||||
// Use extensionSettings.userStats as primary source (contains all stats), fallback to committedTrackerData
|
||||
let allStats = [];
|
||||
try {
|
||||
const userStatsJson = extensionSettings.userStats;
|
||||
const parsedUserStats = typeof userStatsJson === 'string' ? JSON.parse(userStatsJson) : userStatsJson;
|
||||
if (parsedUserStats?.stats) {
|
||||
allStats = parsedUserStats.stats;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RPG FAB Widgets] Failed to parse extensionSettings.userStats:', e);
|
||||
}
|
||||
// Fallback to statsData if extensionSettings.userStats is empty
|
||||
if (allStats.length === 0 && statsData?.stats) {
|
||||
allStats = statsData.stats;
|
||||
}
|
||||
|
||||
if (widgetSettings.stats?.enabled && allStats.length > 0) {
|
||||
// Get enabled stats from trackerConfig - match by id (lowercase)
|
||||
const configuredStats = extensionSettings.trackerConfig?.userStats?.customStats || [];
|
||||
const enabledStatMap = new Map();
|
||||
configuredStats.forEach(s => {
|
||||
if (s.enabled !== false) {
|
||||
enabledStatMap.set(s.id?.toLowerCase(), true);
|
||||
enabledStatMap.set(s.name?.toLowerCase(), true);
|
||||
}
|
||||
});
|
||||
|
||||
const statsHtml = allStats
|
||||
.filter(s => {
|
||||
// If no config, show all stats
|
||||
if (configuredStats.length === 0) return true;
|
||||
// Check if stat is enabled in trackerConfig (match by id or name, case-insensitive)
|
||||
const statId = s.id?.toLowerCase();
|
||||
const statName = s.name?.toLowerCase();
|
||||
return enabledStatMap.has(statId) || enabledStatMap.has(statName);
|
||||
})
|
||||
.map(stat => {
|
||||
const value = typeof stat.value === 'number' ? stat.value : parseInt(stat.value) || 0;
|
||||
const color = getStatColor(value);
|
||||
const abbr = stat.name.substring(0, 3).toUpperCase();
|
||||
return `<span class="rpg-fab-widget-stat-item" title="${stat.name}: ${value}" style="color: ${color};">${abbr}:${value}</span>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
if (statsHtml) {
|
||||
widgets.push({
|
||||
type: 'large',
|
||||
preferredPos: 6, // West
|
||||
html: `<div class="rpg-fab-widget rpg-fab-widget-stats"><div class="rpg-fab-widget-stats-row">${statsHtml}</div></div>`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// RPG Attributes (large - goes to Northwest) - respects trackerConfig.userStats.rpgAttributes
|
||||
if (widgetSettings.attributes?.enabled) {
|
||||
// Check if RPG attributes are enabled in trackerConfig
|
||||
const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||
|
||||
if (showRPGAttributes && extensionSettings.classicStats) {
|
||||
// Get enabled attributes from trackerConfig
|
||||
const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || [];
|
||||
const enabledAttrIds = configuredAttrs.filter(a => a.enabled !== false).map(a => a.id);
|
||||
|
||||
const attrs = extensionSettings.classicStats;
|
||||
const attrItems = Object.entries(attrs)
|
||||
.filter(([key]) => {
|
||||
// Check if attribute is enabled in trackerConfig
|
||||
if (enabledAttrIds.length > 0) {
|
||||
return enabledAttrIds.includes(key.toLowerCase());
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(([key, value]) => `<div class="rpg-fab-widget-attr-item"><span class="rpg-fab-widget-attr-name">${key.toUpperCase()}</span><span class="rpg-fab-widget-attr-value">${value}</span></div>`)
|
||||
.join('');
|
||||
|
||||
if (attrItems) {
|
||||
widgets.push({
|
||||
type: 'large',
|
||||
preferredPos: 7, // Northwest
|
||||
html: `<div class="rpg-fab-widget rpg-fab-widget-attributes" title="Attributes"><div class="rpg-fab-widget-attr-grid">${attrItems}</div></div>`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-assign positions intelligently
|
||||
// Large widgets get their preferred positions first (West=6, Northwest=7)
|
||||
// Bottom widgets get position 4 (South)
|
||||
// Small widgets fill remaining positions clockwise from North (0)
|
||||
const usedPositions = new Set();
|
||||
const positionedWidgets = [];
|
||||
|
||||
// Position order for small widgets: N(0), NE(1), E(2), SE(3), SW(5) - skip S(4) for bottom/clock
|
||||
const smallPositionOrder = [0, 1, 2, 3, 5];
|
||||
let smallPosIndex = 0;
|
||||
|
||||
// Check if only one large widget exists (for centering)
|
||||
const largeWidgets = widgets.filter(w => w.type === 'large');
|
||||
const singleLargeWidget = largeWidgets.length === 1;
|
||||
|
||||
// First: assign bottom widgets to position 4 (South)
|
||||
widgets.filter(w => w.type === 'bottom').forEach(w => {
|
||||
const pos = 4; // South position
|
||||
usedPositions.add(pos);
|
||||
const finalHtml = w.html.replace('class="rpg-fab-widget', `class="rpg-fab-widget rpg-fab-widget-pos-${pos}`);
|
||||
positionedWidgets.push({ position: pos, html: finalHtml });
|
||||
});
|
||||
|
||||
// Second: assign large widgets to their preferred positions
|
||||
largeWidgets.forEach(w => {
|
||||
let pos = w.preferredPos;
|
||||
// If preferred position is taken, find next available from large positions
|
||||
if (usedPositions.has(pos)) {
|
||||
pos = pos === 6 ? 7 : 6; // Try the other large position
|
||||
}
|
||||
usedPositions.add(pos);
|
||||
// Add centered class if this is the only large widget
|
||||
const centeredClass = singleLargeWidget ? ' rpg-fab-widget-centered' : '';
|
||||
const finalHtml = w.html.replace('class="rpg-fab-widget', `class="rpg-fab-widget rpg-fab-widget-pos-${pos}${centeredClass}`);
|
||||
positionedWidgets.push({ position: pos, html: finalHtml });
|
||||
});
|
||||
|
||||
// Third: assign small widgets to remaining positions
|
||||
widgets.filter(w => w.type === 'small').forEach(w => {
|
||||
// Find next available position from small position order
|
||||
while (smallPosIndex < smallPositionOrder.length && usedPositions.has(smallPositionOrder[smallPosIndex])) {
|
||||
smallPosIndex++;
|
||||
}
|
||||
const pos = smallPosIndex < smallPositionOrder.length ? smallPositionOrder[smallPosIndex] : (smallPosIndex % 8);
|
||||
usedPositions.add(pos);
|
||||
smallPosIndex++;
|
||||
const finalHtml = w.html.replace('class="rpg-fab-widget', `class="rpg-fab-widget rpg-fab-widget-pos-${pos}`);
|
||||
positionedWidgets.push({ position: pos, html: finalHtml });
|
||||
});
|
||||
|
||||
// Add widgets to container
|
||||
positionedWidgets.forEach(w => $container.append(w.html));
|
||||
|
||||
// Append container to body
|
||||
if (positionedWidgets.length > 0) {
|
||||
$('body').append($container);
|
||||
|
||||
// Add mobile tap handler for expandable widgets
|
||||
$container.find('.rpg-fab-widget[data-full-text]').on('click touchstart', function(e) {
|
||||
e.stopPropagation();
|
||||
const $this = $(this);
|
||||
const wasExpanded = $this.hasClass('expanded');
|
||||
|
||||
// Collapse all other expanded widgets
|
||||
$container.find('.rpg-fab-widget.expanded').removeClass('expanded');
|
||||
|
||||
// Toggle this one
|
||||
if (!wasExpanded) {
|
||||
$this.addClass('expanded');
|
||||
}
|
||||
});
|
||||
|
||||
// Collapse on tap outside
|
||||
$(document).on('click.fabWidgets touchstart.fabWidgets', function(e) {
|
||||
if (!$(e.target).closest('.rpg-fab-widget').length) {
|
||||
$container.find('.rpg-fab-widget.expanded').removeClass('expanded');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a color for a stat value (0-100) using a gradient from low to high.
|
||||
* @param {number} value - The stat value (0-100)
|
||||
* @returns {string} CSS color value
|
||||
*/
|
||||
function getStatColor(value) {
|
||||
const lowColor = extensionSettings.statBarColorLow || '#cc3333';
|
||||
const highColor = extensionSettings.statBarColorHigh || '#33cc66';
|
||||
|
||||
// Simple linear interpolation between low and high colors
|
||||
const percent = Math.min(100, Math.max(0, value)) / 100;
|
||||
|
||||
// Parse colors
|
||||
const lowRGB = hexToRgb(lowColor);
|
||||
const highRGB = hexToRgb(highColor);
|
||||
|
||||
if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor;
|
||||
|
||||
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
|
||||
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
|
||||
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hex color to RGB object.
|
||||
* @param {string} hex - Hex color string (e.g., "#cc3333")
|
||||
* @returns {{r: number, g: number, b: number}|null}
|
||||
*/
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the FAB widget container position to match FAB button position.
|
||||
* Call this after FAB is dragged.
|
||||
*/
|
||||
export function updateFabWidgetPosition() {
|
||||
const $fab = $('#rpg-mobile-toggle');
|
||||
const $container = $('#rpg-fab-widget-container');
|
||||
|
||||
if ($fab.length === 0 || $container.length === 0) return;
|
||||
|
||||
const fabOffset = $fab.offset();
|
||||
$container.css({
|
||||
top: fabOffset.top + 'px',
|
||||
left: fabOffset.left + 'px'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the FAB loading state (spinning animation during API requests).
|
||||
* @param {boolean} loading - Whether to show loading state
|
||||
*/
|
||||
export function setFabLoadingState(loading) {
|
||||
const $fab = $('#rpg-mobile-toggle');
|
||||
if ($fab.length === 0) return;
|
||||
|
||||
if (loading) {
|
||||
$fab.addClass('rpg-fab-loading');
|
||||
} else {
|
||||
$fab.removeClass('rpg-fab-loading');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { extensionSettings } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT } from '../generation/promptBuilder.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT } from '../generation/promptBuilder.js';
|
||||
|
||||
let $editorModal = null;
|
||||
let tempPrompts = null; // Temporary prompts for cancel functionality
|
||||
@@ -13,6 +13,8 @@ let tempPrompts = null; // Temporary prompts for cancel functionality
|
||||
const DEFAULT_PROMPTS = {
|
||||
html: DEFAULT_HTML_PROMPT,
|
||||
dialogueColoring: DEFAULT_DIALOGUE_COLORING_PROMPT,
|
||||
deception: DEFAULT_DECEPTION_PROMPT,
|
||||
cyoa: DEFAULT_CYOA_PROMPT,
|
||||
spotify: DEFAULT_SPOTIFY_PROMPT,
|
||||
narrator: DEFAULT_NARRATOR_PROMPT,
|
||||
plotRandom: 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.',
|
||||
@@ -26,7 +28,7 @@ Next, detail the facial specifics. Describe the character's current expression,
|
||||
Finally, infuse with aesthetics. Define the artistic style, medium (e.g., digital art, oil painting), and visual tone (e.g., cinematic lighting, ethereal atmosphere).
|
||||
Your final description must be objective and concrete, and the use of metaphors and emotional rhetoric is strictly prohibited. It must also not contain meta tags or drawing instructions such as "8K" or "masterpiece".
|
||||
Output only the final, modified prompt; do not output anything else.`,
|
||||
trackerInstructions: 'Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that {userName} perceives about the current scene and the present characters. For example: "Location" becomes Forest Clearing, "Mood Emoji" becomes "😊". Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user\'s actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).',
|
||||
trackerInstructions: 'Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that {userName} perceives about the current scene and the present characters. For example: "Location" becomes Forest Clearing, "Mood Emoji" becomes "😊". DO NOT include {userName} in the characters section, only NPCs. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user\'s actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).',
|
||||
trackerContinuation: 'After updating the trackers, continue directly from where the last message in the chat history left off. Ensure the trackers you provide naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting the protagonist\'s performance, low hygiene influencing their social interactions, environmental factors shaping the scene, a character\'s emotional state coloring their responses, and so on. Remember, all placeholders (e.g., "Location", "Mood Emoji") MUST be replaced with actual content.',
|
||||
combatNarrative: 'Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don\'t fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn\'t/didn\'t." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn\'t. Absolutely no asterisks, ellipses, or em-dashes. Explicit content is allowed, no plot armor. Do not play for {userName}. Keep your response length under 150 words. Never end on handover cues; finish naturally.\nCRITICAL: Do not repeat, echo, parrot, or restate distinctive words, phrases, and dialogues from the user\'s last message. If reacting to speech, show interpretation or response, not repetition.\nEXAMPLE: "Are you a gooner?" User asks.\nBAD: "Gooner?"\nGOOD: A flat look. "What type of question is that?"'
|
||||
};
|
||||
@@ -46,7 +48,7 @@ export function initPromptsEditor() {
|
||||
$(document).on('click', '#rpg-prompts-save', function() {
|
||||
savePrompts();
|
||||
closePromptsEditor();
|
||||
toastr.success('Prompts saved successfully');
|
||||
toastr.success('Prompts saved successfully.');
|
||||
});
|
||||
|
||||
// Cancel button
|
||||
@@ -62,14 +64,14 @@ export function initPromptsEditor() {
|
||||
// Restore All button
|
||||
$(document).on('click', '#rpg-prompts-restore-all', function() {
|
||||
restoreAllToDefaults();
|
||||
toastr.success('All prompts restored to defaults');
|
||||
toastr.success('All prompts restored to defaults.');
|
||||
});
|
||||
|
||||
// Individual restore buttons
|
||||
$(document).on('click', '.rpg-restore-prompt-btn', function() {
|
||||
const promptType = $(this).data('prompt');
|
||||
restorePromptToDefault(promptType);
|
||||
toastr.success('Prompt restored to default');
|
||||
toastr.success('Prompt restored to default.');
|
||||
});
|
||||
|
||||
// Close on background click
|
||||
@@ -93,6 +95,8 @@ function openPromptsEditor() {
|
||||
tempPrompts = {
|
||||
html: extensionSettings.customHtmlPrompt || '',
|
||||
dialogueColoring: extensionSettings.customDialogueColoringPrompt || '',
|
||||
deception: extensionSettings.customDeceptionPrompt || '',
|
||||
cyoa: extensionSettings.customCYOAPrompt || '',
|
||||
spotify: extensionSettings.customSpotifyPrompt || '',
|
||||
narrator: extensionSettings.customNarratorPrompt || '',
|
||||
plotRandom: extensionSettings.customPlotRandomPrompt || '',
|
||||
@@ -106,6 +110,8 @@ function openPromptsEditor() {
|
||||
// Load current values or defaults
|
||||
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
|
||||
$('#rpg-prompt-dialogue-coloring').val(extensionSettings.customDialogueColoringPrompt || DEFAULT_PROMPTS.dialogueColoring);
|
||||
$('#rpg-prompt-deception').val(extensionSettings.customDeceptionPrompt || DEFAULT_PROMPTS.deception);
|
||||
$('#rpg-prompt-cyoa').val(extensionSettings.customCYOAPrompt || DEFAULT_PROMPTS.cyoa);
|
||||
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
|
||||
$('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator);
|
||||
$('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom);
|
||||
@@ -143,6 +149,8 @@ function closePromptsEditor() {
|
||||
function savePrompts() {
|
||||
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
|
||||
extensionSettings.customDialogueColoringPrompt = $('#rpg-prompt-dialogue-coloring').val().trim();
|
||||
extensionSettings.customDeceptionPrompt = $('#rpg-prompt-deception').val().trim();
|
||||
extensionSettings.customCYOAPrompt = $('#rpg-prompt-cyoa').val().trim();
|
||||
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim();
|
||||
extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim();
|
||||
extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim();
|
||||
@@ -171,6 +179,12 @@ function restorePromptToDefault(promptType) {
|
||||
case 'dialogueColoring':
|
||||
extensionSettings.customDialogueColoringPrompt = '';
|
||||
break;
|
||||
case 'deception':
|
||||
extensionSettings.customDeceptionPrompt = '';
|
||||
break;
|
||||
case 'cyoa':
|
||||
extensionSettings.customCYOAPrompt = '';
|
||||
break;
|
||||
case 'spotify':
|
||||
extensionSettings.customSpotifyPrompt = '';
|
||||
break;
|
||||
@@ -206,6 +220,8 @@ function restorePromptToDefault(promptType) {
|
||||
function restoreAllToDefaults() {
|
||||
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
|
||||
$('#rpg-prompt-dialogue-coloring').val(DEFAULT_PROMPTS.dialogueColoring);
|
||||
$('#rpg-prompt-deception').val(DEFAULT_PROMPTS.deception);
|
||||
$('#rpg-prompt-cyoa').val(DEFAULT_PROMPTS.cyoa);
|
||||
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
|
||||
$('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator);
|
||||
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
|
||||
@@ -218,6 +234,8 @@ function restoreAllToDefaults() {
|
||||
// Clear all custom prompts
|
||||
extensionSettings.customHtmlPrompt = '';
|
||||
extensionSettings.customDialogueColoringPrompt = '';
|
||||
extensionSettings.customDeceptionPrompt = '';
|
||||
extensionSettings.customCYOAPrompt = '';
|
||||
extensionSettings.customSpotifyPrompt = '';
|
||||
extensionSettings.customNarratorPrompt = '';
|
||||
extensionSettings.customPlotRandomPrompt = '';
|
||||
|
||||
@@ -138,6 +138,8 @@ export function updateFeatureTogglesVisibility() {
|
||||
const $featuresRow = $('#rpg-features-row');
|
||||
const $htmlToggle = $('#rpg-html-toggle-wrapper');
|
||||
const $dialogueColoringToggle = $('#rpg-dialogue-coloring-toggle-wrapper');
|
||||
const $deceptionToggle = $('#rpg-deception-toggle-wrapper');
|
||||
const $cyoaToggle = $('#rpg-cyoa-toggle-wrapper');
|
||||
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
|
||||
|
||||
const $dynamicWeatherToggle = $('#rpg-dynamic-weather-toggle-wrapper');
|
||||
@@ -147,6 +149,8 @@ export function updateFeatureTogglesVisibility() {
|
||||
// Show/hide individual toggles
|
||||
$htmlToggle.toggle(extensionSettings.showHtmlToggle);
|
||||
$dialogueColoringToggle.toggle(extensionSettings.showDialogueColoringToggle);
|
||||
$deceptionToggle.toggle(extensionSettings.showDeceptionToggle ?? true);
|
||||
$cyoaToggle.toggle(extensionSettings.showCYOAToggle ?? true);
|
||||
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
|
||||
|
||||
$dynamicWeatherToggle.toggle(extensionSettings.showDynamicWeatherToggle);
|
||||
@@ -156,6 +160,8 @@ export function updateFeatureTogglesVisibility() {
|
||||
// Hide entire row if all toggles are hidden
|
||||
const anyVisible = extensionSettings.showHtmlToggle ||
|
||||
extensionSettings.showDialogueColoringToggle ||
|
||||
(extensionSettings.showDeceptionToggle ?? true) ||
|
||||
(extensionSettings.showCYOAToggle ?? true) ||
|
||||
extensionSettings.showSpotifyToggle ||
|
||||
extensionSettings.showDynamicWeatherToggle ||
|
||||
extensionSettings.showNarratorMode ||
|
||||
|
||||
+580
-44
@@ -4,10 +4,31 @@
|
||||
*/
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { extensionSettings } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import {
|
||||
saveSettings,
|
||||
getPresets,
|
||||
getPreset,
|
||||
getActivePresetId,
|
||||
getDefaultPresetId,
|
||||
setDefaultPreset,
|
||||
isDefaultPreset,
|
||||
createPreset,
|
||||
saveToPreset,
|
||||
loadPreset,
|
||||
renamePreset,
|
||||
deletePreset,
|
||||
associatePresetWithCurrentEntity,
|
||||
removePresetAssociationForCurrentEntity,
|
||||
getPresetForCurrentEntity,
|
||||
hasPresetAssociation,
|
||||
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';
|
||||
@@ -78,6 +99,112 @@ export function initTrackerEditor() {
|
||||
$(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()) {
|
||||
// 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();
|
||||
updatePresetUI();
|
||||
toastr.success(`Switched to preset "${getPreset(presetId)?.name || 'Unknown'}".`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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() {
|
||||
if ($(this).is(':checked')) {
|
||||
associatePresetWithCurrentEntity();
|
||||
toastr.info(`This preset will be used for ${getCurrentEntityName()}.`);
|
||||
} else {
|
||||
removePresetAssociationForCurrentEntity();
|
||||
toastr.info(`Preset association removed for ${getCurrentEntityName()}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(`<option value="${id}">${starPrefix}${preset.name}</option>`);
|
||||
}
|
||||
$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
|
||||
const isAssociated = hasPresetAssociation();
|
||||
$('#rpg-preset-associate').prop('checked', isAssociated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +218,9 @@ function openTrackerEditor() {
|
||||
const theme = extensionSettings.theme || 'modern';
|
||||
$editorModal.attr('data-theme', theme);
|
||||
|
||||
// Update preset UI
|
||||
updatePresetUI();
|
||||
|
||||
renderEditorUI();
|
||||
$editorModal.addClass('is-open').css('display', '');
|
||||
}
|
||||
@@ -116,12 +246,20 @@ function closeTrackerEditor() {
|
||||
*/
|
||||
function applyTrackerConfig() {
|
||||
tempConfig = null; // Clear temp config
|
||||
saveSettings();
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,40 +269,44 @@ function resetToDefaults() {
|
||||
extensionSettings.trackerConfig = {
|
||||
userStats: {
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false },
|
||||
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false }
|
||||
],
|
||||
showRPGAttributes: true,
|
||||
rpgAttributes: [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
|
||||
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
|
||||
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
|
||||
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
|
||||
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
|
||||
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
|
||||
],
|
||||
statusSection: {
|
||||
enabled: true,
|
||||
showMoodEmoji: true,
|
||||
customFields: ['Conditions']
|
||||
customFields: ['Conditions'],
|
||||
persistInHistory: false
|
||||
},
|
||||
skillsSection: {
|
||||
enabled: false,
|
||||
label: 'Skills',
|
||||
customFields: []
|
||||
}
|
||||
customFields: [],
|
||||
persistInHistory: false
|
||||
},
|
||||
inventoryPersistInHistory: false,
|
||||
questsPersistInHistory: false
|
||||
},
|
||||
infoBox: {
|
||||
widgets: {
|
||||
date: { enabled: true, format: 'Weekday, Month, Year' },
|
||||
weather: { enabled: true },
|
||||
temperature: { enabled: true, unit: 'C' },
|
||||
time: { enabled: true },
|
||||
location: { enabled: true },
|
||||
recentEvents: { enabled: true }
|
||||
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: {
|
||||
@@ -189,13 +331,14 @@ function resetToDefaults() {
|
||||
'Neutral': '⚖️'
|
||||
},
|
||||
customFields: [
|
||||
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' },
|
||||
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state' }
|
||||
{ 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 POV, up to three sentences long)'
|
||||
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)',
|
||||
persistInHistory: false
|
||||
},
|
||||
characterStats: {
|
||||
enabled: false,
|
||||
@@ -206,6 +349,13 @@ function resetToDefaults() {
|
||||
}
|
||||
}
|
||||
};
|
||||
// Reset history persistence settings
|
||||
extensionSettings.historyPersistence = {
|
||||
enabled: false,
|
||||
messageCount: 5,
|
||||
injectionPosition: 'assistant_message_end',
|
||||
contextPreamble: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,13 +365,15 @@ 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.0',
|
||||
version: '1.1', // Bumped version for historyPersistence support
|
||||
exportDate: new Date().toISOString(),
|
||||
trackerConfig: JSON.parse(JSON.stringify(config)) // Deep copy
|
||||
trackerConfig: JSON.parse(JSON.stringify(config)), // Deep copy
|
||||
historyPersistence: historyPersistence ? JSON.parse(JSON.stringify(historyPersistence)) : null // Include history persistence settings
|
||||
};
|
||||
|
||||
// Convert to JSON
|
||||
@@ -286,9 +438,67 @@ function migrateTrackerPreset(config) {
|
||||
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 any other migration logic here for future format changes
|
||||
// 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;
|
||||
}
|
||||
@@ -323,22 +533,11 @@ function importTrackerPreset() {
|
||||
// Migrate old preset format to current format
|
||||
const migratedConfig = migrateTrackerPreset(data.trackerConfig);
|
||||
|
||||
// Ask for confirmation
|
||||
const confirmMessage = i18n.getTranslation('template.trackerEditorModal.messages.importConfirm') ||
|
||||
'This will replace your current tracker configuration. Continue?';
|
||||
// Extract historyPersistence if present in the import file
|
||||
const historyPersistence = data.historyPersistence || null;
|
||||
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the migrated configuration
|
||||
extensionSettings.trackerConfig = migratedConfig;
|
||||
|
||||
// Re-render the editor UI
|
||||
renderEditorUI();
|
||||
|
||||
// console.log('[RPG Companion] Tracker preset imported successfully');
|
||||
toastr.success(i18n.getTranslation('template.trackerEditorModal.messages.importSuccess') || 'Tracker preset imported successfully!');
|
||||
// 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') ||
|
||||
@@ -350,6 +549,101 @@ function importTrackerPreset() {
|
||||
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 = `
|
||||
<div id="rpg-import-mode-dialog" class="rpg-import-dialog-overlay">
|
||||
<div class="rpg-import-dialog">
|
||||
<h4><i class="fa-solid fa-file-import"></i> Import Configuration</h4>
|
||||
<p>How would you like to import this configuration?</p>
|
||||
<div class="rpg-import-dialog-buttons">
|
||||
<button id="rpg-import-to-current" class="rpg-btn-secondary">
|
||||
<i class="fa-solid fa-arrow-right-to-bracket"></i>
|
||||
Apply to Current Preset
|
||||
</button>
|
||||
<button id="rpg-import-as-new" class="rpg-btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Create New Preset
|
||||
</button>
|
||||
</div>
|
||||
<button id="rpg-import-cancel" class="rpg-btn-cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('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
|
||||
*/
|
||||
@@ -357,6 +651,7 @@ function renderEditorUI() {
|
||||
renderUserStatsTab();
|
||||
renderInfoBoxTab();
|
||||
renderPresentCharactersTab();
|
||||
renderHistoryPersistenceTab();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -783,7 +1078,7 @@ function renderPresentCharactersTab() {
|
||||
html += '</div>';
|
||||
html += '<div class="rpg-editor-input-group">';
|
||||
html += `<label>${i18n.getTranslation('template.trackerEditorModal.presentCharactersTab.aiInstructionLabel')}</label>`;
|
||||
html += `<input type="text" id="rpg-thoughts-description" value="${config.thoughts?.description || 'Internal monologue (in first person POV, up to three sentences long)'}" placeholder="Description of what to generate">`;
|
||||
html += `<input type="text" id="rpg-thoughts-description" value="${config.thoughts?.description || 'Internal Monologue (in first person from character\'s POV, up to three sentences long)'}" placeholder="Description of what to generate">`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -1059,3 +1354,244 @@ function setupPresentCharactersListeners() {
|
||||
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: ''
|
||||
};
|
||||
const userStatsConfig = extensionSettings.trackerConfig.userStats;
|
||||
const infoBoxConfig = extensionSettings.trackerConfig.infoBox;
|
||||
const presentCharsConfig = extensionSettings.trackerConfig.presentCharacters;
|
||||
|
||||
let html = '<div class="rpg-editor-section">';
|
||||
|
||||
// Main toggle and settings
|
||||
html += `<h4><i class="fa-solid fa-clock-rotate-left"></i> History Persistence Settings</h4>`;
|
||||
html += `<p class="rpg-editor-hint">Inject selected tracker data into historical messages to help the AI maintain continuity for time-sensitive events, weather changes, and location tracking.</p>`;
|
||||
|
||||
// Enable toggle
|
||||
html += '<div class="rpg-editor-toggle-row">';
|
||||
html += `<input type="checkbox" id="rpg-history-persistence-enabled" ${historyPersistence.enabled ? 'checked' : ''}>`;
|
||||
html += `<label for="rpg-history-persistence-enabled">Enable History Persistence</label>`;
|
||||
html += '</div>';
|
||||
|
||||
// Message count
|
||||
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||
html += `<label for="rpg-history-message-count">Number of messages to include (0 = all available):</label>`;
|
||||
html += `<input type="number" id="rpg-history-message-count" min="0" max="50" value="${historyPersistence.messageCount}" class="rpg-input" style="width: 80px; margin-left: 8px;">`;
|
||||
html += '</div>';
|
||||
|
||||
// Injection position
|
||||
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||
html += `<label for="rpg-history-injection-position">Injection Position:</label>`;
|
||||
html += `<select id="rpg-history-injection-position" class="rpg-select" style="margin-left: 8px;">`;
|
||||
html += `<option value="user_message_end" ${historyPersistence.injectionPosition === 'user_message_end' ? 'selected' : ''}>End of the User's Message</option>`;
|
||||
html += `<option value="assistant_message_end" ${historyPersistence.injectionPosition === 'assistant_message_end' ? 'selected' : ''}>End of the Assistant's Message</option>`;
|
||||
html += `</select>`;
|
||||
html += '</div>';
|
||||
|
||||
// Custom preamble
|
||||
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||
html += `<label for="rpg-history-context-preamble">Custom Context Preamble:</label>`;
|
||||
html += `<input type="text" id="rpg-history-context-preamble" value="${historyPersistence.contextPreamble || ''}" class="rpg-text-input" placeholder="Context for that moment:" style="width: 100%; margin-top: 4px;">`;
|
||||
html += '</div>';
|
||||
|
||||
// User Stats section - which stats to persist
|
||||
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-heart-pulse"></i> User Stats</h4>`;
|
||||
html += `<p class="rpg-editor-hint">Select which stats should be included in historical messages.</p>`;
|
||||
|
||||
// Custom stats
|
||||
html += '<div class="rpg-history-persist-list">';
|
||||
userStatsConfig.customStats.forEach((stat, index) => {
|
||||
if (stat.enabled) {
|
||||
html += `
|
||||
<div class="rpg-editor-toggle-row">
|
||||
<input type="checkbox" id="rpg-history-stat-${stat.id}" class="rpg-history-stat-toggle" data-index="${index}" ${stat.persistInHistory ? 'checked' : ''}>
|
||||
<label for="rpg-history-stat-${stat.id}">${stat.name}</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Status section
|
||||
if (userStatsConfig.statusSection?.enabled) {
|
||||
html += `
|
||||
<div class="rpg-editor-toggle-row">
|
||||
<input type="checkbox" id="rpg-history-status" ${userStatsConfig.statusSection.persistInHistory ? 'checked' : ''}>
|
||||
<label for="rpg-history-status">Status (Mood/Conditions)</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Skills section
|
||||
if (userStatsConfig.skillsSection?.enabled) {
|
||||
html += `
|
||||
<div class="rpg-editor-toggle-row">
|
||||
<input type="checkbox" id="rpg-history-skills" ${userStatsConfig.skillsSection.persistInHistory ? 'checked' : ''}>
|
||||
<label for="rpg-history-skills">${userStatsConfig.skillsSection.label || 'Skills'}</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Inventory
|
||||
html += `
|
||||
<div class="rpg-editor-toggle-row">
|
||||
<input type="checkbox" id="rpg-history-inventory" ${userStatsConfig.inventoryPersistInHistory ? 'checked' : ''}>
|
||||
<label for="rpg-history-inventory">Inventory</label>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Quests
|
||||
html += `
|
||||
<div class="rpg-editor-toggle-row">
|
||||
<input type="checkbox" id="rpg-history-quests" ${userStatsConfig.questsPersistInHistory ? 'checked' : ''}>
|
||||
<label for="rpg-history-quests">Quests</label>
|
||||
</div>
|
||||
`;
|
||||
html += '</div>';
|
||||
|
||||
// Info Box section - which widgets to persist
|
||||
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-info-circle"></i> Info Box</h4>`;
|
||||
html += `<p class="rpg-editor-hint">Select which info box fields should be included in historical messages. These are recommended for time tracking.</p>`;
|
||||
|
||||
html += '<div class="rpg-history-persist-list">';
|
||||
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 += `
|
||||
<div class="rpg-editor-toggle-row">
|
||||
<input type="checkbox" id="rpg-history-widget-${widgetId}" class="rpg-history-widget-toggle" data-widget="${widgetId}" ${widget.persistInHistory ? 'checked' : ''}>
|
||||
<label for="rpg-history-widget-${widgetId}">${widgetLabels[widgetId] || widgetId}</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Present Characters section
|
||||
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-users"></i> Present Characters</h4>`;
|
||||
html += `<p class="rpg-editor-hint">Select which character fields should be included in historical messages.</p>`;
|
||||
|
||||
html += '<div class="rpg-history-persist-list">';
|
||||
|
||||
// Custom fields (appearance, demeanor, etc.)
|
||||
presentCharsConfig.customFields.forEach((field, index) => {
|
||||
if (field.enabled) {
|
||||
html += `
|
||||
<div class="rpg-editor-toggle-row">
|
||||
<input type="checkbox" id="rpg-history-charfield-${field.id}" class="rpg-history-charfield-toggle" data-index="${index}" ${field.persistInHistory ? 'checked' : ''}>
|
||||
<label for="rpg-history-charfield-${field.id}">${field.name}</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Thoughts
|
||||
if (presentCharsConfig.thoughts?.enabled) {
|
||||
html += `
|
||||
<div class="rpg-editor-toggle-row">
|
||||
<input type="checkbox" id="rpg-history-thoughts" ${presentCharsConfig.thoughts.persistInHistory ? 'checked' : ''}>
|
||||
<label for="rpg-history-thoughts">${presentCharsConfig.thoughts.name || 'Thoughts'}</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
$('#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: ''
|
||||
};
|
||||
}
|
||||
|
||||
// Main toggle
|
||||
$('#rpg-history-persistence-enabled').off('change').on('change', function() {
|
||||
extensionSettings.historyPersistence.enabled = $(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');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3933,12 +3933,75 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
TRACKER EDITOR MODAL
|
||||
============================================ */
|
||||
|
||||
/* Preset Management Section */
|
||||
.rpg-preset-management {
|
||||
padding: 0.75em 1em;
|
||||
background: var(--rpg-accent);
|
||||
border-bottom: 2px solid var(--rpg-border);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rpg-preset-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.rpg-preset-row label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpg-preset-row .rpg-select {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
max-width: 250px;
|
||||
height: 2.2em;
|
||||
padding: 0 0.6em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.rpg-preset-row .rpg-btn-icon {
|
||||
height: 2.2em;
|
||||
width: 2.2em;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rpg-preset-row .rpg-btn-icon.rpg-btn-active {
|
||||
background: var(--rpg-highlight);
|
||||
border-color: var(--rpg-highlight);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rpg-preset-association-row {
|
||||
margin-top: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.rpg-preset-association-row .checkbox_label {
|
||||
margin: 0;
|
||||
font-size: 0.85em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.rpg-preset-association-row .checkbox_label strong {
|
||||
color: var(--rpg-highlight);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Editor tabs */
|
||||
.rpg-editor-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid var(--rpg-border);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.rpg-editor-tab {
|
||||
@@ -5090,6 +5153,21 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* FAB Spinning animation during API requests */
|
||||
.rpg-mobile-toggle.rpg-fab-loading i {
|
||||
animation: fa-spin 1s infinite linear;
|
||||
}
|
||||
|
||||
.rpg-mobile-toggle.rpg-fab-loading {
|
||||
box-shadow: 0 0 12px rgba(233, 69, 96, 0.6), 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Hide FAB widgets when panel is open */
|
||||
body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Mobile overlay backdrop */
|
||||
.rpg-mobile-overlay {
|
||||
display: none;
|
||||
@@ -9693,3 +9771,517 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
|
||||
#send_form {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
IMPORT MODE DIALOG STYLES
|
||||
============================================ */
|
||||
|
||||
/* Import Mode Dialog Overlay */
|
||||
.rpg-import-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.rpg-import-dialog {
|
||||
background: var(--SmartThemeBlurTintColor, #2a2a3e);
|
||||
border: 1px solid var(--SmartThemeBorderColor, #4a4a4a);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.rpg-import-dialog h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--SmartThemeBodyColor, #eee);
|
||||
}
|
||||
|
||||
.rpg-import-dialog h4 i {
|
||||
margin-right: 8px;
|
||||
color: var(--SmartThemeQuoteColor, #e94560);
|
||||
}
|
||||
|
||||
.rpg-import-dialog p {
|
||||
margin: 0 0 20px 0;
|
||||
color: var(--SmartThemeBodyColor, #ccc);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.rpg-import-dialog-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.rpg-import-dialog-buttons button {
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.rpg-import-dialog .rpg-btn-secondary {
|
||||
background: var(--black30a, rgba(0, 0, 0, 0.3));
|
||||
border: 1px solid var(--SmartThemeBorderColor, #4a4a4a);
|
||||
color: var(--SmartThemeBodyColor, #ccc);
|
||||
}
|
||||
|
||||
.rpg-import-dialog .rpg-btn-secondary:hover {
|
||||
background: var(--SmartThemeBorderColor, #4a4a4a);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rpg-import-dialog .rpg-btn-primary {
|
||||
background: var(--SmartThemeQuoteColor, #e94560);
|
||||
border: 1px solid var(--SmartThemeQuoteColor, #e94560);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rpg-import-dialog .rpg-btn-primary:hover {
|
||||
background: var(--SmartThemeQuoteColor, #ff5a75);
|
||||
border-color: var(--SmartThemeQuoteColor, #ff5a75);
|
||||
}
|
||||
|
||||
.rpg-import-dialog .rpg-btn-cancel {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--SmartThemeBodyColor, #888);
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.rpg-import-dialog .rpg-btn-cancel:hover {
|
||||
color: var(--SmartThemeBodyColor, #ccc);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FAB Widget System - Improved Layout
|
||||
============================================ */
|
||||
|
||||
/* Widget container - positioned relative to FAB */
|
||||
.rpg-fab-widget-container {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
}
|
||||
|
||||
/* Hide FAB widgets on desktop viewport */
|
||||
@media (min-width: 1001px) {
|
||||
.rpg-fab-widget-container {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Individual widget base styling */
|
||||
.rpg-fab-widget {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
background: rgba(20, 20, 35, 0.95);
|
||||
border: 1px solid rgba(100, 150, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5), 0 0 1px rgba(100, 150, 255, 0.3);
|
||||
transition: opacity 0.2s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.rpg-fab-widget:hover {
|
||||
border-color: rgba(100, 150, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Expanded state for truncated widgets - desktop hover and mobile tap */
|
||||
.rpg-fab-widget[data-full-text]:hover,
|
||||
.rpg-fab-widget[data-full-text].expanded {
|
||||
z-index: 9999 !important;
|
||||
max-width: none !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpg-fab-widget[data-full-text]:hover .rpg-fab-widget-text,
|
||||
.rpg-fab-widget[data-full-text].expanded .rpg-fab-widget-text {
|
||||
/* Show full text on hover/tap */
|
||||
}
|
||||
|
||||
/* Hide truncated text and show full text on expand */
|
||||
.rpg-fab-widget[data-full-text]:hover .rpg-truncated,
|
||||
.rpg-fab-widget[data-full-text].expanded .rpg-truncated {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rpg-fab-widget[data-full-text]:hover .rpg-full-text,
|
||||
.rpg-fab-widget[data-full-text].expanded .rpg-full-text {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* Default: show truncated, hide full */
|
||||
.rpg-fab-widget .rpg-full-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rpg-fab-widget .rpg-truncated {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* 8-Position system - spread out more to avoid overlap
|
||||
Positions: 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW */
|
||||
|
||||
/* Position 0: North (top center) */
|
||||
.rpg-fab-widget-pos-0 {
|
||||
bottom: calc(100% + 15px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.rpg-fab-widget-pos-0:hover { transform: translateX(-50%) scale(1.05); }
|
||||
|
||||
/* Position 1: Northeast */
|
||||
.rpg-fab-widget-pos-1 {
|
||||
bottom: calc(100% + 10px);
|
||||
left: calc(100% + 15px);
|
||||
}
|
||||
|
||||
/* Position 2: East (right center) */
|
||||
.rpg-fab-widget-pos-2 {
|
||||
top: 50%;
|
||||
left: calc(100% + 15px);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.rpg-fab-widget-pos-2:hover { transform: translateY(-50%) scale(1.05); }
|
||||
|
||||
/* Position 3: Southeast */
|
||||
.rpg-fab-widget-pos-3 {
|
||||
top: calc(100% + 10px);
|
||||
left: calc(100% + 15px);
|
||||
}
|
||||
|
||||
/* Position 4: South (bottom center) */
|
||||
.rpg-fab-widget-pos-4 {
|
||||
top: calc(100% + 15px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.rpg-fab-widget-pos-4:hover { transform: translateX(-50%) scale(1.05); }
|
||||
|
||||
/* Position 5: Southwest */
|
||||
.rpg-fab-widget-pos-5 {
|
||||
top: calc(100% + 10px);
|
||||
right: calc(100% + 15px);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* Position 6: West - Stats (top edge at FAB center + gap, grows DOWN) */
|
||||
.rpg-fab-widget-pos-6 {
|
||||
top: calc(50% + 8px);
|
||||
right: calc(100% + 15px);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* Position 7: Northwest - Attributes (bottom edge at FAB center - gap, grows UP) */
|
||||
.rpg-fab-widget-pos-7 {
|
||||
bottom: calc(50% + 8px);
|
||||
right: calc(100% + 15px);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* Centered large widget (when only one is visible) - vertically centered */
|
||||
.rpg-fab-widget-centered.rpg-fab-widget-pos-6,
|
||||
.rpg-fab-widget-centered.rpg-fab-widget-pos-7 {
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* Weather icon widget - larger emoji display */
|
||||
.rpg-fab-widget-weather-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
padding: 6px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Weather description widget */
|
||||
.rpg-fab-widget-weather-desc {
|
||||
max-width: 100px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Clock/Time widget - bottom position with animated clock */
|
||||
.rpg-fab-widget-clock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-family: 'Roboto Mono', 'Consolas', monospace;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 4px 8px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Mini animated clock face */
|
||||
.rpg-fab-clock-face {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid var(--rpg-border, #4a7ba7);
|
||||
border-radius: 50%;
|
||||
background: var(--rpg-accent, rgba(22, 33, 62, 0.9));
|
||||
}
|
||||
|
||||
.rpg-fab-clock-hour {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 7px;
|
||||
background: var(--rpg-text, #eaeaea);
|
||||
left: 50%;
|
||||
bottom: 50%;
|
||||
margin-left: -1px;
|
||||
transform-origin: bottom center;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.rpg-fab-clock-minute {
|
||||
position: absolute;
|
||||
width: 1.5px;
|
||||
height: 9px;
|
||||
background: var(--rpg-highlight, #4a90e2);
|
||||
left: 50%;
|
||||
bottom: 50%;
|
||||
margin-left: -0.75px;
|
||||
transform-origin: bottom center;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.rpg-fab-clock-center {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: var(--rpg-highlight, #4a90e2);
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.rpg-fab-clock-time {
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Date widget */
|
||||
.rpg-fab-widget-date {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Location widget - two lines */
|
||||
.rpg-fab-widget-location {
|
||||
max-width: 90px;
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Attributes widget - compact grid */
|
||||
.rpg-fab-widget-attributes {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-attr-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-attr-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-attr-name {
|
||||
font-size: 8px;
|
||||
opacity: 0.7;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-attr-value {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--rpg-highlight, #4a90e2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Stats widget - vertical compact list */
|
||||
.rpg-fab-widget-stats {
|
||||
padding: 6px 10px;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-stats-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-stat-item {
|
||||
font-size: 11px;
|
||||
font-family: 'Roboto Mono', 'Consolas', monospace;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* RPG Attributes widget - 2x3 grid */
|
||||
.rpg-fab-widget-attributes {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-attr-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2px 10px;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-attr-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-attr-name {
|
||||
font-size: 8px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.rpg-fab-widget-attr-value {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #6af;
|
||||
font-family: 'Roboto Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
/* FAB Loading State */
|
||||
#rpg-mobile-toggle.rpg-fab-loading {
|
||||
animation: fabSpin 1s linear infinite;
|
||||
}
|
||||
|
||||
#rpg-mobile-toggle.rpg-fab-loading i {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@keyframes fabSpin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FAB WIDGET THEME VARIATIONS
|
||||
============================================ */
|
||||
|
||||
/* Sci-Fi / Synthwave Theme for FAB Widgets */
|
||||
body:has(.rpg-panel[data-theme="sci-fi"]) .rpg-fab-widget {
|
||||
background: rgba(10, 14, 39, 0.95);
|
||||
border: 1px solid rgba(139, 0, 255, 0.5);
|
||||
color: #00fff9;
|
||||
box-shadow: 0 3px 12px rgba(139, 0, 255, 0.3), 0 0 8px rgba(255, 0, 110, 0.2);
|
||||
}
|
||||
|
||||
body:has(.rpg-panel[data-theme="sci-fi"]) .rpg-fab-widget:hover {
|
||||
border-color: rgba(255, 0, 110, 0.7);
|
||||
box-shadow: 0 4px 16px rgba(139, 0, 255, 0.5), 0 0 12px rgba(255, 0, 110, 0.4);
|
||||
}
|
||||
|
||||
/* Fantasy / Rustic Theme for FAB Widgets */
|
||||
body:has(.rpg-panel[data-theme="fantasy"]) .rpg-fab-widget {
|
||||
background: rgba(43, 24, 16, 0.95);
|
||||
border: 1px solid rgba(139, 105, 20, 0.6);
|
||||
color: #f4e8d0;
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.6), 0 0 1px rgba(212, 175, 55, 0.3);
|
||||
}
|
||||
|
||||
body:has(.rpg-panel[data-theme="fantasy"]) .rpg-fab-widget:hover {
|
||||
border-color: rgba(212, 175, 55, 0.8);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.7), 0 0 8px rgba(212, 175, 55, 0.4);
|
||||
}
|
||||
|
||||
/* Cyberpunk Theme for FAB Widgets */
|
||||
body:has(.rpg-panel[data-theme="cyberpunk"]) .rpg-fab-widget {
|
||||
background: rgba(15, 5, 25, 0.95);
|
||||
border: 1px solid rgba(255, 0, 255, 0.4);
|
||||
color: #00ffff;
|
||||
box-shadow: 0 3px 12px rgba(255, 0, 255, 0.3), 0 0 8px rgba(0, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
body:has(.rpg-panel[data-theme="cyberpunk"]) .rpg-fab-widget:hover {
|
||||
border-color: rgba(0, 255, 255, 0.7);
|
||||
box-shadow: 0 4px 16px rgba(255, 0, 255, 0.5), 0 0 12px rgba(0, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Minimal Theme for FAB Widgets */
|
||||
body:has(.rpg-panel[data-theme="minimal"]) .rpg-fab-widget {
|
||||
background: rgba(245, 245, 250, 0.98);
|
||||
border: 1px solid rgba(200, 200, 210, 0.8);
|
||||
color: #2c3e50;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body:has(.rpg-panel[data-theme="minimal"]) .rpg-fab-widget:hover {
|
||||
border-color: rgba(52, 152, 219, 0.6);
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Dark Theme for FAB Widgets */
|
||||
body:has(.rpg-panel[data-theme="dark"]) .rpg-fab-widget {
|
||||
background: rgba(25, 25, 35, 0.95);
|
||||
border: 1px solid rgba(70, 70, 90, 0.6);
|
||||
color: #e0e0e0;
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
body:has(.rpg-panel[data-theme="dark"]) .rpg-fab-widget:hover {
|
||||
border-color: rgba(100, 100, 130, 0.8);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Light Theme for FAB Widgets */
|
||||
body:has(.rpg-panel[data-theme="light"]) .rpg-fab-widget {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border: 1px solid rgba(220, 220, 230, 0.8);
|
||||
color: #2c3e50;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body:has(.rpg-panel[data-theme="light"]) .rpg-fab-widget:hover {
|
||||
border-color: rgba(52, 152, 219, 0.6);
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
+151
-14
@@ -90,6 +90,24 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Deception System Toggle -->
|
||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-deception-toggle-wrapper">
|
||||
<label class="rpg-toggle-label" title="Deception System">
|
||||
<input type="checkbox" id="rpg-toggle-deception">
|
||||
<i class="fa-solid fa-masks-theater"></i>
|
||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.deceptionSystem">Deception System</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CYOA Toggle -->
|
||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-cyoa-toggle-wrapper">
|
||||
<label class="rpg-toggle-label" title="CYOA">
|
||||
<input type="checkbox" id="rpg-toggle-cyoa">
|
||||
<i class="fa-solid fa-list-ol"></i>
|
||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.cyoa">CYOA</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Spotify Music Toggle -->
|
||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-spotify-toggle-wrapper">
|
||||
<label class="rpg-toggle-label" title="Spotify Music">
|
||||
@@ -321,6 +339,24 @@
|
||||
Display a toggle button to enable/disable colored dialogue formatting.
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-show-deception-toggle" />
|
||||
<span data-i18n-key="template.settingsModal.display.showDeceptionToggle">Show Deception System</span>
|
||||
</label>
|
||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||
data-i18n-key="template.settingsModal.display.showDeceptionToggleNote">
|
||||
Display a toggle button to enable/disable special formatting of lies and deceptions crafted by the model, allowing it to easily track whenever one was committed, without showing it to the user.
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-show-cyoa-toggle" />
|
||||
<span data-i18n-key="template.settingsModal.display.showCYOAToggle">Show CYOA</span>
|
||||
</label>
|
||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||
data-i18n-key="template.settingsModal.display.showCYOAToggleNote">
|
||||
Display a toggle button to enable/disable "Choose Your Own Adventure" formatting instruction that makes the model produce five possible actions/dialogues for you to choose from at the end of the output.
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-show-spotify-toggle" />
|
||||
<span data-i18n-key="template.settingsModal.display.showSpotifyMusicToggle">Show Spotify Music</span>
|
||||
@@ -395,6 +431,61 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Mobile FAB Options Section -->
|
||||
<div class="rpg-settings-group">
|
||||
<h4 data-i18n-key="template.settingsModal.mobileFabTitle">Mobile Button Widgets</h4>
|
||||
<small class="notes" style="display: block; margin-bottom: 10px;"
|
||||
data-i18n-key="template.settingsModal.mobileFabNote">
|
||||
Show compact info widgets around the floating button on mobile. Widgets are positioned automatically.
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-fab-widgets-enabled" />
|
||||
<span data-i18n-key="template.settingsModal.mobileFab.enabled">Enable Floating Mobile Widgets</span>
|
||||
</label>
|
||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||
data-i18n-key="template.settingsModal.mobileFab.enabledNote">
|
||||
Master toggle to show info widgets around the mobile floating button.
|
||||
</small>
|
||||
|
||||
<div id="rpg-fab-widget-options" style="margin-left: 10px; border-left: 2px solid var(--SmartThemeBorderColor); padding-left: 10px; margin-top: 8px;">
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-fab-weather-icon" />
|
||||
<span data-i18n-key="template.settingsModal.mobileFab.weatherIcon">Weather Icon</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-fab-weather-desc" />
|
||||
<span data-i18n-key="template.settingsModal.mobileFab.weatherDesc">Weather Description</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-fab-clock" />
|
||||
<span data-i18n-key="template.settingsModal.mobileFab.clock">Time/Clock</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-fab-date" />
|
||||
<span data-i18n-key="template.settingsModal.mobileFab.date">Date</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-fab-location" />
|
||||
<span data-i18n-key="template.settingsModal.mobileFab.location">Location</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-fab-stats" />
|
||||
<span data-i18n-key="template.settingsModal.mobileFab.stats">Stats (Health, Energy, etc.)</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-fab-attributes" />
|
||||
<span data-i18n-key="template.settingsModal.mobileFab.attributes">RPG Attributes (STR, DEX, etc.)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpg-settings-group">
|
||||
<h4 data-i18n-key="template.settingsModal.advancedTitle"><i class="fa-solid fa-sliders"
|
||||
aria-hidden="true"></i> Advanced</h4>
|
||||
@@ -506,19 +597,7 @@
|
||||
Automatically refresh RPG info after each message.
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label" style="margin-top: 16px;">
|
||||
<input type="checkbox" id="rpg-save-tracker-history" />
|
||||
<span data-i18n-key="template.settingsModal.advanced.saveTrackerHistory">Save Tracker History in
|
||||
Chat</span>
|
||||
</label>
|
||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||
data-i18n-key="template.settingsModal.advanced.saveTrackerHistoryNote">
|
||||
When enabled, tracker data is saved in chat history for each message. In Together mode, trackers
|
||||
appear in <trackers> XML tags (hidden from display). In Separate mode, tracker data is stored
|
||||
in message metadata. When disabled, only the most recent trackers are kept.
|
||||
</small>
|
||||
|
||||
<div class="rpg-setting-row">
|
||||
<div class="rpg-setting-row" style="margin-top: 16px;">
|
||||
<label for="rpg-encounter-history-depth" data-i18n-key="template.settingsModal.advanced.encounterHistoryDepth">Chat History Depth For Encounters:</label>
|
||||
<input type="number" id="rpg-encounter-history-depth" min="1" max="20" value="8"
|
||||
class="rpg-input" />
|
||||
@@ -588,7 +667,7 @@
|
||||
</button>
|
||||
<small style="display: block; margin-top: 8px; color: #888; font-size: 11px;"
|
||||
data-i18n-key="template.settingsModal.advanced.clearCacheNote">
|
||||
Clears all cached data including tracker history and temporary files.
|
||||
Clears committed and displayed tracker data for your currently active chat.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -670,6 +749,31 @@
|
||||
aria-label="Close tracker editor">×</button>
|
||||
</header>
|
||||
|
||||
<!-- Preset Management Section -->
|
||||
<div class="rpg-preset-management">
|
||||
<div class="rpg-preset-row">
|
||||
<label for="rpg-preset-select">Preset:</label>
|
||||
<select id="rpg-preset-select" class="rpg-select">
|
||||
<!-- Options populated by JavaScript -->
|
||||
</select>
|
||||
<button id="rpg-preset-new" class="rpg-btn-icon" type="button" title="Create New Preset">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
<button id="rpg-preset-default" class="rpg-btn-icon" type="button" title="Set as Default Preset">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
</button>
|
||||
<button id="rpg-preset-delete" class="rpg-btn-icon" type="button" title="Delete Current Preset">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-preset-association-row">
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-preset-associate">
|
||||
<span>Use this preset for: <strong id="rpg-preset-entity-name">Character</strong></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="rpg-editor-tabs">
|
||||
<button class="rpg-editor-tab active" data-tab="userStats">
|
||||
@@ -684,6 +788,10 @@
|
||||
<i class="fa-solid fa-users"></i> <span
|
||||
data-i18n-key="template.trackerEditorModal.tabs.presentCharacters">Present Characters</span>
|
||||
</button>
|
||||
<button class="rpg-editor-tab" data-tab="historyPersistence">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i> <span
|
||||
data-i18n-key="template.trackerEditorModal.tabs.historyPersistence">History Persistence</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rpg-settings-popup-body">
|
||||
@@ -691,6 +799,7 @@
|
||||
<div id="rpg-editor-tab-userStats" class="rpg-editor-tab-content"></div>
|
||||
<div id="rpg-editor-tab-infoBox" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||
<div id="rpg-editor-tab-presentCharacters" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||
<div id="rpg-editor-tab-historyPersistence" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<footer class="rpg-settings-popup-footer">
|
||||
@@ -764,6 +873,34 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Deception System Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-deception" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
<i class="fa-solid fa-masks-theater"></i> Deception System Prompt
|
||||
</label>
|
||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
||||
Injected when "Enable Deception System" is enabled. Instructs AI to mark lies and deceptions with hidden tags.
|
||||
</small>
|
||||
<textarea id="rpg-prompt-deception" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="deception" style="margin-top: 8px;">
|
||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- CYOA Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-cyoa" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
<i class="fa-solid fa-list-ol"></i> CYOA Prompt
|
||||
</label>
|
||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
||||
Injected when "Enable CYOA" is enabled. Instructs AI to end responses with numbered action choices. Uses very high priority (depth 102) to ensure it's the last instruction.
|
||||
</small>
|
||||
<textarea id="rpg-prompt-cyoa" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="cyoa" style="margin-top: 8px;">
|
||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Spotify Music Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-spotify" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
|
||||
Reference in New Issue
Block a user