482 lines
19 KiB
JavaScript
482 lines
19 KiB
JavaScript
/**
|
|
* Core State Management Module
|
|
* Centralizes all extension state variables
|
|
*/
|
|
|
|
// Type imports
|
|
/** @typedef {import('../types/inventory.js').InventoryV2} InventoryV2 */
|
|
|
|
/**
|
|
* Extension settings - persisted to SillyTavern settings
|
|
*/
|
|
export let extensionSettings = {
|
|
settingsVersion: 3, // Version number for settings migrations (v3 = JSON format)
|
|
enabled: true,
|
|
autoUpdate: false,
|
|
updateDepth: 4, // How many messages to include in the context
|
|
generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately
|
|
showUserStats: true,
|
|
showInfoBox: true,
|
|
showCharacterThoughts: true,
|
|
showInventory: true, // Show inventory section (v2 system)
|
|
showQuests: true, // Show quests section
|
|
showThoughtsInChat: true, // Show thoughts overlay in chat
|
|
narratorMode: false, // Use character card as narrator instead of fixed character references
|
|
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
|
|
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
|
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
|
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
|
|
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
|
|
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
|
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
|
|
|
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
|
|
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)
|
|
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
|
|
|
|
showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel
|
|
showNarratorMode: true, // Show Narrator Mode toggle in main panel
|
|
showAutoAvatars: true, // Show Auto-generate Avatars toggle in main panel
|
|
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: {
|
|
bg: '#1a1a2e',
|
|
accent: '#16213e',
|
|
text: '#eaeaea',
|
|
highlight: '#e94560'
|
|
},
|
|
statBarColorLow: '#cc3333', // Color for low stat values (red)
|
|
statBarColorHigh: '#33cc66', // Color for high stat values (green)
|
|
enableAnimations: true, // Enable smooth animations for stats and content updates
|
|
mobileFabPosition: {
|
|
top: 'calc(var(--topBarBlockSize) + 60px)',
|
|
right: '12px'
|
|
}, // Saved position for mobile FAB button
|
|
userStats: JSON.stringify({
|
|
stats: [
|
|
{ id: 'health', name: 'Health', value: 100 },
|
|
{ id: 'satiety', name: 'Satiety', value: 100 },
|
|
{ id: 'energy', name: 'Energy', value: 100 },
|
|
{ id: 'hygiene', name: 'Hygiene', value: 100 },
|
|
{ id: 'arousal', name: 'Arousal', value: 0 }
|
|
],
|
|
status: {
|
|
mood: '😐',
|
|
conditions: 'None'
|
|
},
|
|
inventory: {
|
|
onPerson: [],
|
|
stored: []
|
|
},
|
|
quests: {
|
|
active: [],
|
|
completed: []
|
|
}
|
|
}, null, 2),
|
|
statNames: {
|
|
health: 'Health',
|
|
satiety: 'Satiety',
|
|
energy: 'Energy',
|
|
hygiene: 'Hygiene',
|
|
arousal: 'Arousal'
|
|
},
|
|
// Tracker customization configuration
|
|
trackerConfig: {
|
|
userStats: {
|
|
// Array of custom stats (allows add/remove/rename)
|
|
customStats: [
|
|
{ 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, 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
|
|
persistInHistory: false // Persist status in historical messages
|
|
},
|
|
// Optional skills field
|
|
skillsSection: {
|
|
enabled: false,
|
|
label: 'Skills', // User-editable
|
|
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', 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: {
|
|
// Fixed fields (always shown)
|
|
showEmoji: true,
|
|
showName: true,
|
|
// Relationship fields configuration
|
|
relationships: {
|
|
enabled: true,
|
|
// Relationship to emoji mapping (shown on character portraits)
|
|
relationshipEmojis: {
|
|
'Lover': '❤️',
|
|
'Friend': '⭐',
|
|
'Ally': '🤝',
|
|
'Enemy': '⚔️',
|
|
'Neutral': '⚖️'
|
|
}
|
|
},
|
|
// Legacy fields kept for backward compatibility
|
|
relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'],
|
|
relationshipEmojis: {
|
|
'Lover': '❤️',
|
|
'Friend': '⭐',
|
|
'Ally': '🤝',
|
|
'Enemy': '⚔️',
|
|
'Neutral': '⚖️'
|
|
},
|
|
// 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)', 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 from character\'s POV, up to three sentences long)',
|
|
persistInHistory: false
|
|
},
|
|
// Character stats toggle (optional feature)
|
|
characterStats: {
|
|
enabled: false,
|
|
customStats: [
|
|
{ id: 'health', name: 'Health', enabled: true },
|
|
{ id: 'arousal', name: 'Arousal', enabled: true }
|
|
]
|
|
}
|
|
}
|
|
},
|
|
quests: {
|
|
main: "None", // Current main quest title
|
|
optional: [] // Array of optional quest titles
|
|
},
|
|
infoBox: JSON.stringify({
|
|
date: { value: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) },
|
|
weather: { emoji: '☀️', forecast: 'Clear skies' },
|
|
temperature: { value: 20, unit: 'C' },
|
|
time: { start: '00:00', end: '00:00' },
|
|
location: { value: 'Unknown Location' }
|
|
}, null, 2),
|
|
characterThoughts: JSON.stringify({
|
|
characters: []
|
|
}, null, 2),
|
|
level: 1, // User's character level
|
|
classicStats: {
|
|
str: 10,
|
|
dex: 10,
|
|
con: 10,
|
|
int: 10,
|
|
wis: 10,
|
|
cha: 10
|
|
},
|
|
lastDiceRoll: null, // Store last dice roll result
|
|
showDiceDisplay: true, // Show the "Last Roll" display in the panel
|
|
collapsedInventoryLocations: [], // Array of collapsed storage location names
|
|
inventoryViewModes: {
|
|
onPerson: 'list', // 'list' or 'grid' view mode for On Person section
|
|
stored: 'list', // 'list' or 'grid' view mode for Stored section
|
|
assets: 'list' // 'list' or 'grid' view mode for Assets section
|
|
},
|
|
npcAvatars: {}, // Store custom avatar images for NPCs (key: character name, value: base64 data URI)
|
|
// Combat encounter settings
|
|
encounterSettings: {
|
|
enabled: true, // Show Start Encounter button above chat input
|
|
historyDepth: 8, // Number of recent messages to include in combat initialization
|
|
autoSaveLogs: false // Save detailed combat logs to file
|
|
},
|
|
// Auto avatar generation settings
|
|
autoGenerateAvatars: true, // Master toggle for auto-generating avatars
|
|
avatarLLMCustomInstruction: '', // Custom instruction for LLM prompt generation
|
|
// External API settings for 'external' generation mode
|
|
externalApiSettings: {
|
|
baseUrl: '', // OpenAI-compatible API base URL (e.g., "https://api.openai.com/v1")
|
|
// apiKey is NOT stored here for security. It is stored in localStorage('rpg_companion_api_key')
|
|
model: '', // Model identifier (e.g., "gpt-4o-mini")
|
|
maxTokens: 8192, // Maximum tokens for generation
|
|
temperature: 0.7 // Temperature setting for generation
|
|
},
|
|
// Lock state for tracker items (v3 JSON format feature)
|
|
lockedItems: {
|
|
stats: [], // Array of locked stat IDs (e.g., ["health", "satiety"])
|
|
skills: [], // Array of locked skill names (e.g., ["Cooking", "Swordsmanship"])
|
|
inventory: {
|
|
onPerson: [], // Array of locked item indices (e.g., [0, 2])
|
|
clothing: [], // Array of locked item indices
|
|
stored: {}, // Object with location keys, each containing array of locked indices (e.g., {"Home": [0, 1]})
|
|
assets: [] // Array of locked asset indices
|
|
},
|
|
quests: {
|
|
main: false, // Boolean for main quest lock
|
|
optional: [] // Array of locked optional quest indices (e.g., [0, 2])
|
|
},
|
|
infoBox: {
|
|
date: false, // Boolean for date widget lock
|
|
weather: false, // Boolean for weather widget lock
|
|
temperature: false, // Boolean for temperature widget lock
|
|
time: false, // Boolean for time widget lock
|
|
location: false, // Boolean for location widget lock
|
|
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
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Last generated data from AI response
|
|
*/
|
|
export let lastGeneratedData = {
|
|
userStats: null,
|
|
infoBox: null,
|
|
characterThoughts: null,
|
|
html: null
|
|
};
|
|
|
|
/**
|
|
* Tracks the "committed" tracker data that should be used as source for next generation
|
|
* This gets updated when user sends a new message or first time generation
|
|
*/
|
|
export let committedTrackerData = {
|
|
userStats: null,
|
|
infoBox: null,
|
|
characterThoughts: null
|
|
};
|
|
|
|
/**
|
|
* Session-only storage for LLM-generated avatar prompts
|
|
* Maps character names to their generated prompts
|
|
* Resets on new chat (not persisted to extensionSettings)
|
|
*/
|
|
export let sessionAvatarPrompts = {};
|
|
|
|
export function setSessionAvatarPrompt(characterName, prompt) {
|
|
sessionAvatarPrompts[characterName] = prompt;
|
|
}
|
|
|
|
export function getSessionAvatarPrompt(characterName) {
|
|
return sessionAvatarPrompts[characterName] || null;
|
|
}
|
|
|
|
export function clearSessionAvatarPrompts() {
|
|
sessionAvatarPrompts = {};
|
|
}
|
|
|
|
/**
|
|
* Tracks whether the last action was a swipe (for separate mode)
|
|
* Used to determine whether to commit lastGeneratedData to committedTrackerData
|
|
*/
|
|
export let lastActionWasSwipe = false;
|
|
|
|
/**
|
|
* Flag indicating if generation is in progress
|
|
*/
|
|
export let isGenerating = false;
|
|
|
|
/**
|
|
* Tracks if we're currently doing a plot progression
|
|
*/
|
|
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")
|
|
*/
|
|
export let pendingDiceRoll = null;
|
|
|
|
/**
|
|
* Debug logs array for troubleshooting
|
|
*/
|
|
export let debugLogs = [];
|
|
|
|
/**
|
|
* Add a debug log entry
|
|
* @param {string} message - The log message
|
|
* @param {any} data - Optional data to log
|
|
*/
|
|
export function addDebugLog(message, data = null) {
|
|
const timestamp = new Date().toISOString();
|
|
debugLogs.push({ timestamp, message, data });
|
|
// Keep only last 100 logs
|
|
if (debugLogs.length > 100) {
|
|
debugLogs.shift();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Feature flags for gradual rollout of new features
|
|
*/
|
|
export const FEATURE_FLAGS = {
|
|
useNewInventory: true // Enable v2 inventory system with categorized storage
|
|
};
|
|
|
|
/**
|
|
* Fallback avatar image (base64-encoded SVG with "?" icon)
|
|
* Using base64 to avoid quote-encoding issues in HTML attributes
|
|
*/
|
|
export const FALLBACK_AVATAR_DATA_URI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjY2NjYyIgb3BhY2l0eT0iMC4zIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2IiBmb250LXNpemU9IjQwIj4/PC90ZXh0Pjwvc3ZnPg==';
|
|
|
|
/**
|
|
* UI Element References (jQuery objects)
|
|
*/
|
|
export let $panelContainer = null;
|
|
export let $userStatsContainer = null;
|
|
export let $infoBoxContainer = null;
|
|
export let $thoughtsContainer = null;
|
|
export let $inventoryContainer = null;
|
|
export let $questsContainer = null;
|
|
export let $musicPlayerContainer = null;
|
|
|
|
/**
|
|
* State setters - provide controlled mutation of state variables
|
|
*/
|
|
export function setExtensionSettings(newSettings) {
|
|
extensionSettings = newSettings;
|
|
}
|
|
|
|
export function updateExtensionSettings(updates) {
|
|
Object.assign(extensionSettings, updates);
|
|
}
|
|
|
|
export function setLastGeneratedData(data) {
|
|
lastGeneratedData = data;
|
|
}
|
|
|
|
export function updateLastGeneratedData(updates) {
|
|
Object.assign(lastGeneratedData, updates);
|
|
}
|
|
|
|
export function setCommittedTrackerData(data) {
|
|
// console.log('[RPG State] setCommittedTrackerData called with:', data);
|
|
// console.log('[RPG State] Type check on input:', {
|
|
// userStatsType: typeof data.userStats,
|
|
// infoBoxType: typeof data.infoBox,
|
|
// characterThoughtsType: typeof data.characterThoughts,
|
|
// userStatsValue: data.userStats,
|
|
// infoBoxValue: data.infoBox,
|
|
// characterThoughtsValue: data.characterThoughts
|
|
// });
|
|
committedTrackerData = data;
|
|
// console.log('[RPG State] committedTrackerData after assignment:', committedTrackerData);
|
|
}
|
|
|
|
export function updateCommittedTrackerData(updates) {
|
|
// console.log('[RPG State] updateCommittedTrackerData called with:', updates);
|
|
// console.log('[RPG State] committedTrackerData before update:', committedTrackerData);
|
|
Object.assign(committedTrackerData, updates);
|
|
// console.log('[RPG State] committedTrackerData after update:', committedTrackerData);
|
|
}
|
|
|
|
export function setLastActionWasSwipe(value) {
|
|
lastActionWasSwipe = value;
|
|
}
|
|
|
|
export function setIsGenerating(value) {
|
|
isGenerating = value;
|
|
}
|
|
|
|
export function setIsPlotProgression(value) {
|
|
isPlotProgression = value;
|
|
}
|
|
|
|
export function setIsAwaitingNewMessage(value) {
|
|
isAwaitingNewMessage = value;
|
|
}
|
|
|
|
export function setPendingDiceRoll(roll) {
|
|
pendingDiceRoll = roll;
|
|
}
|
|
|
|
export function getPendingDiceRoll() {
|
|
return pendingDiceRoll;
|
|
}
|
|
|
|
export function setPanelContainer($element) {
|
|
$panelContainer = $element;
|
|
}
|
|
|
|
export function setUserStatsContainer($element) {
|
|
$userStatsContainer = $element;
|
|
}
|
|
|
|
export function setInfoBoxContainer($element) {
|
|
$infoBoxContainer = $element;
|
|
}
|
|
|
|
export function setThoughtsContainer($element) {
|
|
$thoughtsContainer = $element;
|
|
}
|
|
|
|
export function setInventoryContainer($element) {
|
|
$inventoryContainer = $element;
|
|
}
|
|
|
|
export function setQuestsContainer($element) {
|
|
$questsContainer = $element;
|
|
}
|
|
|
|
export function setMusicPlayerContainer($element) {
|
|
$musicPlayerContainer = $element;
|
|
}
|