ba50fc5bdc
Extract plot progression button setup from index.js into dedicated feature module at src/systems/features/plotProgression.js. Due to ES6 module import path limitations in deeply nested modules, the generation logic (sendPlotProgression) remains in index.js where it can properly import from SillyTavern's script.js. The UI setup function accepts the generation function as a callback parameter. Creates plotProgression.js with 62 lines of UI setup code. Index.js increases from 800 to 869 lines (+69 for generation logic).
870 lines
36 KiB
JavaScript
870 lines
36 KiB
JavaScript
import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js';
|
|
import { eventSource, event_types, substituteParams, chat, generateRaw, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate } from '../../../../script.js';
|
|
import { selected_group, getGroupMembers } from '../../../group-chats.js';
|
|
import { power_user } from '../../../power-user.js';
|
|
|
|
// Core modules
|
|
import { extensionName, extensionFolderPath } from './src/core/config.js';
|
|
import {
|
|
extensionSettings,
|
|
lastGeneratedData,
|
|
committedTrackerData,
|
|
lastActionWasSwipe,
|
|
isGenerating,
|
|
isPlotProgression,
|
|
pendingDiceRoll,
|
|
FALLBACK_AVATAR_DATA_URI,
|
|
$panelContainer,
|
|
$userStatsContainer,
|
|
$infoBoxContainer,
|
|
$thoughtsContainer,
|
|
setExtensionSettings,
|
|
updateExtensionSettings,
|
|
setLastGeneratedData,
|
|
updateLastGeneratedData,
|
|
setCommittedTrackerData,
|
|
updateCommittedTrackerData,
|
|
setLastActionWasSwipe,
|
|
setIsGenerating,
|
|
setIsPlotProgression,
|
|
setPendingDiceRoll,
|
|
setPanelContainer,
|
|
setUserStatsContainer,
|
|
setInfoBoxContainer,
|
|
setThoughtsContainer
|
|
} from './src/core/state.js';
|
|
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js';
|
|
import { on as eventOn, event_types as coreEventTypes } from './src/core/events.js';
|
|
|
|
// Generation & Parsing modules
|
|
import {
|
|
generateTrackerExample,
|
|
generateTrackerInstructions,
|
|
generateContextualSummary,
|
|
generateRPGPromptText,
|
|
generateSeparateUpdatePrompt
|
|
} from './src/systems/generation/promptBuilder.js';
|
|
import { parseResponse, parseUserStats } from './src/systems/generation/parser.js';
|
|
import { updateRPGData } from './src/systems/generation/apiClient.js';
|
|
import { onGenerationStarted } from './src/systems/generation/injector.js';
|
|
|
|
// Rendering modules
|
|
import { getSafeThumbnailUrl } from './src/utils/avatars.js';
|
|
import { renderUserStats } from './src/systems/rendering/userStats.js';
|
|
import { renderInfoBox, updateInfoBoxField } from './src/systems/rendering/infoBox.js';
|
|
import {
|
|
renderThoughts,
|
|
updateCharacterField,
|
|
updateChatThoughts,
|
|
createThoughtPanel
|
|
} from './src/systems/rendering/thoughts.js';
|
|
|
|
// UI Systems modules
|
|
import {
|
|
applyTheme,
|
|
applyCustomTheme,
|
|
toggleCustomColors,
|
|
toggleAnimations,
|
|
updateSettingsPopupTheme,
|
|
applyCustomThemeToSettingsPopup
|
|
} from './src/systems/ui/theme.js';
|
|
import {
|
|
DiceModal,
|
|
SettingsModal,
|
|
setupDiceRoller,
|
|
setupSettingsPopup,
|
|
updateDiceDisplay,
|
|
addDiceQuickReply,
|
|
getSettingsModal
|
|
} from './src/systems/ui/modals.js';
|
|
import {
|
|
togglePlotButtons,
|
|
updateCollapseToggleIcon,
|
|
setupCollapseToggle,
|
|
updatePanelVisibility,
|
|
updateSectionVisibility,
|
|
applyPanelPosition
|
|
} from './src/systems/ui/layout.js';
|
|
import {
|
|
setupMobileToggle,
|
|
constrainFabToViewport,
|
|
setupMobileTabs,
|
|
removeMobileTabs,
|
|
setupMobileKeyboardHandling,
|
|
setupContentEditableScrolling
|
|
} from './src/systems/ui/mobile.js';
|
|
|
|
// Feature modules
|
|
import { setupPlotButtons } from './src/systems/features/plotProgression.js';
|
|
|
|
// Old state variable declarations removed - now imported from core modules
|
|
// (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js)
|
|
|
|
// Utility functions removed - now imported from src/utils/avatars.js
|
|
// (getSafeThumbnailUrl)
|
|
|
|
// Persistence functions removed - now imported from src/core/persistence.js
|
|
// (loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData)
|
|
|
|
// Theme functions removed - now imported from src/systems/ui/theme.js
|
|
// (applyTheme, applyCustomTheme, toggleCustomColors, toggleAnimations,
|
|
// updateSettingsPopupTheme, applyCustomThemeToSettingsPopup)
|
|
|
|
// Layout functions removed - now imported from src/systems/ui/layout.js
|
|
// (togglePlotButtons, updateCollapseToggleIcon, setupCollapseToggle,
|
|
// updatePanelVisibility, updateSectionVisibility, applyPanelPosition)
|
|
// Note: closeMobilePanelWithAnimation is only used internally by mobile.js
|
|
|
|
// Mobile UI functions removed - now imported from src/systems/ui/mobile.js
|
|
// (setupMobileToggle, constrainFabToViewport, setupMobileTabs, removeMobileTabs,
|
|
// setupMobileKeyboardHandling, setupContentEditableScrolling)
|
|
|
|
/**
|
|
* Adds the extension settings to the Extensions tab.
|
|
*/
|
|
async function addExtensionSettings() {
|
|
const settingsHtml = await renderExtensionTemplateAsync(extensionName, 'settings');
|
|
$('#rpg_companion_container').append(settingsHtml);
|
|
|
|
// Set up the enable/disable toggle
|
|
$('#rpg-extension-enabled').prop('checked', extensionSettings.enabled).on('change', function() {
|
|
extensionSettings.enabled = $(this).prop('checked');
|
|
saveSettings();
|
|
updatePanelVisibility();
|
|
|
|
if (!extensionSettings.enabled) {
|
|
// Clear extension prompts and thought bubbles when disabled
|
|
clearExtensionPrompts();
|
|
updateChatThoughts(); // This will remove the thought bubble since extension is disabled
|
|
} else {
|
|
// Re-create thought bubbles when re-enabled
|
|
updateChatThoughts(); // This will re-create the thought bubble if data exists
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initializes the UI for the extension.
|
|
*/
|
|
async function initUI() {
|
|
// Load the HTML template using SillyTavern's template system
|
|
const templateHtml = await renderExtensionTemplateAsync(extensionName, 'template');
|
|
|
|
// Append panel to body - positioning handled by CSS
|
|
$('body').append(templateHtml);
|
|
|
|
// Add mobile toggle button (FAB - Floating Action Button)
|
|
const mobileToggleHtml = `
|
|
<button id="rpg-mobile-toggle" class="rpg-mobile-toggle" title="Toggle RPG Panel">
|
|
<i class="fa-solid fa-dice-d20"></i>
|
|
</button>
|
|
`;
|
|
$('body').append(mobileToggleHtml);
|
|
|
|
// Cache UI elements using state setters
|
|
setPanelContainer($('#rpg-companion-panel'));
|
|
setUserStatsContainer($('#rpg-user-stats'));
|
|
setInfoBoxContainer($('#rpg-info-box'));
|
|
setThoughtsContainer($('#rpg-thoughts'));
|
|
|
|
// Set up event listeners (enable/disable is handled in Extensions tab)
|
|
$('#rpg-toggle-auto-update').on('change', function() {
|
|
extensionSettings.autoUpdate = $(this).prop('checked');
|
|
saveSettings();
|
|
});
|
|
|
|
$('#rpg-position-select').on('change', function() {
|
|
extensionSettings.panelPosition = String($(this).val());
|
|
saveSettings();
|
|
applyPanelPosition();
|
|
// Recreate thought bubbles to update their position
|
|
updateChatThoughts();
|
|
});
|
|
|
|
$('#rpg-update-depth').on('change', function() {
|
|
const value = $(this).val();
|
|
extensionSettings.updateDepth = parseInt(String(value));
|
|
saveSettings();
|
|
});
|
|
|
|
$('#rpg-generation-mode').on('change', function() {
|
|
extensionSettings.generationMode = String($(this).val());
|
|
saveSettings();
|
|
updateGenerationModeUI();
|
|
});
|
|
|
|
$('#rpg-toggle-user-stats').on('change', function() {
|
|
extensionSettings.showUserStats = $(this).prop('checked');
|
|
saveSettings();
|
|
updateSectionVisibility();
|
|
});
|
|
|
|
$('#rpg-toggle-info-box').on('change', function() {
|
|
extensionSettings.showInfoBox = $(this).prop('checked');
|
|
saveSettings();
|
|
updateSectionVisibility();
|
|
});
|
|
|
|
$('#rpg-toggle-thoughts').on('change', function() {
|
|
extensionSettings.showCharacterThoughts = $(this).prop('checked');
|
|
saveSettings();
|
|
updateSectionVisibility();
|
|
});
|
|
|
|
$('#rpg-toggle-thoughts-in-chat').on('change', function() {
|
|
extensionSettings.showThoughtsInChat = $(this).prop('checked');
|
|
// console.log('[RPG Companion] Toggle showThoughtsInChat changed to:', extensionSettings.showThoughtsInChat);
|
|
saveSettings();
|
|
updateChatThoughts();
|
|
});
|
|
|
|
$('#rpg-toggle-html-prompt').on('change', function() {
|
|
extensionSettings.enableHtmlPrompt = $(this).prop('checked');
|
|
// console.log('[RPG Companion] Toggle enableHtmlPrompt changed to:', extensionSettings.enableHtmlPrompt);
|
|
saveSettings();
|
|
});
|
|
|
|
$('#rpg-toggle-plot-buttons').on('change', function() {
|
|
extensionSettings.enablePlotButtons = $(this).prop('checked');
|
|
// console.log('[RPG Companion] Toggle enablePlotButtons changed to:', extensionSettings.enablePlotButtons);
|
|
saveSettings();
|
|
togglePlotButtons();
|
|
});
|
|
|
|
$('#rpg-toggle-animations').on('change', function() {
|
|
extensionSettings.enableAnimations = $(this).prop('checked');
|
|
saveSettings();
|
|
toggleAnimations();
|
|
});
|
|
|
|
$('#rpg-manual-update').on('click', async function() {
|
|
if (!extensionSettings.enabled) {
|
|
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
|
return;
|
|
}
|
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts);
|
|
});
|
|
|
|
$('#rpg-stat-bar-color-low').on('change', function() {
|
|
extensionSettings.statBarColorLow = String($(this).val());
|
|
saveSettings();
|
|
renderUserStats(); // Re-render with new colors
|
|
});
|
|
|
|
$('#rpg-stat-bar-color-high').on('change', function() {
|
|
extensionSettings.statBarColorHigh = String($(this).val());
|
|
saveSettings();
|
|
renderUserStats(); // Re-render with new colors
|
|
});
|
|
|
|
// Theme selection
|
|
$('#rpg-theme-select').on('change', function() {
|
|
extensionSettings.theme = String($(this).val());
|
|
saveSettings();
|
|
applyTheme();
|
|
toggleCustomColors();
|
|
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly
|
|
updateChatThoughts(); // Recreate thought bubbles with new theme
|
|
});
|
|
|
|
// Custom color pickers
|
|
$('#rpg-custom-bg').on('change', function() {
|
|
extensionSettings.customColors.bg = String($(this).val());
|
|
saveSettings();
|
|
if (extensionSettings.theme === 'custom') {
|
|
applyCustomTheme();
|
|
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly
|
|
updateChatThoughts(); // Update thought bubbles
|
|
}
|
|
});
|
|
|
|
$('#rpg-custom-accent').on('change', function() {
|
|
extensionSettings.customColors.accent = String($(this).val());
|
|
saveSettings();
|
|
if (extensionSettings.theme === 'custom') {
|
|
applyCustomTheme();
|
|
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly
|
|
updateChatThoughts(); // Update thought bubbles
|
|
}
|
|
});
|
|
|
|
$('#rpg-custom-text').on('change', function() {
|
|
extensionSettings.customColors.text = String($(this).val());
|
|
saveSettings();
|
|
if (extensionSettings.theme === 'custom') {
|
|
applyCustomTheme();
|
|
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly
|
|
updateChatThoughts(); // Update thought bubbles
|
|
}
|
|
});
|
|
|
|
$('#rpg-custom-highlight').on('change', function() {
|
|
extensionSettings.customColors.highlight = String($(this).val());
|
|
saveSettings();
|
|
if (extensionSettings.theme === 'custom') {
|
|
applyCustomTheme();
|
|
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly
|
|
updateChatThoughts(); // Update thought bubbles
|
|
}
|
|
});
|
|
|
|
// Initialize UI state (enable/disable is in Extensions tab)
|
|
$('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate);
|
|
$('#rpg-position-select').val(extensionSettings.panelPosition);
|
|
$('#rpg-update-depth').val(extensionSettings.updateDepth);
|
|
$('#rpg-use-main-model').prop('checked', extensionSettings.useMainModel);
|
|
$('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats);
|
|
$('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox);
|
|
$('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts);
|
|
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
|
|
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
|
|
$('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons);
|
|
$('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations);
|
|
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
|
|
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
|
|
$('#rpg-theme-select').val(extensionSettings.theme);
|
|
$('#rpg-custom-bg').val(extensionSettings.customColors.bg);
|
|
$('#rpg-custom-accent').val(extensionSettings.customColors.accent);
|
|
$('#rpg-custom-text').val(extensionSettings.customColors.text);
|
|
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
|
|
$('#rpg-generation-mode').val(extensionSettings.generationMode);
|
|
|
|
updatePanelVisibility();
|
|
updateSectionVisibility();
|
|
updateGenerationModeUI();
|
|
applyTheme();
|
|
applyPanelPosition();
|
|
toggleCustomColors();
|
|
toggleAnimations();
|
|
|
|
// Setup mobile toggle button
|
|
setupMobileToggle();
|
|
|
|
// Setup collapse/expand toggle button
|
|
setupCollapseToggle();
|
|
|
|
// Render initial data if available
|
|
renderUserStats();
|
|
renderInfoBox();
|
|
renderThoughts();
|
|
updateDiceDisplay();
|
|
setupDiceRoller();
|
|
setupClassicStatsButtons();
|
|
setupSettingsPopup();
|
|
addDiceQuickReply();
|
|
setupPlotButtons(sendPlotProgression);
|
|
setupMobileKeyboardHandling();
|
|
setupContentEditableScrolling();
|
|
}
|
|
|
|
/**
|
|
* Sets up event listeners for classic stat +/- buttons using delegation.
|
|
* Uses delegated events to persist across re-renders of the stats section.
|
|
*/
|
|
function setupClassicStatsButtons() {
|
|
if (!$userStatsContainer) return;
|
|
|
|
// Delegated event listener for increase buttons
|
|
$userStatsContainer.on('click', '.rpg-stat-increase', function() {
|
|
const stat = $(this).data('stat');
|
|
if (extensionSettings.classicStats[stat] < 100) {
|
|
extensionSettings.classicStats[stat]++;
|
|
saveSettings();
|
|
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]);
|
|
}
|
|
});
|
|
|
|
// Delegated event listener for decrease buttons
|
|
$userStatsContainer.on('click', '.rpg-stat-decrease', function() {
|
|
const stat = $(this).data('stat');
|
|
if (extensionSettings.classicStats[stat] > 1) {
|
|
extensionSettings.classicStats[stat]--;
|
|
saveSettings();
|
|
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]);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends a plot progression request and appends the result to the last message.
|
|
* @param {string} type - 'random' or 'natural'
|
|
*/
|
|
async function sendPlotProgression(type) {
|
|
if (!extensionSettings.enabled) {
|
|
// console.log('[RPG Companion] Extension is disabled');
|
|
return;
|
|
}
|
|
|
|
// Disable buttons to prevent multiple clicks
|
|
$('#rpg-plot-random, #rpg-plot-natural').prop('disabled', true).css('opacity', '0.5');
|
|
|
|
// Store original enabled state and temporarily disable extension
|
|
// This prevents RPG tracker instructions from being injected during plot progression
|
|
const wasEnabled = extensionSettings.enabled;
|
|
extensionSettings.enabled = false;
|
|
|
|
try {
|
|
// console.log(`[RPG Companion] Sending ${type} plot progression request...`);
|
|
|
|
// Build the prompt based on type
|
|
let prompt = '';
|
|
if (type === 'random') {
|
|
prompt = '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.';
|
|
} else {
|
|
prompt = 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.';
|
|
}
|
|
|
|
// Add HTML prompt if enabled
|
|
if (extensionSettings.enableHtmlPrompt) {
|
|
prompt += '\n\n' + `If appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response:
|
|
- Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on.
|
|
- Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible.
|
|
- Embed all resources directly (e.g., inline SVGs) so nothing relies on external fonts or libraries.
|
|
- Place elements naturally in the narrative where characters would see or use them, with no limits on format or application.
|
|
- These HTML/CSS/JS elements must be rendered directly without enclosing them in code fences.`;
|
|
}
|
|
|
|
// Set flag to indicate we're doing plot progression
|
|
// This will be used by onMessageReceived to clear the prompt after generation completes
|
|
setIsPlotProgression(true);
|
|
|
|
// console.log('[RPG Companion] Calling Generate with continuation and plot prompt');
|
|
// console.log('[RPG Companion] Full prompt:', prompt);
|
|
|
|
// Pass the prompt via options with the correct property name
|
|
// Based on /continue slash command implementation, it uses quiet_prompt (underscore, not camelCase)
|
|
const options = {
|
|
quiet_prompt: prompt, // Use underscore notation, not camelCase
|
|
quietToLoud: true
|
|
};
|
|
|
|
// Call Generate with 'continue' type and our custom prompt
|
|
await Generate('continue', options);
|
|
|
|
// console.log('[RPG Companion] Plot progression generation triggered');
|
|
} catch (error) {
|
|
console.error('[RPG Companion] Error sending plot progression:', error);
|
|
setIsPlotProgression(false);
|
|
} finally {
|
|
// Restore original enabled state and re-enable buttons after a delay
|
|
setTimeout(() => {
|
|
extensionSettings.enabled = wasEnabled;
|
|
$('#rpg-plot-random, #rpg-plot-natural').prop('disabled', false).css('opacity', '1');
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears all extension prompts.
|
|
*/
|
|
function clearExtensionPrompts() {
|
|
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
|
|
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
|
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
|
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
|
// Note: rpg-companion-plot is not cleared here since it's passed via quiet_prompt option
|
|
// console.log('[RPG Companion] Cleared all extension prompts');
|
|
}
|
|
|
|
/**
|
|
* Updates the UI based on generation mode selection.
|
|
*/
|
|
function updateGenerationModeUI() {
|
|
if (extensionSettings.generationMode === 'together') {
|
|
// In "together" mode, manual update button is hidden
|
|
$('#rpg-manual-update').hide();
|
|
} else {
|
|
// In "separate" mode, manual update button is visible
|
|
$('#rpg-manual-update').show();
|
|
}
|
|
}
|
|
|
|
// Rendering functions removed - now imported from src/systems/rendering/*
|
|
// (renderUserStats, renderInfoBox, renderThoughts, updateInfoBoxField,
|
|
// updateCharacterField, updateChatThoughts, createThoughtPanel)
|
|
|
|
|
|
/**
|
|
* Commits the tracker data from the last assistant message to be used as source for next generation.
|
|
* This should be called when the user has replied to a message, ensuring all swipes of the next
|
|
* response use the same committed context.
|
|
*/
|
|
function commitTrackerData() {
|
|
const chat = getContext().chat;
|
|
if (!chat || chat.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Find the last assistant message
|
|
for (let i = chat.length - 1; i >= 0; i--) {
|
|
const message = chat[i];
|
|
if (!message.is_user) {
|
|
// Found last assistant message - commit its tracker data
|
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
|
const swipeId = message.swipe_id || 0;
|
|
const swipeData = message.extra.rpg_companion_swipes[swipeId];
|
|
|
|
if (swipeData) {
|
|
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
|
|
committedTrackerData.userStats = swipeData.userStats || null;
|
|
committedTrackerData.infoBox = swipeData.infoBox || null;
|
|
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
|
|
} else {
|
|
// console.log('[RPG Companion] No swipe data found for swipe', swipeId);
|
|
}
|
|
} else {
|
|
// console.log('[RPG Companion] No RPG data found in last assistant message');
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event handler for when the user sends a message.
|
|
* Sets the flag to indicate this is NOT a swipe.
|
|
*/
|
|
function onMessageSent() {
|
|
if (!extensionSettings.enabled) return;
|
|
|
|
// User sent a new message - NOT a swipe
|
|
lastActionWasSwipe = false;
|
|
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent - lastActionWasSwipe =', lastActionWasSwipe);
|
|
}
|
|
|
|
/**
|
|
* Event handler for when a message is generated.
|
|
*/
|
|
async function onMessageReceived(data) {
|
|
if (!extensionSettings.enabled) {
|
|
return;
|
|
}
|
|
|
|
if (extensionSettings.generationMode === 'together') {
|
|
// In together mode, parse the response to extract RPG data
|
|
// The message should be in chat[chat.length - 1]
|
|
const lastMessage = chat[chat.length - 1];
|
|
if (lastMessage && !lastMessage.is_user) {
|
|
const responseText = lastMessage.mes;
|
|
// console.log('[RPG Companion] Parsing together mode response:', responseText);
|
|
|
|
const parsedData = parseResponse(responseText);
|
|
|
|
// Update stored data
|
|
if (parsedData.userStats) {
|
|
lastGeneratedData.userStats = parsedData.userStats;
|
|
parseUserStats(parsedData.userStats);
|
|
}
|
|
if (parsedData.infoBox) {
|
|
lastGeneratedData.infoBox = parsedData.infoBox;
|
|
}
|
|
if (parsedData.characterThoughts) {
|
|
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
|
}
|
|
|
|
// Store RPG data for this specific swipe in the message's extra field
|
|
if (!lastMessage.extra) {
|
|
lastMessage.extra = {};
|
|
}
|
|
if (!lastMessage.extra.rpg_companion_swipes) {
|
|
lastMessage.extra.rpg_companion_swipes = {};
|
|
}
|
|
|
|
const currentSwipeId = lastMessage.swipe_id || 0;
|
|
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
|
userStats: parsedData.userStats,
|
|
infoBox: parsedData.infoBox,
|
|
characterThoughts: parsedData.characterThoughts
|
|
};
|
|
|
|
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
|
|
|
|
// If there's no committed data yet (first time generating), automatically commit
|
|
if (!committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts) {
|
|
committedTrackerData.userStats = parsedData.userStats;
|
|
committedTrackerData.infoBox = parsedData.infoBox;
|
|
committedTrackerData.characterThoughts = parsedData.characterThoughts;
|
|
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
|
|
} else {
|
|
// console.log('[RPG Companion] Data will be committed when user replies');
|
|
}
|
|
|
|
// Remove the tracker code blocks from the visible message
|
|
let cleanedMessage = responseText;
|
|
// Remove all code blocks that contain tracker data
|
|
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');
|
|
|
|
// Update the message in chat history
|
|
lastMessage.mes = cleanedMessage.trim();
|
|
|
|
// Update the swipe text as well
|
|
if (lastMessage.swipes && lastMessage.swipes[currentSwipeId] !== undefined) {
|
|
lastMessage.swipes[currentSwipeId] = cleanedMessage.trim();
|
|
}
|
|
|
|
// console.log('[RPG Companion] Cleaned message, removed tracker code blocks');
|
|
|
|
// Render the updated data
|
|
renderUserStats();
|
|
renderInfoBox();
|
|
renderThoughts();
|
|
|
|
// Save to chat metadata
|
|
saveChatData();
|
|
}
|
|
} else if (extensionSettings.generationMode === 'separate' && extensionSettings.autoUpdate) {
|
|
// In separate mode with auto-update, trigger update after message
|
|
setTimeout(async () => {
|
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts);
|
|
}, 500);
|
|
}
|
|
|
|
// 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)
|
|
if (lastActionWasSwipe) {
|
|
// console.log('[RPG Companion] 🔄 Generation complete after swipe - resetting lastActionWasSwipe to false');
|
|
lastActionWasSwipe = false;
|
|
}
|
|
|
|
// Clear plot progression flag if this was a plot progression generation
|
|
// Note: No need to clear extension prompt since we used quiet_prompt option
|
|
if (isPlotProgression) {
|
|
isPlotProgression = false;
|
|
console.log('[RPG Companion] Plot progression generation completed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event handler for character change.
|
|
*/
|
|
function onCharacterChanged() {
|
|
// Remove thought panel and icon when changing characters
|
|
$('#rpg-thought-panel').remove();
|
|
$('#rpg-thought-icon').remove();
|
|
$('#chat').off('scroll.thoughtPanel');
|
|
$(window).off('resize.thoughtPanel');
|
|
$(document).off('click.thoughtPanel');
|
|
|
|
// Load chat-specific data when switching chats
|
|
loadChatData();
|
|
|
|
// Commit tracker data from the last assistant message to initialize for this chat
|
|
commitTrackerData();
|
|
|
|
// Re-render with the loaded data
|
|
renderUserStats();
|
|
renderInfoBox();
|
|
renderThoughts();
|
|
|
|
// Update chat thought overlays
|
|
updateChatThoughts();
|
|
}
|
|
|
|
/**
|
|
* Event handler for when a message is swiped.
|
|
* Loads the RPG data for the swipe the user navigated to.
|
|
*/
|
|
function onMessageSwiped(messageIndex) {
|
|
if (!extensionSettings.enabled) {
|
|
return;
|
|
}
|
|
|
|
// console.log('[RPG Companion] Message swiped at index:', messageIndex);
|
|
|
|
// Get the message that was swiped
|
|
const message = chat[messageIndex];
|
|
if (!message || message.is_user) {
|
|
return;
|
|
}
|
|
|
|
const currentSwipeId = message.swipe_id || 0;
|
|
|
|
// Only set flag to true if this swipe will trigger a NEW generation
|
|
// Check if the swipe already exists (has content in the swipes array)
|
|
const isExistingSwipe = message.swipes &&
|
|
message.swipes[currentSwipeId] !== undefined &&
|
|
message.swipes[currentSwipeId] !== null &&
|
|
message.swipes[currentSwipeId].length > 0;
|
|
|
|
if (!isExistingSwipe) {
|
|
// This is a NEW swipe that will trigger generation
|
|
lastActionWasSwipe = true;
|
|
// console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped (NEW generation) - lastActionWasSwipe =', lastActionWasSwipe);
|
|
} else {
|
|
// This is navigating to an EXISTING swipe - don't change the flag
|
|
// console.log('[RPG Companion] 🔵 EVENT: onMessageSwiped (existing swipe navigation) - lastActionWasSwipe unchanged =', lastActionWasSwipe);
|
|
}
|
|
|
|
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
|
|
|
// Load RPG data for this swipe into lastGeneratedData (for display only)
|
|
// This updates what the user sees, but does NOT commit it
|
|
// Committed data will be updated when/if the user replies to this swipe
|
|
if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) {
|
|
const swipeData = message.extra.rpg_companion_swipes[currentSwipeId];
|
|
|
|
// Update display data
|
|
lastGeneratedData.userStats = swipeData.userStats || null;
|
|
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
|
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
|
|
|
// Parse user stats if available
|
|
if (swipeData.userStats) {
|
|
parseUserStats(swipeData.userStats);
|
|
}
|
|
|
|
// console.log('[RPG Companion] Loaded RPG data for swipe', currentSwipeId, '(display only, NOT committed)');
|
|
// console.log('[RPG Companion] committedTrackerData unchanged - will be updated if user replies to this swipe');
|
|
} else {
|
|
// No data for this swipe - keep existing lastGeneratedData (don't clear it)
|
|
// This ensures the display remains consistent and data is available for next commit
|
|
// console.log('[RPG Companion] No RPG data for swipe', currentSwipeId, '- keeping existing lastGeneratedData');
|
|
}
|
|
|
|
// Re-render the panels (display only - committedTrackerData unchanged)
|
|
renderUserStats();
|
|
renderInfoBox();
|
|
renderThoughts();
|
|
|
|
// Update chat thought overlays
|
|
updateChatThoughts();
|
|
}
|
|
|
|
/**
|
|
* Automatically imports the HTML cleaning regex script if it doesn't already exist.
|
|
* This regex removes HTML tags from outgoing prompts to prevent formatting issues.
|
|
*/
|
|
async function ensureHtmlCleaningRegex() {
|
|
try {
|
|
// Check if the HTML cleaning regex already exists
|
|
const scriptName = 'Clean HTML (From Outgoing Prompt)';
|
|
const existingScripts = st_extension_settings?.regex || [];
|
|
const alreadyExists = existingScripts.some(script => script.scriptName === scriptName);
|
|
|
|
if (alreadyExists) {
|
|
console.log('[RPG Companion] HTML cleaning regex already exists, skipping import');
|
|
return;
|
|
}
|
|
|
|
// Generate a UUID for the script
|
|
const uuidv4 = () => {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
};
|
|
|
|
// Create the regex script object based on the attached file
|
|
const regexScript = {
|
|
id: uuidv4(),
|
|
scriptName: scriptName,
|
|
findRegex: '/\\s?<(?!\\!--)(?:\"[^\"]*\"|\'[^\']*\'|[^\'\">])*>/g',
|
|
replaceString: '',
|
|
trimStrings: [],
|
|
placement: [2], // 2 = Input (affects outgoing prompt)
|
|
disabled: false,
|
|
markdownOnly: false,
|
|
promptOnly: true,
|
|
runOnEdit: true,
|
|
substituteRegex: 0,
|
|
minDepth: null,
|
|
maxDepth: null
|
|
};
|
|
|
|
// Add to global regex scripts
|
|
if (!Array.isArray(st_extension_settings.regex)) {
|
|
st_extension_settings.regex = [];
|
|
}
|
|
|
|
st_extension_settings.regex.push(regexScript);
|
|
|
|
// Save the changes using the already-imported function
|
|
saveSettingsDebounced();
|
|
|
|
console.log('[RPG Companion] ✅ HTML cleaning regex imported successfully');
|
|
} catch (error) {
|
|
console.error('[RPG Companion] Failed to import HTML cleaning regex:', error);
|
|
// Don't throw - this is a nice-to-have feature
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the persona avatar image when user switches personas
|
|
*/
|
|
function updatePersonaAvatar() {
|
|
const portraitImg = document.querySelector('.rpg-user-portrait');
|
|
if (!portraitImg) {
|
|
console.log('[RPG Companion] Portrait image element not found in DOM');
|
|
return;
|
|
}
|
|
|
|
// Get current user_avatar from context instead of using imported value
|
|
const context = getContext();
|
|
const currentUserAvatar = context.user_avatar || user_avatar;
|
|
|
|
console.log('[RPG Companion] Attempting to update persona avatar:', currentUserAvatar);
|
|
|
|
// Try to get a valid thumbnail URL using our safe helper
|
|
if (currentUserAvatar) {
|
|
const thumbnailUrl = getSafeThumbnailUrl('persona', currentUserAvatar);
|
|
|
|
if (thumbnailUrl) {
|
|
// Only update the src if we got a valid URL
|
|
portraitImg.src = thumbnailUrl;
|
|
console.log('[RPG Companion] Persona avatar updated successfully');
|
|
} else {
|
|
// Don't update the src if we couldn't get a valid URL
|
|
// This prevents 400 errors and keeps the existing image
|
|
console.warn('[RPG Companion] Could not get valid thumbnail URL for persona avatar, keeping existing image');
|
|
}
|
|
} else {
|
|
console.log('[RPG Companion] No user avatar configured, keeping existing image');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main initialization function.
|
|
*/
|
|
jQuery(async () => {
|
|
try {
|
|
loadSettings();
|
|
await addExtensionSettings();
|
|
await initUI();
|
|
|
|
// Load chat-specific data for current chat
|
|
loadChatData();
|
|
|
|
// Import the HTML cleaning regex if needed
|
|
await ensureHtmlCleaningRegex();
|
|
|
|
// Register event listeners
|
|
eventSource.on(event_types.MESSAGE_SENT, onMessageSent);
|
|
eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);
|
|
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
|
|
// Removed CHARACTER_MESSAGE_RENDERED to prevent race condition with cleaned messages
|
|
eventSource.on(event_types.CHAT_CHANGED, onCharacterChanged);
|
|
eventSource.on(event_types.MESSAGE_SWIPED, onMessageSwiped);
|
|
// Update persona avatar when user switches personas or chat changes
|
|
eventSource.on(event_types.CHAT_CHANGED, updatePersonaAvatar);
|
|
eventSource.on(event_types.USER_MESSAGE_RENDERED, updatePersonaAvatar);
|
|
eventSource.on(event_types.SETTINGS_UPDATED, updatePersonaAvatar);
|
|
|
|
// console.log('[RPG Companion] Extension loaded successfully');
|
|
} catch (error) {
|
|
console.error('[RPG Companion] Failed to initialize:', error);
|
|
throw error;
|
|
}
|
|
});
|