Merge pull request #135 from Tremendoussly/doom-lite-expression-sync-v2
Add optional alternate tracker displays and expression sync
This commit is contained in:
@@ -134,6 +134,20 @@ import {
|
|||||||
removeDesktopTabs,
|
removeDesktopTabs,
|
||||||
updateStripWidgets
|
updateStripWidgets
|
||||||
} from './src/systems/ui/desktop.js';
|
} from './src/systems/ui/desktop.js';
|
||||||
|
import {
|
||||||
|
removeAlternatePresentCharactersPanel,
|
||||||
|
renderAlternatePresentCharacters
|
||||||
|
} from './src/systems/ui/alternatePresentCharacters.js';
|
||||||
|
import {
|
||||||
|
initThoughtBasedExpressions,
|
||||||
|
queueThoughtBasedExpressionsUpdate,
|
||||||
|
onThoughtBasedExpressionsSettingChanged,
|
||||||
|
onAlternatePresentCharactersVisibilityChanged,
|
||||||
|
onHideDefaultExpressionDisplaySettingChanged,
|
||||||
|
clearThoughtBasedExpressionsCache,
|
||||||
|
onThoughtBasedExpressionsChatChanged,
|
||||||
|
setThoughtBasedExpressionsRefreshHandler
|
||||||
|
} from './src/systems/integration/thoughtBasedExpressions.js';
|
||||||
|
|
||||||
// Feature modules
|
// Feature modules
|
||||||
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
|
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
|
||||||
@@ -150,8 +164,10 @@ import {
|
|||||||
onMessageSent,
|
onMessageSent,
|
||||||
onMessageReceived,
|
onMessageReceived,
|
||||||
onCharacterChanged,
|
onCharacterChanged,
|
||||||
onMessageSwiped,
|
onChatLoaded,
|
||||||
onMessageDeleted,
|
onMessageDeleted,
|
||||||
|
onMessageSwiped,
|
||||||
|
scheduleChatStateRehydration,
|
||||||
updatePersonaAvatar,
|
updatePersonaAvatar,
|
||||||
clearExtensionPrompts,
|
clearExtensionPrompts,
|
||||||
onGenerationEnded,
|
onGenerationEnded,
|
||||||
@@ -161,6 +177,10 @@ import {
|
|||||||
// Old state variable declarations removed - now imported from core modules
|
// Old state variable declarations removed - now imported from core modules
|
||||||
// (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js)
|
// (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js)
|
||||||
|
|
||||||
|
setThoughtBasedExpressionsRefreshHandler(() => {
|
||||||
|
renderAlternatePresentCharacters({ useCommittedFallback: true });
|
||||||
|
});
|
||||||
|
|
||||||
// Utility functions removed - now imported from src/utils/avatars.js
|
// Utility functions removed - now imported from src/utils/avatars.js
|
||||||
// (getSafeThumbnailUrl)
|
// (getSafeThumbnailUrl)
|
||||||
|
|
||||||
@@ -217,6 +237,7 @@ async function addExtensionSettings() {
|
|||||||
clearExtensionPrompts();
|
clearExtensionPrompts();
|
||||||
updateChatThoughts(); // Remove thought bubbles
|
updateChatThoughts(); // Remove thought bubbles
|
||||||
cleanupCheckpointUI(); // Remove checkpoint buttons and indicators
|
cleanupCheckpointUI(); // Remove checkpoint buttons and indicators
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
|
||||||
// Disable dynamic weather effects
|
// Disable dynamic weather effects
|
||||||
toggleDynamicWeather(false);
|
toggleDynamicWeather(false);
|
||||||
@@ -226,10 +247,13 @@ async function addExtensionSettings() {
|
|||||||
$('#rpg-mobile-toggle').remove();
|
$('#rpg-mobile-toggle').remove();
|
||||||
$('#rpg-collapse-toggle').remove();
|
$('#rpg-collapse-toggle').remove();
|
||||||
$('#rpg-plot-buttons').remove(); // Remove plot buttons
|
$('#rpg-plot-buttons').remove(); // Remove plot buttons
|
||||||
|
removeAlternatePresentCharactersPanel();
|
||||||
} else if (extensionSettings.enabled && !wasEnabled) {
|
} else if (extensionSettings.enabled && !wasEnabled) {
|
||||||
// Enabling extension - initialize UI
|
// Enabling extension - initialize UI
|
||||||
await initUI();
|
await initUI();
|
||||||
loadChatData(); // Load chat data for current chat
|
loadChatData(); // Load chat data for current chat
|
||||||
|
scheduleChatStateRehydration();
|
||||||
|
initThoughtBasedExpressions();
|
||||||
updateChatThoughts(); // Create thought bubbles if data exists
|
updateChatThoughts(); // Create thought bubbles if data exists
|
||||||
injectCheckpointButton(); // Re-add checkpoint buttons
|
injectCheckpointButton(); // Re-add checkpoint buttons
|
||||||
updateAllCheckpointIndicators(); // Update button states
|
updateAllCheckpointIndicators(); // Update button states
|
||||||
@@ -336,6 +360,26 @@ async function initUI() {
|
|||||||
extensionSettings.showCharacterThoughts = $(this).prop('checked');
|
extensionSettings.showCharacterThoughts = $(this).prop('checked');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
updateSectionVisibility();
|
updateSectionVisibility();
|
||||||
|
renderThoughts();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-alt-present-characters').on('change', function() {
|
||||||
|
extensionSettings.showAlternatePresentCharactersPanel = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
renderThoughts();
|
||||||
|
onAlternatePresentCharactersVisibilityChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-thought-based-expressions').on('change', function() {
|
||||||
|
extensionSettings.enableThoughtBasedExpressions = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
onThoughtBasedExpressionsSettingChanged(extensionSettings.enableThoughtBasedExpressions);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-hide-default-expressions').on('change', function() {
|
||||||
|
extensionSettings.hideDefaultExpressionDisplay = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
onHideDefaultExpressionDisplaySettingChanged(extensionSettings.hideDefaultExpressionDisplay);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-inventory').on('change', function() {
|
$('#rpg-toggle-inventory').on('change', function() {
|
||||||
@@ -368,6 +412,12 @@ async function initUI() {
|
|||||||
updateChatThoughts();
|
updateChatThoughts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-inline-thoughts').on('change', function() {
|
||||||
|
extensionSettings.thoughtsInChatStyle = $(this).prop('checked') ? 'inline' : 'corner';
|
||||||
|
saveSettings();
|
||||||
|
updateChatThoughts();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-html-prompt').on('change', function() {
|
$('#rpg-toggle-html-prompt').on('change', function() {
|
||||||
extensionSettings.enableHtmlPrompt = $(this).prop('checked');
|
extensionSettings.enableHtmlPrompt = $(this).prop('checked');
|
||||||
// console.log('[RPG Companion] Toggle enableHtmlPrompt changed to:', extensionSettings.enableHtmlPrompt);
|
// console.log('[RPG Companion] Toggle enableHtmlPrompt changed to:', extensionSettings.enableHtmlPrompt);
|
||||||
@@ -1066,10 +1116,14 @@ async function initUI() {
|
|||||||
$('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats);
|
$('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats);
|
||||||
$('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox);
|
$('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox);
|
||||||
$('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts);
|
$('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts);
|
||||||
|
$('#rpg-toggle-alt-present-characters').prop('checked', extensionSettings.showAlternatePresentCharactersPanel ?? false);
|
||||||
|
$('#rpg-toggle-thought-based-expressions').prop('checked', extensionSettings.enableThoughtBasedExpressions === true);
|
||||||
|
$('#rpg-toggle-hide-default-expressions').prop('checked', extensionSettings.hideDefaultExpressionDisplay === true);
|
||||||
$('#rpg-toggle-inventory').prop('checked', extensionSettings.showInventory);
|
$('#rpg-toggle-inventory').prop('checked', extensionSettings.showInventory);
|
||||||
$('#rpg-toggle-quests').prop('checked', extensionSettings.showQuests);
|
$('#rpg-toggle-quests').prop('checked', extensionSettings.showQuests);
|
||||||
$('#rpg-toggle-lock-icons').prop('checked', extensionSettings.showLockIcons ?? true);
|
$('#rpg-toggle-lock-icons').prop('checked', extensionSettings.showLockIcons ?? true);
|
||||||
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
|
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
|
||||||
|
$('#rpg-toggle-inline-thoughts').prop('checked', (extensionSettings.thoughtsInChatStyle || 'corner') === 'inline');
|
||||||
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
|
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
|
||||||
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
|
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
|
||||||
$('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false);
|
$('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false);
|
||||||
@@ -1306,6 +1360,8 @@ jQuery(async () => {
|
|||||||
// Load chat-specific data for current chat
|
// Load chat-specific data for current chat
|
||||||
try {
|
try {
|
||||||
loadChatData();
|
loadChatData();
|
||||||
|
scheduleChatStateRehydration();
|
||||||
|
initThoughtBasedExpressions();
|
||||||
// Initialize FAB widgets and strip widgets with any loaded data
|
// Initialize FAB widgets and strip widgets with any loaded data
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
updateStripWidgets();
|
updateStripWidgets();
|
||||||
@@ -1376,11 +1432,69 @@ jQuery(async () => {
|
|||||||
[event_types.GENERATION_STOPPED]: onGenerationEnded,
|
[event_types.GENERATION_STOPPED]: onGenerationEnded,
|
||||||
[event_types.GENERATION_ENDED]: onGenerationEnded,
|
[event_types.GENERATION_ENDED]: onGenerationEnded,
|
||||||
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts],
|
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts],
|
||||||
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
[event_types.CHAT_LOADED]: onChatLoaded,
|
||||||
[event_types.MESSAGE_DELETED]: onMessageDeleted,
|
[event_types.MESSAGE_DELETED]: onMessageDeleted,
|
||||||
|
[event_types.MESSAGE_SWIPE_DELETED]: onMessageDeleted,
|
||||||
|
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
||||||
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
||||||
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedMessage = chat[messageId];
|
||||||
|
if (renderedMessage && !renderedMessage.is_user && !renderedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_UPDATED, (messageId) => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMessage = chat[messageId];
|
||||||
|
if (updatedMessage && !updatedMessage.is_user && !updatedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_SWIPED, (messageIndex) => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipedMessage = chat[messageIndex];
|
||||||
|
if (swipedMessage && !swipedMessage.is_user && !swipedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_DELETED, () => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_SWIPE_DELETED, () => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Event registration failed:', error);
|
console.error('[RPG Companion] Event registration failed:', error);
|
||||||
throw error; // This is critical - can't continue without events
|
throw error; // This is critical - can't continue without events
|
||||||
|
|||||||
@@ -29,10 +29,14 @@ export const defaultSettings = {
|
|||||||
showUserStats: true,
|
showUserStats: true,
|
||||||
showInfoBox: true,
|
showInfoBox: true,
|
||||||
showCharacterThoughts: true,
|
showCharacterThoughts: true,
|
||||||
|
showAlternatePresentCharactersPanel: false,
|
||||||
|
enableThoughtBasedExpressions: false,
|
||||||
|
hideDefaultExpressionDisplay: false,
|
||||||
showInventory: true, // Show inventory section (v2 system)
|
showInventory: true, // Show inventory section (v2 system)
|
||||||
showQuests: true, // Show quests section
|
showQuests: true, // Show quests section
|
||||||
showLockIcons: true, // Show lock/unlock icons on tracker items
|
showLockIcons: true, // Show lock/unlock icons on tracker items
|
||||||
showThoughtsInChat: true, // Show thoughts overlay in chat
|
showThoughtsInChat: true, // Show thoughts overlay in chat
|
||||||
|
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
|
||||||
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
||||||
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
||||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
||||||
|
|||||||
+288
-17
@@ -9,10 +9,13 @@ import {
|
|||||||
extensionSettings,
|
extensionSettings,
|
||||||
lastGeneratedData,
|
lastGeneratedData,
|
||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
|
thoughtBasedExpressionPortraits,
|
||||||
setExtensionSettings,
|
setExtensionSettings,
|
||||||
updateExtensionSettings,
|
updateExtensionSettings,
|
||||||
setLastGeneratedData,
|
setLastGeneratedData,
|
||||||
setCommittedTrackerData,
|
setCommittedTrackerData,
|
||||||
|
setThoughtBasedExpressionPortraits,
|
||||||
|
clearThoughtBasedExpressionPortraits,
|
||||||
FEATURE_FLAGS
|
FEATURE_FLAGS
|
||||||
} from './state.js';
|
} from './state.js';
|
||||||
import { migrateInventory } from '../utils/migration.js';
|
import { migrateInventory } from '../utils/migration.js';
|
||||||
@@ -21,6 +24,242 @@ import { migrateToV3JSON } from '../utils/jsonMigration.js';
|
|||||||
|
|
||||||
const extensionName = 'third-party/rpg-companion-sillytavern';
|
const extensionName = 'third-party/rpg-companion-sillytavern';
|
||||||
|
|
||||||
|
function hasTrackerPayload(payload) {
|
||||||
|
return !!(payload && typeof payload === 'object' && (
|
||||||
|
payload.userStats
|
||||||
|
|| payload.infoBox
|
||||||
|
|| payload.characterThoughts
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTrackerPayloadFromSwipeStore(store, preferredSwipeId = 0) {
|
||||||
|
if (!store) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTrackerPayload(store)) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferredKey = String(preferredSwipeId);
|
||||||
|
const preferredPayload = store[preferredKey] ?? store[preferredSwipeId];
|
||||||
|
if (hasTrackerPayload(preferredPayload)) {
|
||||||
|
return preferredPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrackerPayloadFromSwipeStore(store, preferredSwipeId = 0) {
|
||||||
|
const currentPayload = getCurrentTrackerPayloadFromSwipeStore(store, preferredSwipeId);
|
||||||
|
if (currentPayload) {
|
||||||
|
return currentPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store || typeof store !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericKeys = Object.keys(store)
|
||||||
|
.filter(key => /^\d+$/.test(key))
|
||||||
|
.sort((a, b) => Number(b) - Number(a));
|
||||||
|
|
||||||
|
for (const key of numericKeys) {
|
||||||
|
const payload = store[key];
|
||||||
|
if (hasTrackerPayload(payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const payload of Object.values(store)) {
|
||||||
|
if (hasTrackerPayload(payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureTrackerPayloadSlot(store, swipeId = 0) {
|
||||||
|
if (!store || typeof store !== 'object' || Array.isArray(store)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTrackerPayload(store)) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store[swipeId] || typeof store[swipeId] !== 'object' || Array.isArray(store[swipeId])) {
|
||||||
|
store[swipeId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return store[swipeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSwipeInfoEntry(message, swipeId = 0) {
|
||||||
|
if (!Array.isArray(message?.swipe_info)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.swipe_info[swipeId] || typeof message.swipe_info[swipeId] !== 'object') {
|
||||||
|
message.swipe_info[swipeId] = {
|
||||||
|
send_date: message.send_date,
|
||||||
|
gen_started: message.gen_started,
|
||||||
|
gen_finished: message.gen_finished,
|
||||||
|
extra: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.swipe_info[swipeId].extra || typeof message.swipe_info[swipeId].extra !== 'object') {
|
||||||
|
message.swipe_info[swipeId].extra = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.swipe_info[swipeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentMessageSwipeTrackerData(message) {
|
||||||
|
if (!message || message.is_user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeId = Number(message.swipe_id ?? 0);
|
||||||
|
|
||||||
|
const preferredSources = [
|
||||||
|
message.extra?.rpg_companion_swipes,
|
||||||
|
message.swipe_info?.[swipeId]?.extra?.rpg_companion_swipes
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const source of preferredSources) {
|
||||||
|
const payload = getCurrentTrackerPayloadFromSwipeStore(source, swipeId);
|
||||||
|
if (payload) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessageSwipeTrackerData(message) {
|
||||||
|
if (!message || message.is_user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeId = Number(message.swipe_id ?? 0);
|
||||||
|
const currentPayload = getCurrentMessageSwipeTrackerData(message);
|
||||||
|
if (currentPayload) {
|
||||||
|
return currentPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferredSources = [
|
||||||
|
message.extra?.rpg_companion_swipes,
|
||||||
|
message.swipe_info?.[swipeId]?.extra?.rpg_companion_swipes
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const source of preferredSources) {
|
||||||
|
const payload = getTrackerPayloadFromSwipeStore(source, swipeId);
|
||||||
|
if (payload) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.swipe_info)) {
|
||||||
|
for (let i = message.swipe_info.length - 1; i >= 0; i--) {
|
||||||
|
const payload = getTrackerPayloadFromSwipeStore(message.swipe_info[i]?.extra?.rpg_companion_swipes, swipeId);
|
||||||
|
if (payload) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLatestTrackerDataFromChat(chatMessages) {
|
||||||
|
if (!Array.isArray(chatMessages)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = chatMessages.length - 1; i >= 0; i--) {
|
||||||
|
const message = chatMessages[i];
|
||||||
|
if (message?.is_user) continue;
|
||||||
|
|
||||||
|
const swipeData = getCurrentMessageSwipeTrackerData(message);
|
||||||
|
if (!swipeData) continue;
|
||||||
|
|
||||||
|
return {
|
||||||
|
userStats: swipeData.userStats || null,
|
||||||
|
infoBox: swipeData.infoBox || null,
|
||||||
|
characterThoughts: typeof swipeData.characterThoughts === 'object'
|
||||||
|
? JSON.stringify(swipeData.characterThoughts, null, 2)
|
||||||
|
: (swipeData.characterThoughts || null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreLatestTrackerStateFromChat(chatMessages) {
|
||||||
|
const latestData = getLatestTrackerDataFromChat(chatMessages);
|
||||||
|
if (!latestData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastGeneratedData({
|
||||||
|
userStats: latestData.userStats || null,
|
||||||
|
infoBox: latestData.infoBox || null,
|
||||||
|
characterThoughts: latestData.characterThoughts || null,
|
||||||
|
html: lastGeneratedData.html || null
|
||||||
|
});
|
||||||
|
|
||||||
|
setCommittedTrackerData({
|
||||||
|
userStats: latestData.userStats || committedTrackerData.userStats || null,
|
||||||
|
infoBox: latestData.infoBox || committedTrackerData.infoBox || null,
|
||||||
|
characterThoughts: latestData.characterThoughts || committedTrackerData.characterThoughts || null
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMessageSwipeTrackerData(message, swipeId = 0, trackerData = {}) {
|
||||||
|
if (!message || message.is_user || !trackerData || typeof trackerData !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.extra || typeof message.extra !== 'object') {
|
||||||
|
message.extra = {};
|
||||||
|
}
|
||||||
|
if (!message.extra.rpg_companion_swipes || typeof message.extra.rpg_companion_swipes !== 'object' || Array.isArray(message.extra.rpg_companion_swipes)) {
|
||||||
|
message.extra.rpg_companion_swipes = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraPayload = ensureTrackerPayloadSlot(message.extra.rpg_companion_swipes, swipeId);
|
||||||
|
if (extraPayload) {
|
||||||
|
Object.assign(extraPayload, trackerData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeInfoEntry = ensureSwipeInfoEntry(message, swipeId);
|
||||||
|
if (swipeInfoEntry) {
|
||||||
|
if (!swipeInfoEntry.extra.rpg_companion_swipes || typeof swipeInfoEntry.extra.rpg_companion_swipes !== 'object' || Array.isArray(swipeInfoEntry.extra.rpg_companion_swipes)) {
|
||||||
|
swipeInfoEntry.extra.rpg_companion_swipes = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipePayload = ensureTrackerPayloadSlot(swipeInfoEntry.extra.rpg_companion_swipes, swipeId);
|
||||||
|
if (swipePayload) {
|
||||||
|
Object.assign(swipePayload, trackerData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extraPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMessageSwipeTrackerField(message, swipeId = 0, field, value) {
|
||||||
|
if (!field) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return setMessageSwipeTrackerData(message, swipeId, { [field]: value });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates extension settings structure
|
* Validates extension settings structure
|
||||||
* @param {Object} settings - Settings object to validate
|
* @param {Object} settings - Settings object to validate
|
||||||
@@ -134,6 +373,22 @@ export function loadSettings() {
|
|||||||
settingsChanged = true;
|
settingsChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize additive settings without introducing another schema bump.
|
||||||
|
if (!extensionSettings.thoughtsInChatStyle) {
|
||||||
|
extensionSettings.thoughtsInChatStyle = 'corner';
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.showAlternatePresentCharactersPanel === undefined) {
|
||||||
|
extensionSettings.showAlternatePresentCharactersPanel = false;
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.hideDefaultExpressionDisplay === undefined) {
|
||||||
|
extensionSettings.hideDefaultExpressionDisplay = false;
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Save migrated settings
|
// Save migrated settings
|
||||||
if (settingsChanged) {
|
if (settingsChanged) {
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -218,6 +473,7 @@ export function saveChatData() {
|
|||||||
quests: extensionSettings.quests,
|
quests: extensionSettings.quests,
|
||||||
lastGeneratedData: lastGeneratedData,
|
lastGeneratedData: lastGeneratedData,
|
||||||
committedTrackerData: committedTrackerData,
|
committedTrackerData: committedTrackerData,
|
||||||
|
thoughtBasedExpressionPortraits: thoughtBasedExpressionPortraits,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -257,7 +513,7 @@ export function updateMessageSwipeData() {
|
|||||||
// Find the last assistant message
|
// Find the last assistant message
|
||||||
for (let i = chat.length - 1; i >= 0; i--) {
|
for (let i = chat.length - 1; i >= 0; i--) {
|
||||||
const message = chat[i];
|
const message = chat[i];
|
||||||
if (!message.is_user) {
|
if (!message.is_user && !message.is_system) {
|
||||||
// Found last assistant message - update its swipe data
|
// Found last assistant message - update its swipe data
|
||||||
if (!message.extra) {
|
if (!message.extra) {
|
||||||
message.extra = {};
|
message.extra = {};
|
||||||
@@ -267,15 +523,11 @@ export function updateMessageSwipeData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
const swipeEntry = {
|
setMessageSwipeTrackerData(message, swipeId, {
|
||||||
userStats: lastGeneratedData.userStats,
|
userStats: lastGeneratedData.userStats,
|
||||||
infoBox: lastGeneratedData.infoBox,
|
infoBox: lastGeneratedData.infoBox,
|
||||||
characterThoughts: lastGeneratedData.characterThoughts
|
characterThoughts: lastGeneratedData.characterThoughts
|
||||||
};
|
});
|
||||||
message.extra.rpg_companion_swipes[swipeId] = swipeEntry;
|
|
||||||
|
|
||||||
// Mirror to swipe_info so data survives page reloads regardless of active swipe
|
|
||||||
mirrorToSwipeInfo(message, swipeId, swipeEntry);
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] Updated message swipe data after user edit');
|
// console.log('[RPG Companion] Updated message swipe data after user edit');
|
||||||
break;
|
break;
|
||||||
@@ -405,8 +657,10 @@ export function inheritSwipeDataFromPriorMessage(message, messageIndex) {
|
|||||||
* Automatically migrates v1 inventory to v2 format if needed.
|
* Automatically migrates v1 inventory to v2 format if needed.
|
||||||
*/
|
*/
|
||||||
export function loadChatData() {
|
export function loadChatData() {
|
||||||
if (!chat_metadata || !chat_metadata.rpg_companion) {
|
const savedData = chat_metadata?.rpg_companion;
|
||||||
// Reset to defaults if no data exists
|
|
||||||
|
if (!savedData) {
|
||||||
|
// Reset to defaults if no metadata exists, then try to rebuild from message swipe data below.
|
||||||
updateExtensionSettings({
|
updateExtensionSettings({
|
||||||
userStats: {
|
userStats: {
|
||||||
health: 100,
|
health: 100,
|
||||||
@@ -440,23 +694,21 @@ export function loadChatData() {
|
|||||||
infoBox: null,
|
infoBox: null,
|
||||||
characterThoughts: null
|
characterThoughts: null
|
||||||
});
|
});
|
||||||
return;
|
clearThoughtBasedExpressionPortraits();
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedData = chat_metadata.rpg_companion;
|
|
||||||
|
|
||||||
// Restore stats
|
// Restore stats
|
||||||
if (savedData.userStats) {
|
if (savedData?.userStats) {
|
||||||
extensionSettings.userStats = { ...savedData.userStats };
|
extensionSettings.userStats = { ...savedData.userStats };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore classic stats
|
// Restore classic stats
|
||||||
if (savedData.classicStats) {
|
if (savedData?.classicStats) {
|
||||||
extensionSettings.classicStats = { ...savedData.classicStats };
|
extensionSettings.classicStats = { ...savedData.classicStats };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore quests
|
// Restore quests
|
||||||
if (savedData.quests) {
|
if (savedData?.quests) {
|
||||||
extensionSettings.quests = { ...savedData.quests };
|
extensionSettings.quests = { ...savedData.quests };
|
||||||
} else {
|
} else {
|
||||||
// Initialize with defaults if not present
|
// Initialize with defaults if not present
|
||||||
@@ -467,7 +719,7 @@ export function loadChatData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore committed tracker data first
|
// Restore committed tracker data first
|
||||||
if (savedData.committedTrackerData) {
|
if (savedData?.committedTrackerData) {
|
||||||
// console.log('[RPG Companion] 📥 loadChatData restoring committedTrackerData:', {
|
// console.log('[RPG Companion] 📥 loadChatData restoring committedTrackerData:', {
|
||||||
// userStats: savedData.committedTrackerData.userStats ? `${savedData.committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
// userStats: savedData.committedTrackerData.userStats ? `${savedData.committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
||||||
// infoBox: savedData.committedTrackerData.infoBox ? 'exists' : 'null',
|
// infoBox: savedData.committedTrackerData.infoBox ? 'exists' : 'null',
|
||||||
@@ -484,13 +736,19 @@ export function loadChatData() {
|
|||||||
|
|
||||||
// Restore last generated data (for display)
|
// Restore last generated data (for display)
|
||||||
// Always prefer lastGeneratedData as it contains the most recent generation (including swipes)
|
// Always prefer lastGeneratedData as it contains the most recent generation (including swipes)
|
||||||
if (savedData.lastGeneratedData) {
|
if (savedData?.lastGeneratedData) {
|
||||||
// console.log('[RPG Companion] 📥 loadChatData restoring lastGeneratedData');
|
// console.log('[RPG Companion] 📥 loadChatData restoring lastGeneratedData');
|
||||||
setLastGeneratedData({ ...savedData.lastGeneratedData });
|
setLastGeneratedData({ ...savedData.lastGeneratedData });
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] ⚠️ No lastGeneratedData found in save');
|
// console.log('[RPG Companion] ⚠️ No lastGeneratedData found in save');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedData?.thoughtBasedExpressionPortraits && typeof savedData.thoughtBasedExpressionPortraits === 'object') {
|
||||||
|
setThoughtBasedExpressionPortraits(savedData.thoughtBasedExpressionPortraits);
|
||||||
|
} else {
|
||||||
|
clearThoughtBasedExpressionPortraits();
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate inventory in chat data if feature flag enabled
|
// Migrate inventory in chat data if feature flag enabled
|
||||||
if (FEATURE_FLAGS.useNewInventory && extensionSettings.userStats.inventory) {
|
if (FEATURE_FLAGS.useNewInventory && extensionSettings.userStats.inventory) {
|
||||||
const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
|
const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
|
||||||
@@ -504,6 +762,19 @@ export function loadChatData() {
|
|||||||
// Validate inventory structure (Bug #3 fix)
|
// Validate inventory structure (Bug #3 fix)
|
||||||
validateInventoryStructure(extensionSettings.userStats.inventory, 'chat');
|
validateInventoryStructure(extensionSettings.userStats.inventory, 'chat');
|
||||||
|
|
||||||
|
|
||||||
|
// Sync display data from the latest assistant message's stored swipe payload.
|
||||||
|
// This is more reliable than chat metadata alone on chat re-entry because the
|
||||||
|
// latest rendered swipe data may exist on the message even if the debounced
|
||||||
|
// metadata save did not flush yet.
|
||||||
|
try {
|
||||||
|
const chatContext = getContext();
|
||||||
|
const chatMessages = chatContext?.chat;
|
||||||
|
restoreLatestTrackerStateFromChat(chatMessages);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Companion] Per-message data sync skipped:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
// console.log('[RPG Companion] Loaded chat data:', savedData);
|
// console.log('[RPG Companion] Loaded chat data:', savedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -10,7 +10,7 @@
|
|||||||
* Extension settings - persisted to SillyTavern settings
|
* Extension settings - persisted to SillyTavern settings
|
||||||
*/
|
*/
|
||||||
export let extensionSettings = {
|
export let extensionSettings = {
|
||||||
settingsVersion: 4, // Version number for settings migrations (v4 = FAB widgets enabled by default)
|
settingsVersion: 4, // Version number for settings migrations
|
||||||
enabled: true,
|
enabled: true,
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
updateDepth: 4, // How many messages to include in the context
|
updateDepth: 4, // How many messages to include in the context
|
||||||
@@ -18,9 +18,13 @@ export let extensionSettings = {
|
|||||||
showUserStats: true,
|
showUserStats: true,
|
||||||
showInfoBox: true,
|
showInfoBox: true,
|
||||||
showCharacterThoughts: true,
|
showCharacterThoughts: true,
|
||||||
|
showAlternatePresentCharactersPanel: false,
|
||||||
|
enableThoughtBasedExpressions: false,
|
||||||
|
hideDefaultExpressionDisplay: false,
|
||||||
showInventory: true, // Show inventory section (v2 system)
|
showInventory: true, // Show inventory section (v2 system)
|
||||||
showQuests: true, // Show quests section
|
showQuests: true, // Show quests section
|
||||||
showThoughtsInChat: true, // Show thoughts overlay in chat
|
showThoughtsInChat: true, // Show thoughts overlay in chat
|
||||||
|
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
|
||||||
narratorMode: false, // Use character card as narrator instead of fixed character references
|
narratorMode: false, // Use character card as narrator instead of fixed character references
|
||||||
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
|
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
|
||||||
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
|
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
|
||||||
@@ -359,6 +363,24 @@ export function clearSessionAvatarPrompts() {
|
|||||||
sessionAvatarPrompts = {};
|
sessionAvatarPrompts = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-chat storage for thought-based Character Expressions portraits.
|
||||||
|
* Maps normalized character names to the current below-chat portrait URL.
|
||||||
|
*/
|
||||||
|
export let thoughtBasedExpressionPortraits = {};
|
||||||
|
|
||||||
|
export function setThoughtBasedExpressionPortraits(portraits) {
|
||||||
|
thoughtBasedExpressionPortraits = portraits && typeof portraits === 'object' ? { ...portraits } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThoughtBasedExpressionPortrait(characterName) {
|
||||||
|
return thoughtBasedExpressionPortraits[characterName] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearThoughtBasedExpressionPortraits() {
|
||||||
|
thoughtBasedExpressionPortraits = {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks whether the last action was a swipe (for separate mode)
|
* Tracks whether the last action was a swipe (for separate mode)
|
||||||
* Used to determine whether to commit lastGeneratedData to committedTrackerData
|
* Used to determine whether to commit lastGeneratedData to committedTrackerData
|
||||||
|
|||||||
@@ -36,6 +36,12 @@
|
|||||||
"template.settingsModal.display.showInfoBoxNote": "Display location, time, weather, and recent events.",
|
"template.settingsModal.display.showInfoBoxNote": "Display location, time, weather, and recent events.",
|
||||||
"template.settingsModal.display.showPresentCharacters": "Show Present Characters",
|
"template.settingsModal.display.showPresentCharacters": "Show Present Characters",
|
||||||
"template.settingsModal.display.showPresentCharactersNote": "Display character portraits with their current thoughts and status.",
|
"template.settingsModal.display.showPresentCharactersNote": "Display character portraits with their current thoughts and status.",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "Show Below-Chat Present Characters",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Display a compact Present Characters panel below the chat.",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "Thought-Based Expressions",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "Use SillyTavern Character Expressions to classify each present character's thoughts for the below-chat panel. May increase token usage depending on the selected Classifier API.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
|
||||||
"template.settingsModal.display.narratorMode": "Narrator Mode",
|
"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.narratorModeNote": "Use character card as narrator. Infer characters from context instead of using fixed character references.",
|
||||||
"template.settingsModal.display.showInventory": "Show Inventory",
|
"template.settingsModal.display.showInventory": "Show Inventory",
|
||||||
@@ -46,6 +52,8 @@
|
|||||||
"template.settingsModal.display.showLockIconsNote": "Display lock/unlock icons on tracker items to prevent AI from modifying them.",
|
"template.settingsModal.display.showLockIconsNote": "Display lock/unlock icons on tracker items to prevent AI from modifying them.",
|
||||||
"template.settingsModal.display.showThoughtsInChat": "Show Thoughts",
|
"template.settingsModal.display.showThoughtsInChat": "Show Thoughts",
|
||||||
"template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages.",
|
"template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages.",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "Show Thoughts Below Message Text",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "Switch between the default corner thought bubbles and thought cards below the message text.",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubble": "Always Show Thought Bubble",
|
"template.settingsModal.display.alwaysShowThoughtBubble": "Always Show Thought Bubble",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first",
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first",
|
||||||
"template.settingsModal.display.enableAnimations": "Enable Animations",
|
"template.settingsModal.display.enableAnimations": "Enable Animations",
|
||||||
|
|||||||
@@ -36,6 +36,12 @@
|
|||||||
"template.settingsModal.display.showInfoBoxNote": "Afficher le lieu, l'heure, la météo et les événements récents.",
|
"template.settingsModal.display.showInfoBoxNote": "Afficher le lieu, l'heure, la météo et les événements récents.",
|
||||||
"template.settingsModal.display.showPresentCharacters": "Afficher Personnages Présents",
|
"template.settingsModal.display.showPresentCharacters": "Afficher Personnages Présents",
|
||||||
"template.settingsModal.display.showPresentCharactersNote": "Afficher les portraits des personnages avec leurs pensées actuelles et leur statut.",
|
"template.settingsModal.display.showPresentCharactersNote": "Afficher les portraits des personnages avec leurs pensées actuelles et leur statut.",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "Afficher les personnages sous le chat",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Afficher un panneau compact des personnages présents sous le chat.",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "Expressions basées sur les pensées",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "Utiliser Character Expressions de SillyTavern pour classifier les pensées de chaque personnage présent dans le panneau sous le chat. L'utilisation de tokens peut augmenter selon l'API de classification sélectionnée.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Masquer l'affichage d'expressions par défaut",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Masquer l'affichage intégré des expressions de personnage de SillyTavern.",
|
||||||
"template.settingsModal.display.narratorMode": "Mode Narrateur",
|
"template.settingsModal.display.narratorMode": "Mode Narrateur",
|
||||||
"template.settingsModal.display.narratorModeNote": "Utiliser la carte de personnage comme narrateur. Déduire les personnages du contexte au lieu d'utiliser des références de personnages fixes.",
|
"template.settingsModal.display.narratorModeNote": "Utiliser la carte de personnage comme narrateur. Déduire les personnages du contexte au lieu d'utiliser des références de personnages fixes.",
|
||||||
"template.settingsModal.display.showInventory": "Afficher Inventaire",
|
"template.settingsModal.display.showInventory": "Afficher Inventaire",
|
||||||
@@ -46,6 +52,8 @@
|
|||||||
"template.settingsModal.display.showLockIconsNote": "Afficher les icônes de verrouillage/déverrouillage sur les éléments de suivi pour empêcher l'IA de les modifier.",
|
"template.settingsModal.display.showLockIconsNote": "Afficher les icônes de verrouillage/déverrouillage sur les éléments de suivi pour empêcher l'IA de les modifier.",
|
||||||
"template.settingsModal.display.showThoughtsInChat": "Afficher Pensées",
|
"template.settingsModal.display.showThoughtsInChat": "Afficher Pensées",
|
||||||
"template.settingsModal.display.showThoughtsInChatNote": "Afficher les pensées des personnages sous forme de bulles superposées à côté de leurs messages.",
|
"template.settingsModal.display.showThoughtsInChatNote": "Afficher les pensées des personnages sous forme de bulles superposées à côté de leurs messages.",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "Afficher les pensées sous le texte du message",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "Basculer entre les bulles de pensée dans le coin par défaut et des cartes de pensée sous le texte du message.",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubble": "Toujours Afficher Bulle Pensée",
|
"template.settingsModal.display.alwaysShowThoughtBubble": "Toujours Afficher Bulle Pensée",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Développer automatiquement la bulle de pensée sans cliquer sur l'icône d'abord",
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Développer automatiquement la bulle de pensée sans cliquer sur l'icône d'abord",
|
||||||
"template.settingsModal.display.enableAnimations": "Activer Animations",
|
"template.settingsModal.display.enableAnimations": "Activer Animations",
|
||||||
|
|||||||
@@ -35,6 +35,12 @@
|
|||||||
"template.settingsModal.display.showInfoBoxNote": "Отображение локации, времени, погоды и недавних событий.",
|
"template.settingsModal.display.showInfoBoxNote": "Отображение локации, времени, погоды и недавних событий.",
|
||||||
"template.settingsModal.display.showPresentCharacters": "Показывать персонажей",
|
"template.settingsModal.display.showPresentCharacters": "Показывать персонажей",
|
||||||
"template.settingsModal.display.showPresentCharactersNote": "Показывать портреты персонажей с их текущимы мыслями и статусом.",
|
"template.settingsModal.display.showPresentCharactersNote": "Показывать портреты персонажей с их текущимы мыслями и статусом.",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "Показывать персонажей под чатом",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Показывать компактную панель персонажей под чатом.",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "Выражения на основе мыслей",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "Использовать Character Expressions в SillyTavern для классификации мыслей каждого присутствующего персонажа в панели под чатом. Расход токенов может увеличиться в зависимости от выбранного API классификации.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Скрыть отображение выражений по умолчанию",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Скрыть встроенное отображение выражений персонажей SillyTavern.",
|
||||||
"template.settingsModal.display.narratorMode": "Режим расказчика",
|
"template.settingsModal.display.narratorMode": "Режим расказчика",
|
||||||
"template.settingsModal.display.narratorModeNote": "Использовать карточку персонажа в качестве расказчика. Персонажи берутся из контекста вместо фиксированных отсылок.",
|
"template.settingsModal.display.narratorModeNote": "Использовать карточку персонажа в качестве расказчика. Персонажи берутся из контекста вместо фиксированных отсылок.",
|
||||||
"template.settingsModal.display.showInventory": "Показывать инвентарь",
|
"template.settingsModal.display.showInventory": "Показывать инвентарь",
|
||||||
@@ -45,6 +51,8 @@
|
|||||||
"template.settingsModal.display.showLockIconsNote": "Отображать значки блокировки/разблокировки на элементах трекера, чтобы предотвратить их изменение ИИ.",
|
"template.settingsModal.display.showLockIconsNote": "Отображать значки блокировки/разблокировки на элементах трекера, чтобы предотвратить их изменение ИИ.",
|
||||||
"template.settingsModal.display.showThoughtsInChat": "Показывать мысли",
|
"template.settingsModal.display.showThoughtsInChat": "Показывать мысли",
|
||||||
"template.settingsModal.display.showThoughtsInChatNote": "Отображать мысли персонажей в виде всплывающих пузырьков рядом с их сообщениями.",
|
"template.settingsModal.display.showThoughtsInChatNote": "Отображать мысли персонажей в виде всплывающих пузырьков рядом с их сообщениями.",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "Показывать мысли под текстом сообщения",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "Переключает между стандартными угловыми пузырями мыслей и карточками мыслей под текстом сообщения.",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubble": "Всегда показывать пузырь мыслей",
|
"template.settingsModal.display.alwaysShowThoughtBubble": "Всегда показывать пузырь мыслей",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Автоматически раскрывать пузырь мыслей без предварительного нажатия на значок",
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Автоматически раскрывать пузырь мыслей без предварительного нажатия на значок",
|
||||||
"template.settingsModal.display.enableAnimations": "Включить анимации",
|
"template.settingsModal.display.enableAnimations": "Включить анимации",
|
||||||
|
|||||||
@@ -36,6 +36,12 @@
|
|||||||
"template.settingsModal.display.showInfoBoxNote": "显示位置、时间、天气和最近事件。",
|
"template.settingsModal.display.showInfoBoxNote": "显示位置、时间、天气和最近事件。",
|
||||||
"template.settingsModal.display.showPresentCharacters": "显示在场角色",
|
"template.settingsModal.display.showPresentCharacters": "显示在场角色",
|
||||||
"template.settingsModal.display.showPresentCharactersNote": "显示角色肖像及其当前想法和状态。",
|
"template.settingsModal.display.showPresentCharactersNote": "显示角色肖像及其当前想法和状态。",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "显示聊天下方的在场角色",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "在聊天下方显示紧凑的在场角色面板。",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "基于想法的表情",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "使用 SillyTavern Character Expressions 对聊天下方面板中每个在场角色的想法进行分类。Token 用量可能会因所选的分类 API 而增加。",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "隐藏默认表情显示",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "隐藏 SillyTavern 内置的角色表情显示。",
|
||||||
"template.settingsModal.display.narratorMode": "旁白模式",
|
"template.settingsModal.display.narratorMode": "旁白模式",
|
||||||
"template.settingsModal.display.narratorModeNote": "使用角色卡作为旁白。根据上下文推断角色,而非使用固定的角色引用。",
|
"template.settingsModal.display.narratorModeNote": "使用角色卡作为旁白。根据上下文推断角色,而非使用固定的角色引用。",
|
||||||
"template.settingsModal.display.showInventory": "显示物品栏",
|
"template.settingsModal.display.showInventory": "显示物品栏",
|
||||||
@@ -46,6 +52,8 @@
|
|||||||
"template.settingsModal.display.showLockIconsNote": "在跟踪器项目上显示锁定/解锁图标,以防止 AI 修改它们。",
|
"template.settingsModal.display.showLockIconsNote": "在跟踪器项目上显示锁定/解锁图标,以防止 AI 修改它们。",
|
||||||
"template.settingsModal.display.showThoughtsInChat": "显示想法",
|
"template.settingsModal.display.showThoughtsInChat": "显示想法",
|
||||||
"template.settingsModal.display.showThoughtsInChatNote": "将角色想法显示为其消息旁边的气泡。",
|
"template.settingsModal.display.showThoughtsInChatNote": "将角色想法显示为其消息旁边的气泡。",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "在消息文本下方显示想法",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "在默认角落想法气泡和显示在消息文本下方的想法卡片之间切换。",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubble": "始终显示想法气泡",
|
"template.settingsModal.display.alwaysShowThoughtBubble": "始终显示想法气泡",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自动展开想法气泡,无需先点击图标",
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自动展开想法气泡,无需先点击图标",
|
||||||
"template.settingsModal.display.enableAnimations": "启用动画",
|
"template.settingsModal.display.enableAnimations": "启用动画",
|
||||||
|
|||||||
@@ -32,11 +32,19 @@
|
|||||||
"template.settingsModal.display.showUserStats": "顯示 user 屬性",
|
"template.settingsModal.display.showUserStats": "顯示 user 屬性",
|
||||||
"template.settingsModal.display.showInfoBox": "顯示資訊框",
|
"template.settingsModal.display.showInfoBox": "顯示資訊框",
|
||||||
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
|
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "顯示聊天下方的在場角色",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "在聊天下方顯示精簡的在場角色面板。",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "基於想法的表情",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "使用 SillyTavern Character Expressions 對聊天下方面板中每個在場角色的想法進行分類。Token 用量可能會依所選的分類 API 而增加。",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "隱藏預設表情顯示",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "隱藏 SillyTavern 內建的角色表情顯示。",
|
||||||
"template.settingsModal.display.showInventory": "顯示物品欄",
|
"template.settingsModal.display.showInventory": "顯示物品欄",
|
||||||
"template.settingsModal.display.showLockIcons": "顯示鎖定/解鎖追蹤器",
|
"template.settingsModal.display.showLockIcons": "顯示鎖定/解鎖追蹤器",
|
||||||
"template.settingsModal.display.showLockIconsNote": "在追蹤器項目上顯示鎖定/解鎖圖示,以防止 AI 修改它們。",
|
"template.settingsModal.display.showLockIconsNote": "在追蹤器項目上顯示鎖定/解鎖圖示,以防止 AI 修改它們。",
|
||||||
"template.settingsModal.display.showThoughtsInChat": "在聊天中顯示想法",
|
"template.settingsModal.display.showThoughtsInChat": "在聊天中顯示想法",
|
||||||
"template.settingsModal.display.showThoughtsInChatNote": "將角色想法顯示為其訊息旁的泡泡",
|
"template.settingsModal.display.showThoughtsInChatNote": "將角色想法顯示為其訊息旁的泡泡",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "在訊息文字下方顯示想法",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "在預設角落想法泡泡與顯示在訊息文字下方的想法卡片之間切換。",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubble": "始終顯示想法泡泡",
|
"template.settingsModal.display.alwaysShowThoughtBubble": "始終顯示想法泡泡",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自動展開想法泡泡",
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自動展開想法泡泡",
|
||||||
"template.settingsModal.display.enableAnimations": "啟用動畫",
|
"template.settingsModal.display.enableAnimations": "啟用動畫",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
$musicPlayerContainer,
|
$musicPlayerContainer,
|
||||||
getSeparateGenerationId
|
getSeparateGenerationId
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData, mirrorToSwipeInfo } from '../../core/persistence.js';
|
import { saveChatData, setMessageSwipeTrackerData } from '../../core/persistence.js';
|
||||||
import {
|
import {
|
||||||
generateSeparateUpdatePrompt
|
generateSeparateUpdatePrompt
|
||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
@@ -326,15 +326,11 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = lastMessage.swipe_id || 0;
|
const currentSwipeId = lastMessage.swipe_id || 0;
|
||||||
const swipeEntry = {
|
setMessageSwipeTrackerData(lastMessage, currentSwipeId, {
|
||||||
userStats: parsedData.userStats,
|
userStats: parsedData.userStats,
|
||||||
infoBox: parsedData.infoBox,
|
infoBox: parsedData.infoBox,
|
||||||
characterThoughts: parsedData.characterThoughts
|
characterThoughts: parsedData.characterThoughts
|
||||||
};
|
});
|
||||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = swipeEntry;
|
|
||||||
|
|
||||||
// Mirror to swipe_info so this swipe survives page reload even if never manually edited
|
|
||||||
mirrorToSwipeInfo(lastMessage, currentSwipeId, swipeEntry);
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { selected_group, getGroupMembers, groups } from '../../../../../../group
|
|||||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||||
import { currentEncounter } from '../features/encounterState.js';
|
import { currentEncounter } from '../features/encounterState.js';
|
||||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
|
import { isPresentCharactersEnabled } from '../../utils/presentCharacters.js';
|
||||||
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
|
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
|
||||||
import { applyLocks } from './lockManager.js';
|
import { applyLocks } from './lockManager.js';
|
||||||
|
|
||||||
@@ -709,7 +710,7 @@ export async function buildCombatSummaryPrompt(combatLog, result) {
|
|||||||
summaryMessage += `- Never quote ${userName} directly. Express their actions and dialogue using ONLY indirect speech (e.g., "${userName} swung their sword" or "${userName} asked for help").\n\n`;
|
summaryMessage += `- Never quote ${userName} directly. Express their actions and dialogue using ONLY indirect speech (e.g., "${userName} swung their sword" or "${userName} asked for help").\n\n`;
|
||||||
|
|
||||||
// If in Together mode and trackers are enabled, add tracker update instructions
|
// If in Together mode and trackers are enabled, add tracker update instructions
|
||||||
if (extensionSettings.generationMode === 'together' && (extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts)) {
|
if (extensionSettings.generationMode === 'together' && (extensionSettings.showUserStats || extensionSettings.showInfoBox || isPresentCharactersEnabled())) {
|
||||||
summaryMessage += `\n--- TRACKER UPDATE ---\n\n`;
|
summaryMessage += `\n--- TRACKER UPDATE ---\n\n`;
|
||||||
summaryMessage += `After the [FIGHT CONCLUDED] summary, update the RPG trackers to reflect ${userName}'s state AFTER the combat encounter. `;
|
summaryMessage += `After the [FIGHT CONCLUDED] summary, update the RPG trackers to reflect ${userName}'s state AFTER the combat encounter. `;
|
||||||
summaryMessage += `Account for any injuries sustained, resources used, emotional state changes, or other consequences of the battle.\n\n`;
|
summaryMessage += `Account for any injuries sustained, resources used, emotional state changes, or other consequences of the battle.\n\n`;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
addLockInstruction
|
addLockInstruction
|
||||||
} from './jsonPromptHelpers.js';
|
} from './jsonPromptHelpers.js';
|
||||||
import { applyLocks } from './lockManager.js';
|
import { applyLocks } from './lockManager.js';
|
||||||
|
import { isPresentCharactersEnabled } from '../../utils/presentCharacters.js';
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
@@ -293,7 +294,7 @@ export function generateTrackerExample() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
if (isPresentCharactersEnabled() && committedTrackerData.characterThoughts) {
|
||||||
try {
|
try {
|
||||||
JSON.parse(committedTrackerData.characterThoughts);
|
JSON.parse(committedTrackerData.characterThoughts);
|
||||||
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
||||||
@@ -329,7 +330,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
let instructions = '';
|
let instructions = '';
|
||||||
|
|
||||||
// Check if any trackers are enabled
|
// Check if any trackers are enabled
|
||||||
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts;
|
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox || isPresentCharactersEnabled();
|
||||||
|
|
||||||
// Only add tracker instructions if at least one tracker is enabled
|
// Only add tracker instructions if at least one tracker is enabled
|
||||||
if (hasAnyTrackers) {
|
if (hasAnyTrackers) {
|
||||||
@@ -360,7 +361,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
if (extensionSettings.showInfoBox) {
|
if (extensionSettings.showInfoBox) {
|
||||||
enabledTrackers.push('infoBox');
|
enabledTrackers.push('infoBox');
|
||||||
}
|
}
|
||||||
if (extensionSettings.showCharacterThoughts) {
|
if (isPresentCharactersEnabled()) {
|
||||||
enabledTrackers.push('characters');
|
enabledTrackers.push('characters');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +384,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
instructions += enabledTrackers.indexOf('infoBox') < enabledTrackers.length - 1 ? ',\n' : '\n';
|
instructions += enabledTrackers.indexOf('infoBox') < enabledTrackers.length - 1 ? ',\n' : '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionSettings.showCharacterThoughts) {
|
if (isPresentCharactersEnabled()) {
|
||||||
instructions += ' "characters": ';
|
instructions += ' "characters": ';
|
||||||
const charactersJSON = buildCharactersJSONInstruction();
|
const charactersJSON = buildCharactersJSONInstruction();
|
||||||
// Add 2 spaces to all lines after the first to properly nest within root object
|
// Add 2 spaces to all lines after the first to properly nest within root object
|
||||||
@@ -1061,7 +1062,7 @@ export function generateContextualSummary() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add Present Characters tracker data if enabled
|
// Add Present Characters tracker data if enabled
|
||||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
if (isPresentCharactersEnabled() && committedTrackerData.characterThoughts) {
|
||||||
try {
|
try {
|
||||||
const formatted = formatTrackerDataForContext(committedTrackerData.characterThoughts, 'characters', userName);
|
const formatted = formatTrackerDataForContext(committedTrackerData.characterThoughts, 'characters', userName);
|
||||||
if (formatted) {
|
if (formatted) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { chat, user_avatar, setExtensionPrompt, extension_prompt_types, saveChatDebounced } from '../../../../../../../script.js';
|
import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types } from '../../../../../../../script.js';
|
||||||
|
|
||||||
// Core modules
|
// Core modules
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +23,18 @@ import {
|
|||||||
$musicPlayerContainer,
|
$musicPlayerContainer,
|
||||||
incrementSeparateGenerationId
|
incrementSeparateGenerationId
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData, loadChatData, autoSwitchPresetForEntity, getSwipeData, commitTrackerDataFromPriorMessage, inheritSwipeDataFromPriorMessage, mirrorToSwipeInfo } from '../../core/persistence.js';
|
import {
|
||||||
|
saveChatData,
|
||||||
|
loadChatData,
|
||||||
|
autoSwitchPresetForEntity,
|
||||||
|
getMessageSwipeTrackerData,
|
||||||
|
getCurrentMessageSwipeTrackerData,
|
||||||
|
restoreLatestTrackerStateFromChat,
|
||||||
|
setMessageSwipeTrackerData,
|
||||||
|
getSwipeData,
|
||||||
|
commitTrackerDataFromPriorMessage,
|
||||||
|
inheritSwipeDataFromPriorMessage
|
||||||
|
} from '../../core/persistence.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
// Generation & Parsing
|
// Generation & Parsing
|
||||||
@@ -52,6 +63,8 @@ import { updateStripWidgets } from '../ui/desktop.js';
|
|||||||
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
||||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||||
|
|
||||||
|
let chatStateRehydrateRunId = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the swipe store of the last assistant message in `currentChat` and
|
* Reads the swipe store of the last assistant message in `currentChat` and
|
||||||
* writes its data into `lastGeneratedData`, including syncing stat bars via
|
* writes its data into `lastGeneratedData`, including syncing stat bars via
|
||||||
@@ -133,6 +146,237 @@ export function commitTrackerData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSwipeTrackerData(message) {
|
||||||
|
return getMessageSwipeTrackerData(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSwipeTrackerData(message) {
|
||||||
|
return getCurrentMessageSwipeTrackerData(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAssistantMessageBody() {
|
||||||
|
const $messages = $('#chat .mes');
|
||||||
|
|
||||||
|
for (let i = $messages.length - 1; i >= 0; i--) {
|
||||||
|
const $message = $messages.eq(i);
|
||||||
|
if ($message.attr('is_user') === 'true') continue;
|
||||||
|
if ($message.find('.mes_text').length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAnyTrackerStateInChat() {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
|
||||||
|
for (let i = chatMessages.length - 1; i >= 0; i--) {
|
||||||
|
const swipeData = getSwipeTrackerData(chatMessages[i]);
|
||||||
|
if (swipeData?.userStats || swipeData?.infoBox || swipeData?.characterThoughts) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAssistantMessagesInChat() {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
return chatMessages.some(message => message && !message.is_user && !message.is_system);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPotentialTrackerSourceInChat() {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
|
||||||
|
for (const message of chatMessages) {
|
||||||
|
if (!message || message.is_user || message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.extra?.rpg_companion_swipes) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.swipe_info) && message.swipe_info.some(info => info?.extra?.rpg_companion_swipes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.swipes) && message.swipes.length > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRehydrateUserStatsFromDisplayData() {
|
||||||
|
const hasSavedUserStats = !!chat_metadata?.rpg_companion?.userStats;
|
||||||
|
if (!hasSavedUserStats && lastGeneratedData.userStats) {
|
||||||
|
try {
|
||||||
|
parseUserStats(lastGeneratedData.userStats);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[RPG Companion] Failed to rebuild user stats from display data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSwipeText(message) {
|
||||||
|
const swipeId = Number(message?.swipe_id ?? 0);
|
||||||
|
|
||||||
|
if (Array.isArray(message?.swipes) && typeof message.swipes[swipeId] === 'string' && message.swipes[swipeId].trim()) {
|
||||||
|
return message.swipes[swipeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof message?.mes === 'string' ? message.mes : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function repairLatestTrackerStateFromCurrentSwipeContent(chatMessages = getContext()?.chat || []) {
|
||||||
|
for (let i = chatMessages.length - 1; i >= 0; i--) {
|
||||||
|
const message = chatMessages[i];
|
||||||
|
if (!message || message.is_user || message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeId = Number(message.swipe_id ?? 0);
|
||||||
|
if (getCurrentSwipeTrackerData(message)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSwipeText = getCurrentSwipeText(message);
|
||||||
|
if (!currentSwipeText) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedData = parseResponse(currentSwipeText);
|
||||||
|
if (parsedData.userStats) {
|
||||||
|
parsedData.userStats = removeLocks(parsedData.userStats);
|
||||||
|
}
|
||||||
|
if (parsedData.infoBox) {
|
||||||
|
parsedData.infoBox = removeLocks(parsedData.infoBox);
|
||||||
|
}
|
||||||
|
if (parsedData.characterThoughts) {
|
||||||
|
parsedData.characterThoughts = removeLocks(parsedData.characterThoughts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedData.userStats && !parsedData.infoBox && !parsedData.characterThoughts) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessageSwipeTrackerData(message, swipeId, {
|
||||||
|
userStats: parsedData.userStats || null,
|
||||||
|
infoBox: parsedData.infoBox || null,
|
||||||
|
characterThoughts: parsedData.characterThoughts || null
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOrRepairLatestTrackerState() {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
let restored = restoreLatestTrackerStateFromChat(chatMessages);
|
||||||
|
|
||||||
|
if (!restored) {
|
||||||
|
const repaired = repairLatestTrackerStateFromCurrentSwipeContent(chatMessages);
|
||||||
|
if (repaired) {
|
||||||
|
restored = restoreLatestTrackerStateFromChat(chatMessages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return restored;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerenderRpgState() {
|
||||||
|
renderUserStats();
|
||||||
|
renderInfoBox();
|
||||||
|
renderThoughts();
|
||||||
|
renderInventory();
|
||||||
|
renderQuests();
|
||||||
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scheduleChatStateRehydration() {
|
||||||
|
chatStateRehydrateRunId++;
|
||||||
|
const runId = chatStateRehydrateRunId;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 15;
|
||||||
|
const eagerRetryAttempts = 4;
|
||||||
|
|
||||||
|
const tryRestoreState = () => {
|
||||||
|
if (runId !== chatStateRehydrateRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
loadChatData();
|
||||||
|
restoreOrRepairLatestTrackerState();
|
||||||
|
maybeRehydrateUserStatsFromDisplayData();
|
||||||
|
rerenderRpgState();
|
||||||
|
|
||||||
|
const hasRestoredTrackerState = !!(
|
||||||
|
lastGeneratedData.userStats
|
||||||
|
|| lastGeneratedData.infoBox
|
||||||
|
|| lastGeneratedData.characterThoughts
|
||||||
|
|| committedTrackerData.userStats
|
||||||
|
|| committedTrackerData.infoBox
|
||||||
|
|| committedTrackerData.characterThoughts
|
||||||
|
);
|
||||||
|
const hasStoredTrackerState = !!chat_metadata?.rpg_companion || hasAnyTrackerStateInChat();
|
||||||
|
const hasAssistantMessages = hasAssistantMessagesInChat();
|
||||||
|
const hasPotentialTrackerSource = hasPotentialTrackerSourceInChat();
|
||||||
|
const chatBodyReady = hasAssistantMessageBody();
|
||||||
|
|
||||||
|
if (chatBodyReady) {
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRetryForRestore = !hasRestoredTrackerState && (
|
||||||
|
hasStoredTrackerState
|
||||||
|
|| (hasAssistantMessages && attempts < eagerRetryAttempts)
|
||||||
|
|| (hasPotentialTrackerSource && attempts < maxAttempts)
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldRetryForDom = !chatBodyReady && hasAssistantMessages;
|
||||||
|
|
||||||
|
if ((shouldRetryForRestore || shouldRetryForDom) && attempts < maxAttempts) {
|
||||||
|
setTimeout(tryRestoreState, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(tryRestoreState, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onChatLoaded() {
|
||||||
|
loadChatData();
|
||||||
|
restoreOrRepairLatestTrackerState();
|
||||||
|
maybeRehydrateUserStatsFromDisplayData();
|
||||||
|
rerenderRpgState();
|
||||||
|
scheduleChatStateRehydration();
|
||||||
|
updateAllCheckpointIndicators();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDisplayedTrackerStateFromChat() {
|
||||||
|
const restored = restoreOrRepairLatestTrackerState();
|
||||||
|
|
||||||
|
if (!restored) {
|
||||||
|
lastGeneratedData.userStats = null;
|
||||||
|
lastGeneratedData.infoBox = null;
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
rerenderRpgState();
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event handler for when the user sends a message.
|
* Event handler for when the user sends a message.
|
||||||
* Sets the flag to indicate this is NOT a swipe.
|
* Sets the flag to indicate this is NOT a swipe.
|
||||||
@@ -228,15 +472,11 @@ export async function onMessageReceived(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = lastMessage.swipe_id || 0;
|
const currentSwipeId = lastMessage.swipe_id || 0;
|
||||||
const swipeEntry = {
|
setMessageSwipeTrackerData(lastMessage, currentSwipeId, {
|
||||||
userStats: parsedData.userStats,
|
userStats: parsedData.userStats,
|
||||||
infoBox: parsedData.infoBox,
|
infoBox: parsedData.infoBox,
|
||||||
characterThoughts: parsedData.characterThoughts
|
characterThoughts: parsedData.characterThoughts
|
||||||
};
|
});
|
||||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = swipeEntry;
|
|
||||||
|
|
||||||
// Mirror to swipe_info so this swipe survives page reload even if never manually edited
|
|
||||||
mirrorToSwipeInfo(lastMessage, currentSwipeId, swipeEntry);
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
|
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
|
||||||
|
|
||||||
@@ -283,6 +523,11 @@ export async function onMessageReceived(data) {
|
|||||||
|
|
||||||
// console.log('[RPG Companion] Cleaned message, removed tracker code blocks from DOM');
|
// console.log('[RPG Companion] Cleaned message, removed tracker code blocks from DOM');
|
||||||
|
|
||||||
|
// Re-insert chat thoughts after SillyTavern finishes rerendering the cleaned message DOM.
|
||||||
|
if (parsedData.characterThoughts) {
|
||||||
|
setTimeout(() => updateChatThoughts(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
// Save to chat metadata
|
// Save to chat metadata
|
||||||
saveChatData();
|
saveChatData();
|
||||||
}
|
}
|
||||||
@@ -362,6 +607,7 @@ export function onCharacterChanged() {
|
|||||||
// Remove thought panel and icon when changing characters
|
// Remove thought panel and icon when changing characters
|
||||||
$('#rpg-thought-panel').remove();
|
$('#rpg-thought-panel').remove();
|
||||||
$('#rpg-thought-icon').remove();
|
$('#rpg-thought-icon').remove();
|
||||||
|
$('.rpg-inline-thoughts, .rpg-inline-thought').remove();
|
||||||
$('#chat').off('scroll.thoughtPanel');
|
$('#chat').off('scroll.thoughtPanel');
|
||||||
$(window).off('resize.thoughtPanel');
|
$(window).off('resize.thoughtPanel');
|
||||||
$(document).off('click.thoughtPanel');
|
$(document).off('click.thoughtPanel');
|
||||||
@@ -396,20 +642,9 @@ export function onCharacterChanged() {
|
|||||||
// already contains the committed state from when we last left this chat.
|
// already contains the committed state from when we last left this chat.
|
||||||
// commitTrackerData() will be called naturally when new messages arrive.
|
// commitTrackerData() will be called naturally when new messages arrive.
|
||||||
|
|
||||||
// Re-render with the loaded data
|
// Re-render with the loaded data and retry once SillyTavern finishes restoring chat state.
|
||||||
renderUserStats();
|
rerenderRpgState();
|
||||||
renderInfoBox();
|
scheduleChatStateRehydration();
|
||||||
renderThoughts();
|
|
||||||
renderInventory();
|
|
||||||
renderQuests();
|
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
|
||||||
|
|
||||||
// Update FAB widgets and strip widgets with loaded data
|
|
||||||
updateFabWidgets();
|
|
||||||
updateStripWidgets();
|
|
||||||
|
|
||||||
// Update chat thought overlays
|
|
||||||
updateChatThoughts();
|
|
||||||
|
|
||||||
// Update checkpoint indicators for the loaded chat
|
// Update checkpoint indicators for the loaded chat
|
||||||
updateAllCheckpointIndicators();
|
updateAllCheckpointIndicators();
|
||||||
@@ -434,6 +669,7 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = message.swipe_id || 0;
|
const currentSwipeId = message.swipe_id || 0;
|
||||||
|
const swipeCount = Array.isArray(message.swipes) ? message.swipes.length : 0;
|
||||||
|
|
||||||
// Only set flag to true if this swipe will trigger a NEW generation
|
// 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)
|
// Check if the swipe already exists (has content in the swipes array)
|
||||||
@@ -441,6 +677,8 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
message.swipes[currentSwipeId] !== undefined &&
|
message.swipes[currentSwipeId] !== undefined &&
|
||||||
message.swipes[currentSwipeId] !== null &&
|
message.swipes[currentSwipeId] !== null &&
|
||||||
message.swipes[currentSwipeId].length > 0;
|
message.swipes[currentSwipeId].length > 0;
|
||||||
|
const swipeData = getCurrentSwipeTrackerData(message);
|
||||||
|
const isPendingNewSwipe = currentSwipeId >= swipeCount;
|
||||||
|
|
||||||
if (!isExistingSwipe) {
|
if (!isExistingSwipe) {
|
||||||
// This is a NEW swipe that will trigger generation
|
// This is a NEW swipe that will trigger generation
|
||||||
@@ -455,13 +693,15 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
// console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe);
|
// console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPendingNewSwipe) {
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
||||||
|
|
||||||
// Load saved swipe data into both display (lastGeneratedData) and extensionSettings.
|
// Load saved swipe data for the active swipe only.
|
||||||
// Safe to call parseUserStats() unconditionally because updateMessageSwipeData() is called
|
// Using the current-swipe helper here avoids falling back to another
|
||||||
// on every manual edit, so the swipe store always reflects the latest user changes before
|
// stored swipe payload and showing stale tracker state.
|
||||||
// any navigation can overwrite them.
|
|
||||||
const swipeData = getSwipeData(message, currentSwipeId);
|
|
||||||
if (swipeData) {
|
if (swipeData) {
|
||||||
// Load swipe data into lastGeneratedData for display (both modes)
|
// Load swipe data into lastGeneratedData for display (both modes)
|
||||||
lastGeneratedData.userStats = swipeData.userStats || null;
|
lastGeneratedData.userStats = swipeData.userStats || null;
|
||||||
@@ -500,24 +740,16 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
updateChatThoughts();
|
updateChatThoughts();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Event handler for when a message is deleted.
|
|
||||||
* Re-syncs lastGeneratedData, committedTrackerData, and all UI panels to the
|
|
||||||
* new last assistant message's active swipe — or clears everything if no
|
|
||||||
* assistant messages remain.
|
|
||||||
*/
|
|
||||||
export function onMessageDeleted() {
|
export function onMessageDeleted() {
|
||||||
if (!extensionSettings.enabled) return;
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
// console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted');
|
}
|
||||||
|
|
||||||
// Invalidate any pending or in-flight separate-mode generation so
|
// Invalidate any pending or in-flight separate-mode generation so
|
||||||
// its result is not applied to the (now-changed) chat tail.
|
// its result is not applied to the (now-changed) chat tail.
|
||||||
incrementSeparateGenerationId();
|
incrementSeparateGenerationId();
|
||||||
|
|
||||||
const currentChat = getContext().chat;
|
const currentChat = getContext().chat || [];
|
||||||
|
|
||||||
// Walk backward to find the new last assistant message.
|
|
||||||
let lastAssistantIndex = -1;
|
let lastAssistantIndex = -1;
|
||||||
for (let i = currentChat.length - 1; i >= 0; i--) {
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
@@ -526,48 +758,15 @@ export function onMessageDeleted() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastAssistantIndex === -1) {
|
syncDisplayedTrackerStateFromChat();
|
||||||
// No assistant messages remain — clear all state.
|
|
||||||
lastGeneratedData.userStats = null;
|
|
||||||
lastGeneratedData.infoBox = null;
|
|
||||||
lastGeneratedData.characterThoughts = null;
|
|
||||||
committedTrackerData.userStats = null;
|
|
||||||
committedTrackerData.infoBox = null;
|
|
||||||
committedTrackerData.characterThoughts = null;
|
|
||||||
// console.log('[RPG Companion] 🗑️ No assistant messages remain — cleared all tracker state.');
|
|
||||||
} else {
|
|
||||||
// Restore display state from the new tail message's active swipe.
|
|
||||||
// If the message has no swipe data yet, null the fields so we
|
|
||||||
// don't show stale data from the deleted message.
|
|
||||||
const hadSwipeData = syncLastGeneratedDataFromSwipeStore(currentChat);
|
|
||||||
if (!hadSwipeData) {
|
|
||||||
lastGeneratedData.userStats = null;
|
|
||||||
lastGeneratedData.infoBox = null;
|
|
||||||
lastGeneratedData.characterThoughts = null;
|
|
||||||
committedTrackerData.userStats = null;
|
|
||||||
committedTrackerData.infoBox = null;
|
|
||||||
committedTrackerData.characterThoughts = null;
|
|
||||||
// console.log('[RPG Companion] 🗑️ No swipe data for last assistant message — cleared display state.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit context from the message *before* the new tail assistant message,
|
// After the display state has been rebuilt, restore generation context from
|
||||||
// so any subsequent generation uses the correct N-1 world state.
|
// the assistant message immediately before the new tail message so the next
|
||||||
|
// generation uses the correct N-1 tracker state.
|
||||||
|
if (lastAssistantIndex !== -1) {
|
||||||
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-render all panels.
|
|
||||||
renderUserStats();
|
|
||||||
renderInfoBox();
|
|
||||||
renderThoughts();
|
|
||||||
renderInventory();
|
|
||||||
renderQuests();
|
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
|
||||||
|
|
||||||
// Update widget strips.
|
|
||||||
updateFabWidgets();
|
|
||||||
updateStripWidgets();
|
|
||||||
|
|
||||||
// Persist updated state.
|
|
||||||
saveChatData();
|
saveChatData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,550 @@
|
|||||||
|
/**
|
||||||
|
* Thought-based Character Expressions for the below-chat Present Characters panel.
|
||||||
|
*
|
||||||
|
* Derives portrait expressions from the current Present Characters thoughts
|
||||||
|
* payload, while keeping SillyTavern's native Character Expressions widget
|
||||||
|
* independent from the below-chat panel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import {
|
||||||
|
extensionSettings,
|
||||||
|
thoughtBasedExpressionPortraits,
|
||||||
|
setThoughtBasedExpressionPortraits
|
||||||
|
} from '../../core/state.js';
|
||||||
|
import {
|
||||||
|
getCurrentMessageSwipeTrackerData,
|
||||||
|
saveChatData,
|
||||||
|
setMessageSwipeTrackerField
|
||||||
|
} from '../../core/persistence.js';
|
||||||
|
import { isUsableThoughtBasedExpressionSrc } from '../../utils/thoughtBasedExpressionPortraits.js';
|
||||||
|
import {
|
||||||
|
getPresentCharactersTrackerData,
|
||||||
|
parsePresentCharacters
|
||||||
|
} from '../../utils/presentCharacters.js';
|
||||||
|
import {
|
||||||
|
classifyExpressionText,
|
||||||
|
clearExpressionsCompatibilityCache,
|
||||||
|
getExpressionClassificationSettingsSignature,
|
||||||
|
getExpressionPortraitSettingsSignature,
|
||||||
|
getExpressionsSettingsSignature,
|
||||||
|
isExpressionsExtensionEnabled,
|
||||||
|
resolveSpriteFolderNameForCharacter,
|
||||||
|
resolveExpressionPortraitForCharacter
|
||||||
|
} from '../../utils/sillyTavernExpressions.js';
|
||||||
|
|
||||||
|
const OFF_SCENE_THOUGHT_PATTERN = /\b(not\s+(currently\s+)?(in|at|present|in\s+the)\s+(the\s+)?(scene|area|room|location|vicinity))\b|\b(off[\s-]?scene)\b|\b(not\s+present)\b|\b(absent)\b|\b(away\s+from\s+(the\s+)?scene)\b/i;
|
||||||
|
const CHAT_CHANGE_RETRY_DELAYS = [0, 80, 220, 500];
|
||||||
|
const REFRESH_DEBOUNCE_DELAY = 80;
|
||||||
|
const THOUGHT_BASED_EXPRESSIONS_CACHE_VERSION = 1;
|
||||||
|
const THOUGHT_BASED_EXPRESSIONS_CACHE_FIELD = 'thoughtBasedExpressions';
|
||||||
|
|
||||||
|
let hiddenExpressionStyleElement = null;
|
||||||
|
let thoughtBasedExpressionsRefreshHandler = null;
|
||||||
|
let scheduledRefreshTimer = null;
|
||||||
|
let activeRefreshRunId = 0;
|
||||||
|
let lastCompletedRefreshSignature = null;
|
||||||
|
let lastExpressionSettingsSignature = null;
|
||||||
|
|
||||||
|
function normalizeName(name) {
|
||||||
|
return String(name || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldHideNativeExpressionDisplay() {
|
||||||
|
return extensionSettings.enabled === true && extensionSettings.hideDefaultExpressionDisplay === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseThoughtBasedExpressions() {
|
||||||
|
return extensionSettings.enabled === true
|
||||||
|
&& extensionSettings.enableThoughtBasedExpressions === true
|
||||||
|
&& extensionSettings.showAlternatePresentCharactersPanel === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyThoughtBasedExpressionsConsumers() {
|
||||||
|
thoughtBasedExpressionsRefreshHandler?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHideStyleCss() {
|
||||||
|
return `
|
||||||
|
#expression-image,
|
||||||
|
#expression-holder,
|
||||||
|
.expression-holder,
|
||||||
|
[data-expression-container],
|
||||||
|
#expression-image img,
|
||||||
|
#expression-holder img,
|
||||||
|
.expression-holder img,
|
||||||
|
[data-expression-container] img {
|
||||||
|
position: absolute !important;
|
||||||
|
left: -10000px !important;
|
||||||
|
top: 0 !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNativeExpressionDisplay() {
|
||||||
|
if (hiddenExpressionStyleElement?.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleElement = document.createElement('style');
|
||||||
|
styleElement.id = 'rpg-hidden-native-expression-display-style';
|
||||||
|
styleElement.textContent = getHideStyleCss();
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
hiddenExpressionStyleElement = styleElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNativeExpressionDisplay() {
|
||||||
|
if (hiddenExpressionStyleElement?.isConnected) {
|
||||||
|
hiddenExpressionStyleElement.remove();
|
||||||
|
} else {
|
||||||
|
document.getElementById('rpg-hidden-native-expression-display-style')?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
hiddenExpressionStyleElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNativeExpressionDisplayVisibility() {
|
||||||
|
if (shouldHideNativeExpressionDisplay()) {
|
||||||
|
hideNativeExpressionDisplay();
|
||||||
|
} else {
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearScheduledRefresh() {
|
||||||
|
if (scheduledRefreshTimer !== null) {
|
||||||
|
clearTimeout(scheduledRefreshTimer);
|
||||||
|
scheduledRefreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableStringify(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return `[${value.map(item => stableStringify(item)).join(',')}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeThoughtPayload(payload) {
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload === 'object') {
|
||||||
|
return stableStringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload !== 'string') {
|
||||||
|
return String(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = payload.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return stableStringify(JSON.parse(trimmed));
|
||||||
|
} catch {
|
||||||
|
return trimmed.replace(/\r\n/g, '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExpressionLabel(label) {
|
||||||
|
return String(label || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function arePortraitMapsEqual(left, right) {
|
||||||
|
const leftKeys = Object.keys(left);
|
||||||
|
const rightKeys = Object.keys(right);
|
||||||
|
|
||||||
|
if (leftKeys.length !== rightKeys.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftKeys.every(key => left[key] === right[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyThoughtBasedExpressionPortraits(nextPortraits) {
|
||||||
|
if (arePortraitMapsEqual(thoughtBasedExpressionPortraits, nextPortraits)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThoughtBasedExpressionPortraits(nextPortraits);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function purgeInvalidThoughtBasedExpressionPortraits() {
|
||||||
|
const nextPortraits = {};
|
||||||
|
|
||||||
|
for (const [characterName, src] of Object.entries(thoughtBasedExpressionPortraits)) {
|
||||||
|
if (isUsableThoughtBasedExpressionSrc(src)) {
|
||||||
|
nextPortraits[characterName] = src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyThoughtBasedExpressionPortraits(nextPortraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageThoughtPayload(message) {
|
||||||
|
if (!message || message.is_user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeData = getCurrentMessageSwipeTrackerData(message);
|
||||||
|
return normalizeThoughtPayload(swipeData?.characterThoughts ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findThoughtSourceMessageInfo(characterThoughtsData) {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
const currentThoughts = normalizeThoughtPayload(characterThoughtsData);
|
||||||
|
let fallback = null;
|
||||||
|
|
||||||
|
for (let i = chatMessages.length - 1; i >= 0; i--) {
|
||||||
|
const message = chatMessages[i];
|
||||||
|
if (!message || message.is_user || message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeData = getCurrentMessageSwipeTrackerData(message);
|
||||||
|
if (!swipeData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceInfo = {
|
||||||
|
message,
|
||||||
|
messageIndex: i,
|
||||||
|
swipeId: Number(message.swipe_id ?? 0),
|
||||||
|
swipeData
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fallback) {
|
||||||
|
fallback = sourceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageThoughts = getMessageThoughtPayload(message);
|
||||||
|
if (currentThoughts && messageThoughts === currentThoughts) {
|
||||||
|
return sourceInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentThoughts ? null : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isThoughtBasedExpressionsCache(candidate) {
|
||||||
|
return !!(
|
||||||
|
candidate
|
||||||
|
&& typeof candidate === 'object'
|
||||||
|
&& !Array.isArray(candidate)
|
||||||
|
&& candidate.version === THOUGHT_BASED_EXPRESSIONS_CACHE_VERSION
|
||||||
|
&& candidate.entries
|
||||||
|
&& typeof candidate.entries === 'object'
|
||||||
|
&& !Array.isArray(candidate.entries)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSwipeThoughtBasedExpressionsCache(sourceInfo) {
|
||||||
|
const directCache = sourceInfo?.swipeData?.[THOUGHT_BASED_EXPRESSIONS_CACHE_FIELD];
|
||||||
|
return isThoughtBasedExpressionsCache(directCache) ? directCache : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areThoughtBasedExpressionsCachesEqual(left, right) {
|
||||||
|
return stableStringify(left) === stableStringify(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThoughtBasedExpressionEntries(characterThoughtsData) {
|
||||||
|
const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts;
|
||||||
|
if (thoughtsConfig?.enabled === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!characterThoughtsData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentCharacters = parsePresentCharacters(characterThoughtsData);
|
||||||
|
return presentCharacters
|
||||||
|
.map(character => ({
|
||||||
|
name: String(character?.name || '').trim(),
|
||||||
|
thought: String(character?.ThoughtsContent || '').trim()
|
||||||
|
}))
|
||||||
|
.filter(character => character.name && character.thought && !OFF_SCENE_THOUGHT_PATTERN.test(character.thought));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRefreshSignature(thoughtEntries, expressionsSettingsSignature) {
|
||||||
|
return JSON.stringify({
|
||||||
|
expressionsSettingsSignature,
|
||||||
|
thoughtEntries: thoughtEntries.map(entry => ({
|
||||||
|
name: normalizeName(entry.name),
|
||||||
|
thought: entry.thought,
|
||||||
|
spriteFolderName: resolveSpriteFolderNameForCharacter(entry.name)
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshThoughtBasedExpressions({ force = false } = {}) {
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldUseThoughtBasedExpressions()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
const portraitsChanged = applyThoughtBasedExpressionPortraits({});
|
||||||
|
if (portraitsChanged) {
|
||||||
|
saveChatData();
|
||||||
|
}
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressionsSettingsSignature = getExpressionsSettingsSignature();
|
||||||
|
if (expressionsSettingsSignature !== lastExpressionSettingsSignature) {
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
lastExpressionSettingsSignature = expressionsSettingsSignature;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characterThoughtsData = getPresentCharactersTrackerData({ useCommittedFallback: true });
|
||||||
|
const thoughtEntries = getThoughtBasedExpressionEntries(characterThoughtsData);
|
||||||
|
const refreshSignature = buildRefreshSignature(thoughtEntries, expressionsSettingsSignature);
|
||||||
|
if (!force && refreshSignature === lastCompletedRefreshSignature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceInfo = findThoughtSourceMessageInfo(characterThoughtsData);
|
||||||
|
const cachedThoughtBasedExpressions = getSwipeThoughtBasedExpressionsCache(sourceInfo);
|
||||||
|
const cachedEntries = cachedThoughtBasedExpressions?.entries && typeof cachedThoughtBasedExpressions.entries === 'object' && !Array.isArray(cachedThoughtBasedExpressions.entries)
|
||||||
|
? cachedThoughtBasedExpressions.entries
|
||||||
|
: {};
|
||||||
|
const currentThoughtsSignature = normalizeThoughtPayload(characterThoughtsData);
|
||||||
|
const classificationSettingsSignature = getExpressionClassificationSettingsSignature();
|
||||||
|
const portraitSettingsSignature = getExpressionPortraitSettingsSignature();
|
||||||
|
const runId = ++activeRefreshRunId;
|
||||||
|
const nextPortraits = {};
|
||||||
|
const nextCacheEntries = {};
|
||||||
|
|
||||||
|
for (const entry of thoughtEntries) {
|
||||||
|
const portraitKey = normalizeName(entry.name);
|
||||||
|
if (!portraitKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spriteFolderName = resolveSpriteFolderNameForCharacter(entry.name);
|
||||||
|
const cachedEntry = cachedEntries[portraitKey] && typeof cachedEntries[portraitKey] === 'object'
|
||||||
|
? cachedEntries[portraitKey]
|
||||||
|
: null;
|
||||||
|
const previousSrc = nextPortraits[portraitKey] || thoughtBasedExpressionPortraits[portraitKey] || null;
|
||||||
|
const canReuseExpression = cachedEntry
|
||||||
|
&& cachedEntry.thought === entry.thought
|
||||||
|
&& cachedEntry.classificationSettingsSignature === classificationSettingsSignature
|
||||||
|
&& cachedEntry.spriteFolderName === spriteFolderName
|
||||||
|
&& typeof cachedEntry.expression === 'string';
|
||||||
|
|
||||||
|
const expression = canReuseExpression
|
||||||
|
? normalizeExpressionLabel(cachedEntry.expression)
|
||||||
|
: normalizeExpressionLabel(await classifyExpressionText(entry.thought, { characterName: entry.name }));
|
||||||
|
if (runId !== activeRefreshRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canReusePortrait = cachedEntry
|
||||||
|
&& cachedEntry.thought === entry.thought
|
||||||
|
&& cachedEntry.expression === expression
|
||||||
|
&& cachedEntry.portraitSettingsSignature === portraitSettingsSignature
|
||||||
|
&& cachedEntry.spriteFolderName === spriteFolderName
|
||||||
|
&& cachedEntry.portraitResolved === true;
|
||||||
|
|
||||||
|
const portraitSrc = canReusePortrait
|
||||||
|
? (isUsableThoughtBasedExpressionSrc(cachedEntry.portraitSrc) ? cachedEntry.portraitSrc : null)
|
||||||
|
: await resolveExpressionPortraitForCharacter(entry.name, expression, { previousSrc });
|
||||||
|
if (runId !== activeRefreshRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUsableThoughtBasedExpressionSrc(portraitSrc)) {
|
||||||
|
nextPortraits[portraitKey] = portraitSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCacheEntries[portraitKey] = {
|
||||||
|
name: entry.name,
|
||||||
|
thought: entry.thought,
|
||||||
|
spriteFolderName,
|
||||||
|
classificationSettingsSignature,
|
||||||
|
portraitSettingsSignature,
|
||||||
|
expression,
|
||||||
|
portraitSrc: isUsableThoughtBasedExpressionSrc(portraitSrc) ? portraitSrc : null,
|
||||||
|
portraitResolved: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runId !== activeRefreshRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cacheChanged = false;
|
||||||
|
if (sourceInfo) {
|
||||||
|
const nextCache = {
|
||||||
|
version: THOUGHT_BASED_EXPRESSIONS_CACHE_VERSION,
|
||||||
|
thoughtsSignature: currentThoughtsSignature,
|
||||||
|
entries: nextCacheEntries
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!areThoughtBasedExpressionsCachesEqual(cachedThoughtBasedExpressions, nextCache)) {
|
||||||
|
setMessageSwipeTrackerField(sourceInfo.message, sourceInfo.swipeId, THOUGHT_BASED_EXPRESSIONS_CACHE_FIELD, nextCache);
|
||||||
|
cacheChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCompletedRefreshSignature = refreshSignature;
|
||||||
|
const portraitsChanged = applyThoughtBasedExpressionPortraits(nextPortraits);
|
||||||
|
if (portraitsChanged || cacheChanged) {
|
||||||
|
saveChatData();
|
||||||
|
}
|
||||||
|
if (portraitsChanged) {
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setThoughtBasedExpressionsRefreshHandler(handler) {
|
||||||
|
thoughtBasedExpressionsRefreshHandler = typeof handler === 'function' ? handler : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queueThoughtBasedExpressionsUpdate({ immediate = false, force = false } = {}) {
|
||||||
|
clearScheduledRefresh();
|
||||||
|
|
||||||
|
const runRefresh = () => {
|
||||||
|
refreshThoughtBasedExpressions({ force }).catch(error => {
|
||||||
|
console.warn('[RPG Companion] Thought-based expressions update failed:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
runRefresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledRefreshTimer = setTimeout(() => {
|
||||||
|
scheduledRefreshTimer = null;
|
||||||
|
runRefresh();
|
||||||
|
}, REFRESH_DEBOUNCE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initThoughtBasedExpressions() {
|
||||||
|
const purged = purgeInvalidThoughtBasedExpressionPortraits();
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (purged) {
|
||||||
|
saveChatData();
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUseThoughtBasedExpressions()) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onThoughtBasedExpressionsChatChanged() {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScheduledRefresh();
|
||||||
|
activeRefreshRunId += 1;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
|
||||||
|
const purged = purgeInvalidThoughtBasedExpressionPortraits();
|
||||||
|
if (purged) {
|
||||||
|
saveChatData();
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const delay of CHAT_CHANGE_RETRY_DELAYS) {
|
||||||
|
setTimeout(() => {
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
if (shouldUseThoughtBasedExpressions()) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
} else {
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onThoughtBasedExpressionsSettingChanged(enabled) {
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
const purged = purgeInvalidThoughtBasedExpressionPortraits();
|
||||||
|
if (purged) {
|
||||||
|
saveChatData();
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUseThoughtBasedExpressions()) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
} else {
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScheduledRefresh();
|
||||||
|
activeRefreshRunId += 1;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onAlternatePresentCharactersVisibilityChanged() {
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (shouldUseThoughtBasedExpressions()) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScheduledRefresh();
|
||||||
|
activeRefreshRunId += 1;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onHideDefaultExpressionDisplaySettingChanged(enabled) {
|
||||||
|
extensionSettings.hideDefaultExpressionDisplay = enabled === true;
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
setTimeout(() => updateNativeExpressionDisplayVisibility(), 0);
|
||||||
|
setTimeout(() => updateNativeExpressionDisplayVisibility(), 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearThoughtBasedExpressionsCache() {
|
||||||
|
clearScheduledRefresh();
|
||||||
|
activeRefreshRunId += 1;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
$infoBoxContainer
|
$infoBoxContainer
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData } from '../../core/persistence.js';
|
import { saveChatData, setMessageSwipeTrackerField } from '../../core/persistence.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { isItemLocked } from '../generation/lockManager.js';
|
import { isItemLocked } from '../generation/lockManager.js';
|
||||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
@@ -989,7 +989,7 @@ export function updateInfoBoxField(field, value) {
|
|||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||||
message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n');
|
setMessageSwipeTrackerField(message, swipeId, 'infoBox', updatedLines.join('\n'));
|
||||||
// console.log('[RPG Companion] Updated infoBox in message swipe data');
|
// console.log('[RPG Companion] Updated infoBox in message swipe data');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1074,7 +1074,7 @@ function updateRecentEvent(field, value) {
|
|||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||||
message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n');
|
setMessageSwipeTrackerField(message, swipeId, 'infoBox', updatedLines.join('\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
+322
-208
@@ -4,20 +4,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { this_chid, characters } from '../../../../../../../script.js';
|
|
||||||
import { selected_group, getGroupMembers } from '../../../../../../group-chats.js';
|
|
||||||
import {
|
import {
|
||||||
extensionSettings,
|
extensionSettings,
|
||||||
lastGeneratedData,
|
lastGeneratedData,
|
||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
$thoughtsContainer,
|
$thoughtsContainer,
|
||||||
FALLBACK_AVATAR_DATA_URI,
|
|
||||||
addDebugLog
|
addDebugLog
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { saveChatData, saveSettings } from '../../core/persistence.js';
|
import { saveChatData, saveSettings, getCurrentMessageSwipeTrackerData, setMessageSwipeTrackerField } from '../../core/persistence.js';
|
||||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
import {
|
||||||
|
stripBrackets,
|
||||||
|
extractFieldValue,
|
||||||
|
toSnakeCase,
|
||||||
|
getPresentCharactersTrackerData,
|
||||||
|
resolvePresentCharacterPortrait
|
||||||
|
} from '../../utils/presentCharacters.js';
|
||||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||||
|
import { renderAlternatePresentCharacters } from '../ui/alternatePresentCharacters.js';
|
||||||
|
import { queueThoughtBasedExpressionsUpdate } from '../integration/thoughtBasedExpressions.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to generate lock icon HTML if setting is enabled
|
* Helper to generate lock icon HTML if setting is enabled
|
||||||
@@ -81,80 +86,15 @@ function getStatColor(percentage, lowColor, highColor, lowOpacity = 100, highOpa
|
|||||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips leading and trailing square brackets from a string value.
|
|
||||||
* Used to clean placeholder notation that AI might include in responses.
|
|
||||||
* @param {string} value - The value to clean
|
|
||||||
* @returns {string} Cleaned value without surrounding brackets
|
|
||||||
*/
|
|
||||||
function stripBrackets(value) {
|
|
||||||
if (typeof value !== 'string') return value;
|
|
||||||
return value.replace(/^\[|\]$/g, '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the actual value from a field that might be locked.
|
|
||||||
* If the field is an object with {value, locked}, returns the value.
|
|
||||||
* Otherwise returns the field as-is.
|
|
||||||
* @param {any} fieldValue - The field value (might be string or {value, locked} object)
|
|
||||||
* @returns {string} The actual string value
|
|
||||||
*/
|
|
||||||
function extractFieldValue(fieldValue) {
|
|
||||||
if (fieldValue && typeof fieldValue === 'object' && 'value' in fieldValue) {
|
|
||||||
return fieldValue.value || '';
|
|
||||||
}
|
|
||||||
return fieldValue || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a field name to snake_case for use as JSON key
|
|
||||||
* Example: "Test Tracker" -> "test_tracker"
|
|
||||||
* @param {string} name - Field name to convert
|
|
||||||
* @returns {string} snake_case version
|
|
||||||
*/
|
|
||||||
function toSnakeCase(name) {
|
|
||||||
return name
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
|
||||||
.replace(/^_+|_+$/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fuzzy name matching that handles:
|
|
||||||
* - Exact matches: "Sabrina" === "Sabrina"
|
|
||||||
* - Parenthetical additions: "Sabrina" matches "Sabrina (Margrokha's Avatar)"
|
|
||||||
* - Title additions: "Sabrina" matches "Princess Sabrina"
|
|
||||||
* - Word boundaries: "Sabrina" won't match "Sabrina's Mother"
|
|
||||||
*
|
|
||||||
* @param {string} cardName - Name from the character card
|
|
||||||
* @param {string} aiName - Name generated by the AI
|
|
||||||
* @returns {boolean} True if names match
|
|
||||||
*/
|
|
||||||
function namesMatch(cardName, aiName) {
|
|
||||||
if (!cardName || !aiName) return false;
|
|
||||||
|
|
||||||
// 1. Exact match (fast path)
|
|
||||||
if (cardName.toLowerCase() === aiName.toLowerCase()) return true;
|
|
||||||
|
|
||||||
// 2. Strip parentheses and match
|
|
||||||
const stripParens = (s) => s.replace(/\s*\([^)]*\)/g, '').trim();
|
|
||||||
const cardCore = stripParens(cardName).toLowerCase();
|
|
||||||
const aiCore = stripParens(aiName).toLowerCase();
|
|
||||||
if (cardCore === aiCore) return true;
|
|
||||||
|
|
||||||
// 3. Check if card name appears as complete word in AI name
|
|
||||||
// Escape special regex characters to prevent "Invalid regular expression" errors
|
|
||||||
const escapedCardCore = cardCore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const wordBoundary = new RegExp(`\\b${escapedCardCore}\\b`);
|
|
||||||
return wordBoundary.test(aiCore);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders character thoughts (Present Characters) panel.
|
* Renders character thoughts (Present Characters) panel.
|
||||||
* Displays character cards with avatars, relationship badges, and traits.
|
* Displays character cards with avatars, relationship badges, and traits.
|
||||||
* Includes event listeners for editable character fields.
|
* Includes event listeners for editable character fields.
|
||||||
*/
|
*/
|
||||||
export function renderThoughts({ preserveScroll = false } = {}) {
|
export function renderThoughts({ preserveScroll = false, useCommittedFallback = true } = {}) {
|
||||||
|
renderAlternatePresentCharacters({ useCommittedFallback });
|
||||||
|
queueThoughtBasedExpressionsUpdate();
|
||||||
|
|
||||||
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
|
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -169,7 +109,7 @@ export function renderThoughts({ preserveScroll = false } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't render if no data exists (e.g., after cache clear)
|
// Don't render if no data exists (e.g., after cache clear)
|
||||||
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
|
const thoughtsData = getPresentCharactersTrackerData({ useCommittedFallback });
|
||||||
if (!thoughtsData) {
|
if (!thoughtsData) {
|
||||||
$thoughtsContainer.html('<div class="rpg-inventory-empty">' + (i18n.getTranslation('thoughts.empty') || 'No character data generated yet') + '</div>');
|
$thoughtsContainer.html('<div class="rpg-inventory-empty">' + (i18n.getTranslation('thoughts.empty') || 'No character data generated yet') + '</div>');
|
||||||
return;
|
return;
|
||||||
@@ -193,7 +133,7 @@ export function renderThoughts({ preserveScroll = false } = {}) {
|
|||||||
const hasRelationshipEnabled = relationshipFields.length > 0;
|
const hasRelationshipEnabled = relationshipFields.length > 0;
|
||||||
|
|
||||||
// Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh)
|
// Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh)
|
||||||
const characterThoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts || '';
|
const characterThoughtsData = getPresentCharactersTrackerData({ useCommittedFallback });
|
||||||
|
|
||||||
// console.log('[RPG Companion] renderThoughts - Reading from lastGeneratedData:', JSON.stringify(lastGeneratedData.characterThoughts));
|
// console.log('[RPG Companion] renderThoughts - Reading from lastGeneratedData:', JSON.stringify(lastGeneratedData.characterThoughts));
|
||||||
// console.log('[RPG Companion] renderThoughts - Reading from committedTrackerData:', JSON.stringify(committedTrackerData.characterThoughts));
|
// console.log('[RPG Companion] renderThoughts - Reading from committedTrackerData:', JSON.stringify(committedTrackerData.characterThoughts));
|
||||||
@@ -416,70 +356,8 @@ export function renderThoughts({ preserveScroll = false } = {}) {
|
|||||||
try {
|
try {
|
||||||
debugLog(`[RPG Thoughts] Building HTML for character ${characterIndex}/${presentCharacters.length}:`, char.name);
|
debugLog(`[RPG Thoughts] Building HTML for character ${characterIndex}/${presentCharacters.length}:`, char.name);
|
||||||
|
|
||||||
// Find character portrait
|
|
||||||
// Use a base64-encoded SVG placeholder as fallback to avoid 400 errors
|
|
||||||
let characterPortrait = FALLBACK_AVATAR_DATA_URI;
|
|
||||||
|
|
||||||
debugLog(`[RPG Thoughts] Looking up avatar for: ${char.name}`);
|
debugLog(`[RPG Thoughts] Looking up avatar for: ${char.name}`);
|
||||||
|
const characterPortrait = resolvePresentCharacterPortrait(char.name);
|
||||||
// First, check if user manually uploaded a custom avatar
|
|
||||||
if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[char.name]) {
|
|
||||||
characterPortrait = extensionSettings.npcAvatars[char.name];
|
|
||||||
debugLog('[RPG Thoughts] Found custom uploaded avatar');
|
|
||||||
}
|
|
||||||
|
|
||||||
// For group chats, search through group members
|
|
||||||
if (characterPortrait === FALLBACK_AVATAR_DATA_URI && selected_group) {
|
|
||||||
debugLog('[RPG Thoughts] In group chat, checking group members...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const groupMembers = getGroupMembers(selected_group);
|
|
||||||
debugLog('[RPG Thoughts] Group members count:', groupMembers ? groupMembers.length : 0);
|
|
||||||
|
|
||||||
if (groupMembers && groupMembers.length > 0) {
|
|
||||||
const matchingMember = groupMembers.find(member =>
|
|
||||||
member && member.name && namesMatch(member.name, char.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') {
|
|
||||||
const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingMember.avatar);
|
|
||||||
if (thumbnailUrl) {
|
|
||||||
characterPortrait = thumbnailUrl;
|
|
||||||
debugLog('[RPG Thoughts] Found avatar in group members');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (groupError) {
|
|
||||||
debugLog('[RPG Thoughts] Error checking group members:', groupError.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For regular chats or if not found in group, search all characters
|
|
||||||
if (characterPortrait === FALLBACK_AVATAR_DATA_URI && characters && characters.length > 0) {
|
|
||||||
debugLog('[RPG Thoughts] Searching all characters...');
|
|
||||||
|
|
||||||
const matchingCharacter = characters.find(c =>
|
|
||||||
c && c.name && namesMatch(c.name, char.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') {
|
|
||||||
const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingCharacter.avatar);
|
|
||||||
if (thumbnailUrl) {
|
|
||||||
characterPortrait = thumbnailUrl;
|
|
||||||
debugLog('[RPG Thoughts] Found avatar in all characters');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the current character in a 1-on-1 chat, use their portrait
|
|
||||||
if (this_chid !== undefined && characters[this_chid] &&
|
|
||||||
characters[this_chid].name && namesMatch(characters[this_chid].name, char.name)) {
|
|
||||||
const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar);
|
|
||||||
if (thumbnailUrl) {
|
|
||||||
characterPortrait = thumbnailUrl;
|
|
||||||
debugLog('[RPG Thoughts] Found avatar from current character');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog(`[RPG Thoughts] Final avatar for ${char.name}:`, characterPortrait.substring(0, 50) + '...');
|
debugLog(`[RPG Thoughts] Final avatar for ${char.name}:`, characterPortrait.substring(0, 50) + '...');
|
||||||
|
|
||||||
@@ -632,7 +510,9 @@ export function renderThoughts({ preserveScroll = false } = {}) {
|
|||||||
|
|
||||||
// Update icon
|
// Update icon
|
||||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||||
const newTitle = !currentlyLocked ? 'Locked' : 'Unlocked';
|
const newTitle = !currentlyLocked
|
||||||
|
? (i18n.getTranslation('thoughts.locked') || 'Locked')
|
||||||
|
: (i18n.getTranslation('thoughts.unlocked') || 'Unlocked');
|
||||||
$icon.text(newIcon);
|
$icon.text(newIcon);
|
||||||
$icon.attr('title', newTitle);
|
$icon.attr('title', newTitle);
|
||||||
|
|
||||||
@@ -839,7 +719,7 @@ export function removeCharacter(characterName) {
|
|||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||||
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lastGeneratedData.characterThoughts;
|
setMessageSwipeTrackerField(message, swipeId, 'characterThoughts', lastGeneratedData.characterThoughts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -969,7 +849,7 @@ export function addNewCharacter() {
|
|||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||||
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lastGeneratedData.characterThoughts;
|
setMessageSwipeTrackerField(message, swipeId, 'characterThoughts', lastGeneratedData.characterThoughts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1152,7 +1032,7 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||||
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lastGeneratedData.characterThoughts;
|
setMessageSwipeTrackerField(message, swipeId, 'characterThoughts', lastGeneratedData.characterThoughts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1381,7 +1261,7 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||||
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lines.join('\n');
|
setMessageSwipeTrackerField(message, swipeId, 'characterThoughts', lines.join('\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1405,6 +1285,10 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
// console.log('[RPG Companion] Is editing thoughts?', isEditingThoughts, 'Field:', field, 'Thoughts field name:', thoughtsFieldName);
|
// console.log('[RPG Companion] Is editing thoughts?', isEditingThoughts, 'Field:', field, 'Thoughts field name:', thoughtsFieldName);
|
||||||
// console.log('[RPG Companion] After update - lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts);
|
// console.log('[RPG Companion] After update - lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts);
|
||||||
|
|
||||||
|
if (field === 'name' || isEditingThoughts) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (isEditingThoughts && extensionSettings.showThoughtsInChat) {
|
if (isEditingThoughts && extensionSettings.showThoughtsInChat) {
|
||||||
// console.log('[RPG Companion] Updating chat thought bubbles');
|
// console.log('[RPG Companion] Updating chat thought bubbles');
|
||||||
// Update chat thought bubbles when thoughts are edited
|
// Update chat thought bubbles when thoughts are edited
|
||||||
@@ -1438,66 +1322,265 @@ function renderThoughtsSidebarOnly() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates or removes thought overlays in the chat.
|
* Updates or removes thoughts shown in chat.
|
||||||
* Creates floating thought bubbles positioned near character avatars.
|
* Renders either the original corner bubbles or inline dropdown cards.
|
||||||
*/
|
*/
|
||||||
export function updateChatThoughts() {
|
|
||||||
// console.log('[RPG Companion] ======== updateChatThoughts called ========');
|
|
||||||
// console.log('[RPG Companion] Extension enabled:', extensionSettings.enabled);
|
|
||||||
// console.log('[RPG Companion] showThoughtsInChat setting:', extensionSettings.showThoughtsInChat);
|
|
||||||
// console.log('[RPG Companion] Toggle element checked:', $('#rpg-toggle-thoughts-in-chat').prop('checked'));
|
|
||||||
// console.log('[RPG Companion] lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts);
|
|
||||||
|
|
||||||
// Remove existing thought panel and icon
|
let inlineThoughtsObserver = null;
|
||||||
|
let inlineThoughtsRefreshTimeout = null;
|
||||||
|
let isRefreshingInlineThoughts = false;
|
||||||
|
|
||||||
|
export function updateChatThoughts(attempt = 0) {
|
||||||
|
const thoughtsStyle = extensionSettings.thoughtsInChatStyle || 'corner';
|
||||||
|
const openInlineThoughts = getOpenInlineThoughts();
|
||||||
|
|
||||||
|
// Remove old floating thought panel/icon (legacy cleanup)
|
||||||
$('#rpg-thought-panel').remove();
|
$('#rpg-thought-panel').remove();
|
||||||
$('#rpg-thought-icon').remove();
|
$('#rpg-thought-icon').remove();
|
||||||
$('#chat').off('scroll.thoughtPanel');
|
$('#chat').off('scroll.thoughtPanel');
|
||||||
$(window).off('resize.thoughtPanel');
|
$(window).off('resize.thoughtPanel');
|
||||||
$(document).off('click.thoughtPanel');
|
$(document).off('click.thoughtPanel');
|
||||||
|
|
||||||
// If extension is disabled, thoughts in chat are disabled, or no thoughts, just return
|
// Remove any existing inline thought dropdowns from previous renders
|
||||||
if (!extensionSettings.enabled || !extensionSettings.showThoughtsInChat || !lastGeneratedData.characterThoughts) {
|
$('.rpg-inline-thoughts, .rpg-inline-thought').remove();
|
||||||
// console.log('[RPG Companion] Thoughts in chat disabled or no data');
|
|
||||||
|
const canRenderThoughts = extensionSettings.enabled
|
||||||
|
&& extensionSettings.showThoughtsInChat
|
||||||
|
&& !!lastGeneratedData.characterThoughts;
|
||||||
|
|
||||||
|
if (!canRenderThoughts) {
|
||||||
|
teardownInlineThoughtsObserver();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the Present Characters data to get thoughts
|
const thoughtsArray = parseThoughtsArray();
|
||||||
let thoughtsArray = []; // Array of {name, emoji, thought}
|
|
||||||
|
if (thoughtsArray.length === 0) {
|
||||||
|
teardownInlineThoughtsObserver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetInfo = findThoughtTargetMessage();
|
||||||
|
let $targetMessage = targetInfo.$message;
|
||||||
|
|
||||||
|
if ((!$targetMessage || !$targetMessage.length || !$targetMessage.find('.mes_text').length) && attempt < 10) {
|
||||||
|
setTimeout(() => updateChatThoughts(attempt + 1), 120);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$targetMessage || !$targetMessage.length) {
|
||||||
|
teardownInlineThoughtsObserver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thoughtsStyle === 'inline') {
|
||||||
|
insertInlineThoughts($targetMessage, thoughtsArray, openInlineThoughts);
|
||||||
|
ensureInlineThoughtsObserver();
|
||||||
|
} else {
|
||||||
|
teardownInlineThoughtsObserver();
|
||||||
|
createThoughtPanel($targetMessage, thoughtsArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findThoughtTargetMessage() {
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context?.chat || [];
|
||||||
|
const currentThoughts = normalizeThoughtPayload(lastGeneratedData.characterThoughts);
|
||||||
|
|
||||||
|
let fallbackIndex = -1;
|
||||||
|
|
||||||
|
// Match the currently displayed thoughts against stored swipe payloads so the
|
||||||
|
// UI stays attached to the visible assistant reply after swipes, deletes, and reloads.
|
||||||
|
for (let i = chat.length - 1; i >= 0; i--) {
|
||||||
|
const message = chat[i];
|
||||||
|
if (message?.is_user) continue;
|
||||||
|
|
||||||
|
if (fallbackIndex === -1) {
|
||||||
|
fallbackIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageThoughts = getMessageThoughtPayload(message);
|
||||||
|
if (!currentThoughts || !messageThoughts) continue;
|
||||||
|
|
||||||
|
if (messageThoughts === currentThoughts) {
|
||||||
|
const $message = $(`#chat .mes[mesid="${i}"]`);
|
||||||
|
return { index: i, $message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackIndex !== -1) {
|
||||||
|
return {
|
||||||
|
index: fallbackIndex,
|
||||||
|
$message: $(`#chat .mes[mesid="${fallbackIndex}"]`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { index: -1, $message: $() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageThoughtPayload(message) {
|
||||||
|
if (!message || message.is_user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeData = getCurrentMessageSwipeTrackerData(message);
|
||||||
|
return normalizeThoughtPayload(swipeData?.characterThoughts ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeThoughtPayload(payload) {
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload === 'object') {
|
||||||
|
return stableStringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload !== 'string') {
|
||||||
|
return String(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = payload.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return stableStringify(JSON.parse(trimmed));
|
||||||
|
} catch {
|
||||||
|
return trimmed.replace(/\r\n/g, '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableStringify(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return `[${value.map(item => stableStringify(item)).join(',')}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenInlineThoughts() {
|
||||||
|
const openThoughts = new Set();
|
||||||
|
|
||||||
|
$('.rpg-inline-thought[open]').each(function () {
|
||||||
|
const characterName = ($(this).attr('data-character') || '').trim();
|
||||||
|
if (characterName) {
|
||||||
|
openThoughts.add(characterName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return openThoughts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureInlineThoughtsObserver() {
|
||||||
|
if (inlineThoughtsObserver) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatElement = document.getElementById('chat');
|
||||||
|
if (!chatElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inlineThoughtsObserver = new MutationObserver((mutations) => {
|
||||||
|
if (isRefreshingInlineThoughts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SillyTavern rerenders message DOM after swipes, deletes, and message cleanup.
|
||||||
|
// Watch for those chat-level mutations and reattach inline thoughts once the
|
||||||
|
// target message body exists again, while ignoring our own refresh churn.
|
||||||
|
const shouldRefresh = mutations.some((mutation) => {
|
||||||
|
if (mutation.type !== 'childList') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const touchedThoughtNode = [...mutation.addedNodes, ...mutation.removedNodes].some((node) => {
|
||||||
|
if (!(node instanceof HTMLElement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.classList?.contains('mes')
|
||||||
|
|| node.classList?.contains('mes_text')
|
||||||
|
|| node.querySelector?.('.mes, .mes_text');
|
||||||
|
});
|
||||||
|
|
||||||
|
return touchedThoughtNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldRefresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(inlineThoughtsRefreshTimeout);
|
||||||
|
inlineThoughtsRefreshTimeout = setTimeout(() => {
|
||||||
|
if (!extensionSettings.enabled
|
||||||
|
|| !extensionSettings.showThoughtsInChat
|
||||||
|
|| (extensionSettings.thoughtsInChatStyle || 'corner') !== 'inline') {
|
||||||
|
teardownInlineThoughtsObserver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRefreshingInlineThoughts = true;
|
||||||
|
try {
|
||||||
|
updateChatThoughts();
|
||||||
|
} finally {
|
||||||
|
isRefreshingInlineThoughts = false;
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
inlineThoughtsObserver.observe(chatElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardownInlineThoughtsObserver() {
|
||||||
|
if (inlineThoughtsRefreshTimeout) {
|
||||||
|
clearTimeout(inlineThoughtsRefreshTimeout);
|
||||||
|
inlineThoughtsRefreshTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inlineThoughtsObserver) {
|
||||||
|
inlineThoughtsObserver.disconnect();
|
||||||
|
inlineThoughtsObserver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseThoughtsArray() {
|
||||||
|
let thoughtsArray = [];
|
||||||
const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts;
|
const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts;
|
||||||
const thoughtsLabel = thoughtsConfig?.name || 'Thoughts';
|
const thoughtsLabel = thoughtsConfig?.name || 'Thoughts';
|
||||||
|
|
||||||
// Try JSON format first
|
|
||||||
try {
|
try {
|
||||||
const parsed = typeof lastGeneratedData.characterThoughts === 'string'
|
const parsed = typeof lastGeneratedData.characterThoughts === 'string'
|
||||||
? JSON.parse(lastGeneratedData.characterThoughts)
|
? JSON.parse(lastGeneratedData.characterThoughts)
|
||||||
: lastGeneratedData.characterThoughts;
|
: lastGeneratedData.characterThoughts;
|
||||||
|
|
||||||
// Handle both {characters: [...]} and direct array formats
|
|
||||||
const charactersArray = Array.isArray(parsed) ? parsed : (parsed.characters || []);
|
const charactersArray = Array.isArray(parsed) ? parsed : (parsed.characters || []);
|
||||||
|
|
||||||
if (charactersArray.length > 0) {
|
if (charactersArray.length > 0) {
|
||||||
// Extract thoughts from JSON character objects
|
const offScene = /\b(not\s+(currently\s+)?(in|at|present|in\s+the)\s+(the\s+)?(scene|area|room|location|vicinity))\b|\b(off[\s-]?scene)\b|\b(not\s+present)\b|\b(absent)\b|\b(away\s+from\s+(the\s+)?scene)\b/i;
|
||||||
thoughtsArray = charactersArray
|
thoughtsArray = charactersArray
|
||||||
.filter(char => char.thoughts && char.thoughts.content)
|
.filter(char => char.thoughts && char.thoughts.content && !offScene.test(char.thoughts.content))
|
||||||
.map(char => ({
|
.map(char => ({
|
||||||
name: (char.name || '').toLowerCase(),
|
name: (char.name || ''),
|
||||||
emoji: char.emoji || '👤',
|
emoji: char.emoji || '👤',
|
||||||
thought: char.thoughts.content
|
thought: char.thoughts.content
|
||||||
}));
|
}));
|
||||||
|
|
||||||
debugLog('[RPG Thoughts Bubble] ✓ Parsed JSON format, thoughts:', thoughtsArray.length);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog('[RPG Thoughts Bubble] Not JSON format, falling back to text parsing');
|
debugLog('[RPG Thoughts Bubble] Not JSON format, falling back to text parsing');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If JSON parsing failed or returned empty, try text format
|
if (thoughtsArray.length === 0 && lastGeneratedData.characterThoughts) {
|
||||||
if (thoughtsArray.length === 0) {
|
|
||||||
const lines = lastGeneratedData.characterThoughts.split('\n');
|
const lines = lastGeneratedData.characterThoughts.split('\n');
|
||||||
|
|
||||||
// console.log('[RPG Companion] Parsing thoughts from lines:', lines);
|
|
||||||
|
|
||||||
// Parse new format to build character map and thoughts
|
|
||||||
let currentCharName = null;
|
let currentCharName = null;
|
||||||
let currentCharEmoji = null;
|
let currentCharEmoji = null;
|
||||||
|
|
||||||
@@ -1513,74 +1596,103 @@ export function updateChatThoughts() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a character name line (starts with "- ")
|
|
||||||
if (line.startsWith('- ')) {
|
if (line.startsWith('- ')) {
|
||||||
const name = line.substring(2).trim();
|
const name = line.substring(2).trim();
|
||||||
if (name && name.toLowerCase() !== 'unavailable') {
|
if (name && name.toLowerCase() !== 'unavailable') {
|
||||||
currentCharName = name;
|
currentCharName = name;
|
||||||
currentCharEmoji = null; // Reset emoji for new character
|
currentCharEmoji = null;
|
||||||
} else {
|
} else {
|
||||||
currentCharName = null;
|
currentCharName = null;
|
||||||
currentCharEmoji = null;
|
currentCharEmoji = null;
|
||||||
}
|
}
|
||||||
}
|
} else if (line.startsWith('Details:') && currentCharName) {
|
||||||
// Check if this is a Details line (contains the emoji)
|
|
||||||
else if (line.startsWith('Details:') && currentCharName) {
|
|
||||||
const detailsContent = line.substring(line.indexOf(':') + 1).trim();
|
const detailsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||||
const parts = detailsContent.split('|').map(p => p.trim());
|
const parts = detailsContent.split('|').map(p => p.trim());
|
||||||
|
|
||||||
// First part is the emoji
|
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
currentCharEmoji = parts[0];
|
currentCharEmoji = parts[0];
|
||||||
}
|
}
|
||||||
}
|
} else if (line.startsWith(thoughtsLabel + ':') && currentCharName && currentCharEmoji) {
|
||||||
// Check if this is a Thoughts line
|
|
||||||
else if (line.startsWith(thoughtsLabel + ':') && currentCharName && currentCharEmoji) {
|
|
||||||
const thoughtContent = line.substring(thoughtsLabel.length + 1).trim();
|
const thoughtContent = line.substring(thoughtsLabel.length + 1).trim();
|
||||||
|
|
||||||
// The thought content is just the text (no emoji prefix in new format)
|
|
||||||
if (thoughtContent) {
|
if (thoughtContent) {
|
||||||
thoughtsArray.push({
|
thoughtsArray.push({
|
||||||
name: currentCharName.toLowerCase(),
|
name: currentCharName,
|
||||||
emoji: currentCharEmoji,
|
emoji: currentCharEmoji,
|
||||||
thought: thoughtContent
|
thought: thoughtContent
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // End of text format parsing for thoughts bubbles
|
}
|
||||||
|
|
||||||
debugLog('[RPG Thoughts] Parsed thoughts:', thoughtsArray);
|
return thoughtsArray;
|
||||||
|
}
|
||||||
|
|
||||||
// If no thoughts parsed, return
|
function escapeInlineThoughtHtml(value) {
|
||||||
if (thoughtsArray.length === 0) {
|
return String(value ?? '')
|
||||||
// console.log('[RPG Companion] No thoughts parsed, returning');
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertInlineThoughts($message, thoughtsArray, openThoughts = new Set()) {
|
||||||
|
const $mesText = $message.find('.mes_text');
|
||||||
|
if (!$mesText.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('[RPG Companion] Total thoughts:', thoughtsArray.length);
|
const thoughtsMap = {};
|
||||||
// console.log('[RPG Companion] Thoughts array:', thoughtsArray);
|
for (const thoughtData of thoughtsArray) {
|
||||||
|
thoughtsMap[(thoughtData.name || '').toLowerCase()] = thoughtData;
|
||||||
// Find the last message to position near
|
|
||||||
const $messages = $('#chat .mes');
|
|
||||||
let $targetMessage = null;
|
|
||||||
|
|
||||||
// Find the most recent non-user message
|
|
||||||
for (let i = $messages.length - 1; i >= 0; i--) {
|
|
||||||
const $message = $messages.eq(i);
|
|
||||||
if ($message.attr('is_user') !== 'true') {
|
|
||||||
$targetMessage = $message;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$targetMessage) {
|
const $container = $('<div class="rpg-inline-thoughts"></div>');
|
||||||
// console.log('[RPG Companion] No target message found');
|
bindInlineThoughtEvents($container);
|
||||||
|
|
||||||
|
for (const [, thoughtData] of Object.entries(thoughtsMap)) {
|
||||||
|
const $dropdown = createInlineThoughtDropdown(thoughtData, openThoughts);
|
||||||
|
$container.append($dropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$container.children().length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the thought panel with all thoughts
|
// Mount outside .mes_text so SillyTavern's click-to-edit handlers do not
|
||||||
createThoughtPanel($targetMessage, thoughtsArray);
|
// intercept summary clicks before the details element can toggle.
|
||||||
|
const $mediaWrapper = $message.find('.mes_media_wrapper').first();
|
||||||
|
if ($mediaWrapper.length) {
|
||||||
|
$container.insertBefore($mediaWrapper);
|
||||||
|
} else {
|
||||||
|
$container.insertAfter($mesText.last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindInlineThoughtEvents($container) {
|
||||||
|
$container.on('click mousedown touchstart', '.rpg-inline-thought, .rpg-inline-thought-summary, .rpg-inline-thought-content', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInlineThoughtDropdown(thoughtData, openThoughts = new Set()) {
|
||||||
|
const characterName = thoughtData.name || '';
|
||||||
|
const characterEmoji = thoughtData.emoji || '👤';
|
||||||
|
const thoughtText = thoughtData.thought || '';
|
||||||
|
const normalizedCharacterName = characterName.toLowerCase();
|
||||||
|
const openAttribute = openThoughts.has(normalizedCharacterName) ? ' open' : '';
|
||||||
|
|
||||||
|
return $(`
|
||||||
|
<details class="rpg-inline-thought" data-character="${escapeInlineThoughtHtml(normalizedCharacterName)}"${openAttribute}>
|
||||||
|
<summary class="rpg-inline-thought-summary">
|
||||||
|
<span class="rpg-inline-thought-icon">${escapeInlineThoughtHtml(characterEmoji)}</span>
|
||||||
|
<span class="rpg-inline-thought-name">${escapeInlineThoughtHtml(characterName)}'s thoughts</span>
|
||||||
|
</summary>
|
||||||
|
<div class="rpg-inline-thought-content">
|
||||||
|
<div class="rpg-inline-thought-text">${escapeInlineThoughtHtml(thoughtText)}</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== GLOBAL DRAGGING SETUP FOR THOUGHT ICON (MOBILE ONLY) =====
|
// ===== GLOBAL DRAGGING SETUP FOR THOUGHT ICON (MOBILE ONLY) =====
|
||||||
@@ -2326,7 +2438,9 @@ export function createThoughtPanel($message, thoughtsArray) {
|
|||||||
|
|
||||||
// Update icon
|
// Update icon
|
||||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||||
const newTitle = !currentlyLocked ? 'Locked' : 'Unlocked';
|
const newTitle = !currentlyLocked
|
||||||
|
? (i18n.getTranslation('thoughts.locked') || 'Locked')
|
||||||
|
: (i18n.getTranslation('thoughts.unlocked') || 'Unlocked');
|
||||||
$icon.text(newIcon);
|
$icon.text(newIcon);
|
||||||
$icon.attr('title', newTitle);
|
$icon.attr('title', newTitle);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { extensionSettings } from '../../core/state.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { getThoughtBasedExpressionPortraitForCharacter } from '../../utils/thoughtBasedExpressionPortraits.js';
|
||||||
|
import { getSafeImageSrc } from '../../utils/imageUrls.js';
|
||||||
|
import {
|
||||||
|
getPresentCharactersTrackerData,
|
||||||
|
parsePresentCharacters,
|
||||||
|
resolvePresentCharacterPortrait
|
||||||
|
} from '../../utils/presentCharacters.js';
|
||||||
|
|
||||||
|
const PANEL_ID = 'rpg-alt-present-characters';
|
||||||
|
|
||||||
|
function ensureAlternatePresentCharactersPanel() {
|
||||||
|
let $panel = $(`#${PANEL_ID}`);
|
||||||
|
if ($panel.length) {
|
||||||
|
return $panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel = $(`<div id="${PANEL_ID}" class="rpg-alt-present-characters" style="display:none;"></div>`);
|
||||||
|
|
||||||
|
const $sendForm = $('#send_form');
|
||||||
|
const $sheld = $('#sheld');
|
||||||
|
const $chat = $sheld.find('#chat');
|
||||||
|
|
||||||
|
if ($sendForm.length) {
|
||||||
|
$sendForm.before($panel);
|
||||||
|
} else if ($chat.length) {
|
||||||
|
$chat.after($panel);
|
||||||
|
} else if ($sheld.length) {
|
||||||
|
$sheld.append($panel);
|
||||||
|
} else {
|
||||||
|
$('body').append($panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgba(hex, opacity = 100) {
|
||||||
|
const r = parseInt(hex.slice(1, 3), 16);
|
||||||
|
const g = parseInt(hex.slice(3, 5), 16);
|
||||||
|
const b = parseInt(hex.slice(5, 7), 16);
|
||||||
|
const a = opacity / 100;
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePortraitLoadError() {
|
||||||
|
this.style.opacity = '0.5';
|
||||||
|
$(this).off('error', handlePortraitLoadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAlternatePresentCharacterCard(character) {
|
||||||
|
const rawPortrait = (extensionSettings.enableThoughtBasedExpressions
|
||||||
|
? getThoughtBasedExpressionPortraitForCharacter(character.name)
|
||||||
|
: null) || resolvePresentCharacterPortrait(character.name);
|
||||||
|
const portrait = getSafeImageSrc(rawPortrait);
|
||||||
|
const name = String(character.name || '');
|
||||||
|
|
||||||
|
const $card = $('<div class="rpg-alt-present-character"></div>')
|
||||||
|
.attr('data-character-name', name)
|
||||||
|
.attr('title', name);
|
||||||
|
|
||||||
|
const $portrait = $('<div class="rpg-alt-present-character__portrait"></div>');
|
||||||
|
const $image = $('<img />')
|
||||||
|
.attr({
|
||||||
|
alt: name,
|
||||||
|
loading: 'lazy'
|
||||||
|
})
|
||||||
|
.on('error', handlePortraitLoadError);
|
||||||
|
|
||||||
|
if (portrait) {
|
||||||
|
$image.attr('src', portrait);
|
||||||
|
}
|
||||||
|
|
||||||
|
const $meta = $('<div class="rpg-alt-present-character__meta"></div>');
|
||||||
|
const $name = $('<div class="rpg-alt-present-character__name"></div>').text(name);
|
||||||
|
|
||||||
|
$portrait.append($image);
|
||||||
|
$meta.append($name);
|
||||||
|
$card.append($portrait, $meta);
|
||||||
|
|
||||||
|
return $card;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeAlternatePresentCharactersPanel() {
|
||||||
|
$(`#${PANEL_ID}`).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncAlternatePresentCharactersTheme() {
|
||||||
|
const $panel = $(`#${PANEL_ID}`);
|
||||||
|
if (!$panel.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = extensionSettings.theme || 'default';
|
||||||
|
|
||||||
|
$panel.css({
|
||||||
|
'--rpg-bg': '',
|
||||||
|
'--rpg-accent': '',
|
||||||
|
'--rpg-text': '',
|
||||||
|
'--rpg-highlight': '',
|
||||||
|
'--rpg-border': '',
|
||||||
|
'--rpg-shadow': ''
|
||||||
|
});
|
||||||
|
|
||||||
|
if (theme === 'default') {
|
||||||
|
$panel.removeAttr('data-theme');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel.attr('data-theme', theme);
|
||||||
|
|
||||||
|
if (theme === 'custom') {
|
||||||
|
const colors = extensionSettings.customColors || {};
|
||||||
|
const bgColor = hexToRgba(colors.bg || '#1a1a2e', colors.bgOpacity ?? 100);
|
||||||
|
const accentColor = hexToRgba(colors.accent || '#16213e', colors.accentOpacity ?? 100);
|
||||||
|
const textColor = hexToRgba(colors.text || '#eaeaea', colors.textOpacity ?? 100);
|
||||||
|
const highlightColor = hexToRgba(colors.highlight || '#e94560', colors.highlightOpacity ?? 100);
|
||||||
|
const shadowColor = hexToRgba(colors.highlight || '#e94560', (colors.highlightOpacity ?? 100) * 0.5);
|
||||||
|
|
||||||
|
$panel.css({
|
||||||
|
'--rpg-bg': bgColor,
|
||||||
|
'--rpg-accent': accentColor,
|
||||||
|
'--rpg-text': textColor,
|
||||||
|
'--rpg-highlight': highlightColor,
|
||||||
|
'--rpg-border': highlightColor,
|
||||||
|
'--rpg-shadow': shadowColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderAlternatePresentCharacters({ useCommittedFallback = true } = {}) {
|
||||||
|
if (!extensionSettings.enabled || !extensionSettings.showAlternatePresentCharactersPanel) {
|
||||||
|
removeAlternatePresentCharactersPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characterThoughtsData = getPresentCharactersTrackerData({ useCommittedFallback });
|
||||||
|
if (!characterThoughtsData) {
|
||||||
|
const $panel = ensureAlternatePresentCharactersPanel();
|
||||||
|
$panel.empty().hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentCharacters = parsePresentCharacters(characterThoughtsData);
|
||||||
|
if (presentCharacters.length === 0) {
|
||||||
|
const $panel = ensureAlternatePresentCharactersPanel();
|
||||||
|
$panel.empty().hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = i18n.getTranslation('template.trackerEditorModal.tabs.presentCharacters') || 'Present Characters';
|
||||||
|
|
||||||
|
const $panel = ensureAlternatePresentCharactersPanel();
|
||||||
|
const $header = $('<div class="rpg-alt-present-characters__header"></div>');
|
||||||
|
const $headerTitle = $('<div class="rpg-alt-present-characters__title"></div>');
|
||||||
|
const $scroll = $('<div class="rpg-alt-present-characters__scroll"></div>');
|
||||||
|
const $track = $('<div class="rpg-alt-present-characters__track"></div>');
|
||||||
|
|
||||||
|
$headerTitle.append(
|
||||||
|
$('<i class="fa-solid fa-users" aria-hidden="true"></i>'),
|
||||||
|
$('<span></span>').text(title)
|
||||||
|
);
|
||||||
|
|
||||||
|
$header.append(
|
||||||
|
$headerTitle,
|
||||||
|
$('<div class="rpg-alt-present-characters__count"></div>').text(String(presentCharacters.length))
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const character of presentCharacters) {
|
||||||
|
$track.append(createAlternatePresentCharacterCard(character));
|
||||||
|
}
|
||||||
|
|
||||||
|
$scroll.append($track);
|
||||||
|
|
||||||
|
$panel.empty().append($header, $scroll).show();
|
||||||
|
syncAlternatePresentCharactersTheme();
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
$infoBoxContainer,
|
$infoBoxContainer,
|
||||||
$thoughtsContainer,
|
$thoughtsContainer,
|
||||||
$userStatsContainer,
|
$userStatsContainer,
|
||||||
|
clearThoughtBasedExpressionPortraits,
|
||||||
setPendingDiceRoll,
|
setPendingDiceRoll,
|
||||||
getPendingDiceRoll,
|
getPendingDiceRoll,
|
||||||
clearSessionAvatarPrompts
|
clearSessionAvatarPrompts
|
||||||
@@ -370,6 +371,7 @@ export function setupSettingsPopup() {
|
|||||||
|
|
||||||
// Clear session avatar prompts
|
// Clear session avatar prompts
|
||||||
clearSessionAvatarPrompts();
|
clearSessionAvatarPrompts();
|
||||||
|
clearThoughtBasedExpressionPortraits();
|
||||||
|
|
||||||
// Clear chat metadata immediately (don't wait for debounced save)
|
// Clear chat metadata immediately (don't wait for debounced save)
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
@@ -387,6 +389,14 @@ export function setupSettingsPopup() {
|
|||||||
delete message.extra.rpg_companion_swipes;
|
delete message.extra.rpg_companion_swipes;
|
||||||
// console.log('[RPG Companion] Cleared swipe data from message at index', i);
|
// console.log('[RPG Companion] Cleared swipe data from message at index', i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.swipe_info)) {
|
||||||
|
for (const swipeInfo of message.swipe_info) {
|
||||||
|
if (swipeInfo?.extra?.rpg_companion_swipes) {
|
||||||
|
delete swipeInfo.extra.rpg_companion_swipes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { extensionSettings, $panelContainer } from '../../core/state.js';
|
import { extensionSettings, $panelContainer } from '../../core/state.js';
|
||||||
|
import { syncAlternatePresentCharactersTheme } from './alternatePresentCharacters.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts hex color and opacity percentage to rgba string
|
* Converts hex color and opacity percentage to rgba string
|
||||||
@@ -96,6 +97,8 @@ export function applyTheme() {
|
|||||||
$thoughtPanel.attr('data-theme', theme);
|
$thoughtPanel.attr('data-theme', theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncAlternatePresentCharactersTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,6 +153,8 @@ export function applyCustomTheme() {
|
|||||||
if ($thoughtPanel.length) {
|
if ($thoughtPanel.length) {
|
||||||
$thoughtPanel.attr('data-theme', 'custom').css(customStyles);
|
$thoughtPanel.attr('data-theme', 'custom').css(customStyles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncAlternatePresentCharactersTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Image URL Utilities Module
|
||||||
|
* Centralizes validation for image sources captured from DOM or settings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DEFAULT_IMAGE_BASE_URL = typeof window !== 'undefined'
|
||||||
|
? window.location.href
|
||||||
|
: 'http://localhost/';
|
||||||
|
|
||||||
|
export function normalizeImageSrc(src) {
|
||||||
|
return String(src ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveImageUrl(src, baseUrl = DEFAULT_IMAGE_BASE_URL) {
|
||||||
|
const normalized = normalizeImageSrc(src);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(normalized, baseUrl);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSafeImageSrc(src) {
|
||||||
|
const normalized = normalizeImageSrc(src);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = resolveImageUrl(normalized);
|
||||||
|
if (!candidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = candidate.protocol.toLowerCase();
|
||||||
|
if (protocol === 'http:' || protocol === 'https:' || protocol === 'blob:') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol === 'data:') {
|
||||||
|
return normalized.toLowerCase().startsWith('data:image/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSafeImageSrc(src) {
|
||||||
|
const normalized = normalizeImageSrc(src);
|
||||||
|
return isSafeImageSrc(normalized) ? normalized : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import { this_chid, characters } from '../../../../../../script.js';
|
||||||
|
import { selected_group, getGroupMembers } from '../../../../../group-chats.js';
|
||||||
|
import {
|
||||||
|
extensionSettings,
|
||||||
|
lastGeneratedData,
|
||||||
|
committedTrackerData,
|
||||||
|
FALLBACK_AVATAR_DATA_URI
|
||||||
|
} from '../core/state.js';
|
||||||
|
import { getSafeThumbnailUrl } from './avatars.js';
|
||||||
|
|
||||||
|
export function stripBrackets(value) {
|
||||||
|
if (typeof value !== 'string') return value;
|
||||||
|
return value.replace(/^\[|\]$/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractFieldValue(fieldValue) {
|
||||||
|
if (fieldValue && typeof fieldValue === 'object' && 'value' in fieldValue) {
|
||||||
|
return fieldValue.value || '';
|
||||||
|
}
|
||||||
|
return fieldValue || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSnakeCase(name) {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
|
.replace(/^_+|_+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function namesMatch(cardName, aiName) {
|
||||||
|
if (!cardName || !aiName) return false;
|
||||||
|
|
||||||
|
if (cardName.toLowerCase() === aiName.toLowerCase()) return true;
|
||||||
|
|
||||||
|
const stripParens = (s) => s.replace(/\s*\([^)]*\)/g, '').trim();
|
||||||
|
const cardCore = stripParens(cardName).toLowerCase();
|
||||||
|
const aiCore = stripParens(aiName).toLowerCase();
|
||||||
|
if (cardCore === aiCore) return true;
|
||||||
|
|
||||||
|
const escapedCardCore = cardCore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const wordBoundary = new RegExp(`\\b${escapedCardCore}\\b`);
|
||||||
|
return wordBoundary.test(aiCore);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPresentCharactersEnabled() {
|
||||||
|
return !!(
|
||||||
|
extensionSettings.showCharacterThoughts
|
||||||
|
|| extensionSettings.showAlternatePresentCharactersPanel
|
||||||
|
|| extensionSettings.showThoughtsInChat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPresentCharactersTrackerData({ useCommittedFallback = true } = {}) {
|
||||||
|
return lastGeneratedData.characterThoughts || (useCommittedFallback ? committedTrackerData.characterThoughts : null) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePresentCharacters(characterThoughtsData, { enabledFields = [], enabledCharStats = [] } = {}) {
|
||||||
|
if (!characterThoughtsData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentCharacters = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = typeof characterThoughtsData === 'string'
|
||||||
|
? JSON.parse(characterThoughtsData)
|
||||||
|
: characterThoughtsData;
|
||||||
|
|
||||||
|
const charactersArray = Array.isArray(parsed) ? parsed : (parsed.characters || []);
|
||||||
|
|
||||||
|
if (charactersArray.length > 0) {
|
||||||
|
presentCharacters = charactersArray.map(char => {
|
||||||
|
const character = {
|
||||||
|
name: char.name,
|
||||||
|
emoji: char.emoji || '👤'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (char.details) {
|
||||||
|
for (const field of enabledFields) {
|
||||||
|
if (char.details[field.name] !== undefined) {
|
||||||
|
character[field.name] = stripBrackets(char.details[field.name]);
|
||||||
|
} else {
|
||||||
|
const fieldKey = toSnakeCase(field.name);
|
||||||
|
if (char.details[fieldKey] !== undefined) {
|
||||||
|
character[field.name] = stripBrackets(char.details[fieldKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of enabledFields) {
|
||||||
|
if (character[field.name] === undefined) {
|
||||||
|
const fieldKey = toSnakeCase(field.name);
|
||||||
|
if (char[fieldKey] !== undefined) {
|
||||||
|
character[field.name] = stripBrackets(char[fieldKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.Relationship) {
|
||||||
|
character.Relationship = stripBrackets(char.Relationship);
|
||||||
|
} else if (char.relationship) {
|
||||||
|
character.Relationship = stripBrackets(char.relationship.status || char.relationship);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.thoughts) {
|
||||||
|
character.ThoughtsContent = stripBrackets(char.thoughts.content || char.thoughts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.stats && enabledCharStats.length > 0) {
|
||||||
|
if (Array.isArray(char.stats)) {
|
||||||
|
for (const statObj of char.stats) {
|
||||||
|
if (statObj.name && statObj.value !== undefined) {
|
||||||
|
const matchingStat = enabledCharStats.find(s => s.name === statObj.name);
|
||||||
|
if (matchingStat) {
|
||||||
|
character[statObj.name] = statObj.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const stat of enabledCharStats) {
|
||||||
|
if (char.stats[stat.name] !== undefined) {
|
||||||
|
character[stat.name] = char.stats[stat.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return character;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to the legacy text format below.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presentCharacters.length > 0 || typeof characterThoughtsData !== 'string') {
|
||||||
|
return presentCharacters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = characterThoughtsData.split('\n');
|
||||||
|
let currentCharacter = null;
|
||||||
|
const thoughtsLabel = extensionSettings.trackerConfig?.presentCharacters?.thoughts?.name || 'Thoughts';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()
|
||||||
|
|| line.includes('Present Characters')
|
||||||
|
|| line.includes('---')
|
||||||
|
|| line.trim().startsWith('```')
|
||||||
|
|| line.trim() === '- …'
|
||||||
|
|| line.includes('(Repeat the format')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.trim().startsWith('- ')) {
|
||||||
|
const name = line.trim().substring(2).trim();
|
||||||
|
|
||||||
|
if (name && name.toLowerCase() !== 'unavailable') {
|
||||||
|
currentCharacter = { name };
|
||||||
|
presentCharacters.push(currentCharacter);
|
||||||
|
} else {
|
||||||
|
currentCharacter = null;
|
||||||
|
}
|
||||||
|
} else if (line.trim().startsWith('Details:') && currentCharacter) {
|
||||||
|
const detailsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||||
|
const parts = detailsContent.split('|').map(p => p.trim());
|
||||||
|
|
||||||
|
if (parts.length > 0) {
|
||||||
|
currentCharacter.emoji = parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < enabledFields.length && i + 1 < parts.length; i++) {
|
||||||
|
currentCharacter[enabledFields[i].name] = parts[i + 1];
|
||||||
|
}
|
||||||
|
} else if (line.trim().startsWith('Relationship:') && currentCharacter) {
|
||||||
|
currentCharacter.Relationship = line.substring(line.indexOf(':') + 1).trim();
|
||||||
|
} else if (line.trim().startsWith('Stats:') && currentCharacter && enabledCharStats.length > 0) {
|
||||||
|
const statsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||||
|
const statParts = statsContent.split('|').map(p => p.trim());
|
||||||
|
|
||||||
|
for (const statPart of statParts) {
|
||||||
|
const statMatch = statPart.match(/^(.+?):\s*(\d+)%$/);
|
||||||
|
if (statMatch) {
|
||||||
|
currentCharacter[statMatch[1].trim()] = parseInt(statMatch[2], 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (line.trim().startsWith(thoughtsLabel + ':') && currentCharacter) {
|
||||||
|
currentCharacter.ThoughtsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return presentCharacters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePresentCharacterPortrait(name) {
|
||||||
|
let characterPortrait = FALLBACK_AVATAR_DATA_URI;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return characterPortrait;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[name]) {
|
||||||
|
return extensionSettings.npcAvatars[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected_group) {
|
||||||
|
try {
|
||||||
|
const groupMembers = getGroupMembers(selected_group);
|
||||||
|
const matchingMember = groupMembers?.find(member =>
|
||||||
|
member && member.name && namesMatch(member.name, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingMember?.avatar && matchingMember.avatar !== 'none') {
|
||||||
|
const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingMember.avatar);
|
||||||
|
if (thumbnailUrl) {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore avatar lookup issues and continue through fallback chain.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (characters?.length > 0) {
|
||||||
|
const matchingCharacter = characters.find(character =>
|
||||||
|
character && character.name && namesMatch(character.name, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingCharacter?.avatar && matchingCharacter.avatar !== 'none') {
|
||||||
|
const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingCharacter.avatar);
|
||||||
|
if (thumbnailUrl) {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this_chid !== undefined && characters?.[this_chid]?.name && namesMatch(characters[this_chid].name, name)) {
|
||||||
|
const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar);
|
||||||
|
if (thumbnailUrl) {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return characterPortrait;
|
||||||
|
}
|
||||||
@@ -0,0 +1,626 @@
|
|||||||
|
import { Fuse } from '../../../../../../lib.js';
|
||||||
|
import {
|
||||||
|
characters,
|
||||||
|
eventSource,
|
||||||
|
event_types,
|
||||||
|
generateQuietPrompt,
|
||||||
|
generateRaw,
|
||||||
|
getRequestHeaders,
|
||||||
|
online_status,
|
||||||
|
substituteParams,
|
||||||
|
substituteParamsExtended,
|
||||||
|
this_chid
|
||||||
|
} from '../../../../../../script.js';
|
||||||
|
import {
|
||||||
|
doExtrasFetch,
|
||||||
|
extension_settings as stExtensionSettings,
|
||||||
|
getApiUrl,
|
||||||
|
modules
|
||||||
|
} from '../../../../../extensions.js';
|
||||||
|
import { selected_group, getGroupMembers } from '../../../../../group-chats.js';
|
||||||
|
import { removeReasoningFromString } from '../../../../../reasoning.js';
|
||||||
|
import { isJsonSchemaSupported } from '../../../../../textgen-settings.js';
|
||||||
|
import { trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../../../../utils.js';
|
||||||
|
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../../../../../extensions/shared.js';
|
||||||
|
import { namesMatch } from './presentCharacters.js';
|
||||||
|
import { normalizeImageSrc } from './imageUrls.js';
|
||||||
|
|
||||||
|
const EXPRESSIONS_EXTENSION_NAME = 'expressions';
|
||||||
|
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
|
||||||
|
const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
|
||||||
|
const DEFAULT_EXPRESSIONS = [
|
||||||
|
'admiration',
|
||||||
|
'amusement',
|
||||||
|
'anger',
|
||||||
|
'annoyance',
|
||||||
|
'approval',
|
||||||
|
'caring',
|
||||||
|
'confusion',
|
||||||
|
'curiosity',
|
||||||
|
'desire',
|
||||||
|
'disappointment',
|
||||||
|
'disapproval',
|
||||||
|
'disgust',
|
||||||
|
'embarrassment',
|
||||||
|
'excitement',
|
||||||
|
'fear',
|
||||||
|
'gratitude',
|
||||||
|
'grief',
|
||||||
|
'joy',
|
||||||
|
'love',
|
||||||
|
'nervousness',
|
||||||
|
'optimism',
|
||||||
|
'pride',
|
||||||
|
'realization',
|
||||||
|
'relief',
|
||||||
|
'remorse',
|
||||||
|
'sadness',
|
||||||
|
'surprise',
|
||||||
|
'neutral'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EXPRESSION_API = {
|
||||||
|
local: 0,
|
||||||
|
extras: 1,
|
||||||
|
llm: 2,
|
||||||
|
webllm: 3,
|
||||||
|
none: 99
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROMPT_TYPE = {
|
||||||
|
raw: 'raw',
|
||||||
|
full: 'full'
|
||||||
|
};
|
||||||
|
|
||||||
|
let expressionsListCache = null;
|
||||||
|
const spriteCache = new Map();
|
||||||
|
|
||||||
|
function getNormalizedExpressionsSettings() {
|
||||||
|
const settings = stExtensionSettings.expressions || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
api: Number.isInteger(settings.api) ? settings.api : EXPRESSION_API.none,
|
||||||
|
custom: Array.isArray(settings.custom) ? settings.custom.slice() : [],
|
||||||
|
showDefault: settings.showDefault === true,
|
||||||
|
translate: settings.translate === true,
|
||||||
|
fallbackExpression: typeof settings.fallback_expression === 'string' && settings.fallback_expression.trim()
|
||||||
|
? settings.fallback_expression.trim().toLowerCase()
|
||||||
|
: '',
|
||||||
|
llmPrompt: typeof settings.llmPrompt === 'string' && settings.llmPrompt.trim()
|
||||||
|
? settings.llmPrompt
|
||||||
|
: DEFAULT_LLM_PROMPT,
|
||||||
|
allowMultiple: settings.allowMultiple !== false,
|
||||||
|
rerollIfSame: settings.rerollIfSame === true,
|
||||||
|
filterAvailable: settings.filterAvailable === true,
|
||||||
|
promptType: settings.promptType === PROMPT_TYPE.full ? PROMPT_TYPE.full : PROMPT_TYPE.raw,
|
||||||
|
expressionOverrides: Array.isArray(stExtensionSettings.expressionOverrides)
|
||||||
|
? stExtensionSettings.expressionOverrides.slice()
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExpressionsExtensionEnabled() {
|
||||||
|
return !stExtensionSettings.disabledExtensions?.includes(EXPRESSIONS_EXTENSION_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpressionsSettingsSignature() {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
return JSON.stringify({
|
||||||
|
api: settings.api,
|
||||||
|
custom: settings.custom,
|
||||||
|
showDefault: settings.showDefault,
|
||||||
|
translate: settings.translate,
|
||||||
|
fallbackExpression: settings.fallbackExpression,
|
||||||
|
llmPrompt: settings.llmPrompt,
|
||||||
|
allowMultiple: settings.allowMultiple,
|
||||||
|
rerollIfSame: settings.rerollIfSame,
|
||||||
|
filterAvailable: settings.filterAvailable,
|
||||||
|
promptType: settings.promptType,
|
||||||
|
expressionOverrides: settings.expressionOverrides
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpressionClassificationSettingsSignature() {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
return JSON.stringify({
|
||||||
|
api: settings.api,
|
||||||
|
custom: settings.custom,
|
||||||
|
translate: settings.translate,
|
||||||
|
fallbackExpression: settings.fallbackExpression,
|
||||||
|
llmPrompt: settings.llmPrompt,
|
||||||
|
filterAvailable: settings.filterAvailable,
|
||||||
|
promptType: settings.promptType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpressionPortraitSettingsSignature() {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
return JSON.stringify({
|
||||||
|
custom: settings.custom,
|
||||||
|
showDefault: settings.showDefault,
|
||||||
|
fallbackExpression: settings.fallbackExpression,
|
||||||
|
allowMultiple: settings.allowMultiple,
|
||||||
|
rerollIfSame: settings.rerollIfSame
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearExpressionsCompatibilityCache() {
|
||||||
|
expressionsListCache = null;
|
||||||
|
spriteCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueValues(values) {
|
||||||
|
return values.filter((value, index) => values.indexOf(value) === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExpressionLabel(label) {
|
||||||
|
return String(label || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripExtension(fileName) {
|
||||||
|
return String(fileName || '').replace(/\.[^/.]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFolderOverride(folderName, expressionOverrides) {
|
||||||
|
const override = expressionOverrides.find(entry => entry?.name === folderName);
|
||||||
|
return override?.path ? String(override.path) : folderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvatarFolderName(avatar) {
|
||||||
|
if (!avatar || avatar === 'none') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(avatar).replace(/\.[^/.]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSpriteFolderNameForCharacter(characterName) {
|
||||||
|
if (!characterName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
const groupId = selected_group;
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
try {
|
||||||
|
const groupMembers = getGroupMembers(groupId) || [];
|
||||||
|
const matchingMember = groupMembers.find(member =>
|
||||||
|
member?.name && namesMatch(member.name, characterName));
|
||||||
|
|
||||||
|
const memberFolder = getAvatarFolderName(matchingMember?.avatar);
|
||||||
|
if (memberFolder) {
|
||||||
|
return resolveFolderOverride(memberFolder, settings.expressionOverrides);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore group lookup issues and continue through the fallback chain.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(characters) && characters.length > 0) {
|
||||||
|
const matchingCharacter = characters.find(character =>
|
||||||
|
character?.name && namesMatch(character.name, characterName));
|
||||||
|
|
||||||
|
const characterFolder = getAvatarFolderName(matchingCharacter?.avatar);
|
||||||
|
if (characterFolder) {
|
||||||
|
return resolveFolderOverride(characterFolder, settings.expressionOverrides);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this_chid !== undefined && characters?.[this_chid]?.name && namesMatch(characters[this_chid].name, characterName)) {
|
||||||
|
const currentCharacterFolder = getAvatarFolderName(characters[this_chid].avatar);
|
||||||
|
if (currentCharacterFolder) {
|
||||||
|
return resolveFolderOverride(currentCharacterFolder, settings.expressionOverrides);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sampleClassifyText(text, expressionsApi) {
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = substituteParams(text).replace(/[*"]/g, '');
|
||||||
|
|
||||||
|
if (expressionsApi === EXPRESSION_API.llm) {
|
||||||
|
return result.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const SAMPLE_THRESHOLD = 500;
|
||||||
|
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
|
||||||
|
|
||||||
|
if (text.length < SAMPLE_THRESHOLD) {
|
||||||
|
result = trimToEndSentence(result);
|
||||||
|
} else {
|
||||||
|
result = `${trimToEndSentence(result.slice(0, HALF_SAMPLE_THRESHOLD))} ${trimToStartSentence(result.slice(-HALF_SAMPLE_THRESHOLD))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJsonSchema(labels) {
|
||||||
|
return {
|
||||||
|
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
emotion: {
|
||||||
|
type: 'string',
|
||||||
|
enum: labels
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['emotion'],
|
||||||
|
additionalProperties: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFullContextThoughtPrompt(prompt, text) {
|
||||||
|
return [
|
||||||
|
prompt,
|
||||||
|
'',
|
||||||
|
'Classify the emotion of the following text instead of the last chat message.',
|
||||||
|
'Output exactly one label from the allowed list.',
|
||||||
|
'',
|
||||||
|
`Text: ${text}`
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLlmResponse(emotionResponse, labels) {
|
||||||
|
try {
|
||||||
|
const parsedEmotion = JSON.parse(emotionResponse);
|
||||||
|
const response = parsedEmotion?.emotion?.trim()?.toLowerCase();
|
||||||
|
|
||||||
|
if (response && labels.includes(response)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to the fuzzy parse below.
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedResponse = removeReasoningFromString(String(emotionResponse || ''));
|
||||||
|
const lowerCaseResponse = cleanedResponse.toLowerCase();
|
||||||
|
|
||||||
|
for (const label of labels) {
|
||||||
|
if (lowerCaseResponse.includes(label.toLowerCase())) {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuse = new Fuse(labels, { includeScore: true });
|
||||||
|
const match = fuse.search(cleanedResponse)[0];
|
||||||
|
if (match?.item) {
|
||||||
|
return match.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Could not parse expression label from response');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveExpressionsList() {
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (settings.api === EXPRESSION_API.extras && modules.includes('classify')) {
|
||||||
|
const url = new URL(getApiUrl());
|
||||||
|
url.pathname = '/api/classify/labels';
|
||||||
|
|
||||||
|
const response = await doExtrasFetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Bypass-Tunnel-Reminder': 'bypass' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return Array.isArray(data?.labels)
|
||||||
|
? data.labels.map(normalizeExpressionLabel).filter(Boolean)
|
||||||
|
: DEFAULT_EXPRESSIONS.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.api === EXPRESSION_API.local) {
|
||||||
|
const response = await fetch('/api/extra/classify/labels', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders({ omitContentType: true })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return Array.isArray(data?.labels)
|
||||||
|
? data.labels.map(normalizeExpressionLabel).filter(Boolean)
|
||||||
|
: DEFAULT_EXPRESSIONS.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to the built-in labels below.
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_EXPRESSIONS.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAvailableExpressionLabelsForCharacter(characterName) {
|
||||||
|
const spriteFolderName = resolveSpriteFolderNameForCharacter(characterName);
|
||||||
|
if (!spriteFolderName) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressions = await getSpritesList(spriteFolderName);
|
||||||
|
return expressions
|
||||||
|
.filter(expression => Array.isArray(expression?.files) && expression.files.length > 0)
|
||||||
|
.map(expression => String(expression.label || '').trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExpressionsList({ characterName = '', filterAvailable = false } = {}) {
|
||||||
|
if (!Array.isArray(expressionsListCache)) {
|
||||||
|
expressionsListCache = await resolveExpressionsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
const expressions = uniqueValues([...expressionsListCache, ...settings.custom.map(value => String(value).trim().toLowerCase())])
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (!filterAvailable || ![EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(settings.api)) {
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableExpressions = await getAvailableExpressionLabelsForCharacter(characterName);
|
||||||
|
if (!availableExpressions.length) {
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expressions.filter(expression => availableExpressions.includes(expression));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSpritesList(spriteFolderName) {
|
||||||
|
if (!spriteFolderName) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spriteCache.has(spriteFolderName)) {
|
||||||
|
return spriteCache.get(spriteFolderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/sprites/get?name=${encodeURIComponent(spriteFolderName)}`);
|
||||||
|
const sprites = response.ok ? await response.json() : [];
|
||||||
|
const grouped = [];
|
||||||
|
|
||||||
|
for (const sprite of Array.isArray(sprites) ? sprites : []) {
|
||||||
|
const fileName = String(sprite?.path || '').split('/').pop()?.split('?')[0] || '';
|
||||||
|
const imageData = {
|
||||||
|
expression: normalizeExpressionLabel(sprite?.label),
|
||||||
|
fileName,
|
||||||
|
title: stripExtension(fileName),
|
||||||
|
imageSrc: String(sprite?.path || ''),
|
||||||
|
type: 'success',
|
||||||
|
isCustom: getNormalizedExpressionsSettings().custom.includes(normalizeExpressionLabel(sprite?.label))
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing = grouped.find(entry => entry.label === imageData.expression);
|
||||||
|
if (!existing) {
|
||||||
|
existing = { label: imageData.expression, files: [] };
|
||||||
|
grouped.push(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.files.push(imageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const expression of grouped) {
|
||||||
|
expression.files.sort((left, right) => {
|
||||||
|
if (left.title === expression.label) return -1;
|
||||||
|
if (right.title === expression.label) return 1;
|
||||||
|
return left.title.localeCompare(right.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteCache.set(spriteFolderName, grouped);
|
||||||
|
return grouped;
|
||||||
|
} catch {
|
||||||
|
spriteCache.set(spriteFolderName, []);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseSpriteForExpression(expressions, expression, { previousSrc = null } = {}) {
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
let sprite = expressions.find(entry => entry.label === expression);
|
||||||
|
|
||||||
|
if (!(sprite?.files?.length > 0) && settings.fallbackExpression) {
|
||||||
|
sprite = expressions.find(entry => entry.label === settings.fallbackExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(sprite?.files?.length > 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let candidates = sprite.files;
|
||||||
|
if (settings.allowMultiple && sprite.files.length > 1) {
|
||||||
|
if (settings.rerollIfSame) {
|
||||||
|
const filtered = sprite.files.filter(file => !previousSrc || file.imageSrc !== previousSrc);
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
candidates = filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[Math.floor(Math.random() * candidates.length)] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultExpressionImage(expression, customExpressions) {
|
||||||
|
let normalizedExpression = String(expression || '').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!normalizedExpression) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customExpressions.includes(normalizedExpression)) {
|
||||||
|
normalizedExpression = DEFAULT_FALLBACK_EXPRESSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/img/default-expressions/${normalizedExpression}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function classifyExpressionText(text, { characterName = '' } = {}) {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
if (!text) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.api === EXPRESSION_API.none) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedText = text;
|
||||||
|
if (settings.translate && typeof globalThis.translate === 'function') {
|
||||||
|
processedText = await globalThis.translate(processedText, 'en');
|
||||||
|
}
|
||||||
|
|
||||||
|
processedText = sampleClassifyText(processedText, settings.api);
|
||||||
|
if (!processedText) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = await getExpressionsList({
|
||||||
|
characterName,
|
||||||
|
filterAvailable: settings.filterAvailable === true
|
||||||
|
});
|
||||||
|
const fallbackLabels = labels.length > 0 ? labels : await getExpressionsList();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (settings.api) {
|
||||||
|
case EXPRESSION_API.local: {
|
||||||
|
const response = await fetch('/api/extra/classify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({ text: processedText })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return String(data?.classification?.[0]?.label || settings.fallbackExpression || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EXPRESSION_API.extras: {
|
||||||
|
if (!modules.includes('classify')) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(getApiUrl());
|
||||||
|
url.pathname = '/api/classify';
|
||||||
|
|
||||||
|
const response = await doExtrasFetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Bypass-Tunnel-Reminder': 'bypass'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text: processedText })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return String(data?.classification?.[0]?.label || settings.fallbackExpression || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EXPRESSION_API.llm: {
|
||||||
|
await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);
|
||||||
|
|
||||||
|
const labelsString = fallbackLabels.map(label => `"${label}"`).join(', ');
|
||||||
|
const basePrompt = substituteParamsExtended(settings.llmPrompt, { labels: labelsString });
|
||||||
|
const prompt = settings.promptType === PROMPT_TYPE.full
|
||||||
|
? buildFullContextThoughtPrompt(basePrompt, processedText)
|
||||||
|
: basePrompt;
|
||||||
|
const onReady = (args) => {
|
||||||
|
if (isJsonSchemaSupported()) {
|
||||||
|
Object.assign(args, {
|
||||||
|
top_k: 1,
|
||||||
|
stop: [],
|
||||||
|
stopping_strings: [],
|
||||||
|
custom_token_bans: [],
|
||||||
|
json_schema: getJsonSchema(fallbackLabels)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onReady);
|
||||||
|
|
||||||
|
const responseText = settings.promptType === PROMPT_TYPE.full
|
||||||
|
? await generateQuietPrompt({ quietPrompt: prompt })
|
||||||
|
: await generateRaw({ prompt: processedText, systemPrompt: prompt });
|
||||||
|
|
||||||
|
return parseLlmResponse(responseText, fallbackLabels);
|
||||||
|
}
|
||||||
|
case EXPRESSION_API.webllm: {
|
||||||
|
if (!isWebLlmSupported()) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsString = fallbackLabels.map(label => `"${label}"`).join(', ');
|
||||||
|
const prompt = substituteParamsExtended(settings.llmPrompt, { labels: labelsString });
|
||||||
|
const responseText = await generateWebLlmChatPrompt([
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `${processedText}\n\n${prompt}`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return parseLlmResponse(responseText, fallbackLabels);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveExpressionPortraitForCharacter(characterName, expression, { previousSrc = null } = {}) {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
const normalizedExpression = String(expression || '').trim().toLowerCase();
|
||||||
|
const spriteFolderName = resolveSpriteFolderNameForCharacter(characterName);
|
||||||
|
|
||||||
|
if (spriteFolderName) {
|
||||||
|
const expressions = await getSpritesList(spriteFolderName);
|
||||||
|
const spriteFile = chooseSpriteForExpression(expressions, normalizedExpression, { previousSrc });
|
||||||
|
const spriteSrc = normalizeImageSrc(spriteFile?.imageSrc || '');
|
||||||
|
if (spriteSrc) {
|
||||||
|
return spriteSrc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.showDefault) {
|
||||||
|
const defaultExpression = normalizedExpression || settings.fallbackExpression;
|
||||||
|
const defaultImage = normalizeImageSrc(getDefaultExpressionImage(defaultExpression, settings.custom));
|
||||||
|
if (defaultImage) {
|
||||||
|
return defaultImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
thoughtBasedExpressionPortraits,
|
||||||
|
getThoughtBasedExpressionPortrait
|
||||||
|
} from '../core/state.js';
|
||||||
|
import {
|
||||||
|
isSafeImageSrc,
|
||||||
|
normalizeImageSrc,
|
||||||
|
resolveImageUrl
|
||||||
|
} from './imageUrls.js';
|
||||||
|
import { isExpressionsExtensionEnabled } from './sillyTavernExpressions.js';
|
||||||
|
|
||||||
|
function normalizeName(name) {
|
||||||
|
return String(name || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function namesMatch(a, b) {
|
||||||
|
const left = normalizeName(a);
|
||||||
|
const right = normalizeName(b);
|
||||||
|
if (!left || !right) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left === right || left.startsWith(right + ' ') || right.startsWith(left + ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDocumentLikeUrl(src) {
|
||||||
|
const candidate = resolveImageUrl(src);
|
||||||
|
if (!candidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = new URL(window.location.href);
|
||||||
|
return candidate.origin === current.origin
|
||||||
|
&& candidate.pathname === current.pathname
|
||||||
|
&& candidate.search === current.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUsableThoughtBasedExpressionSrc(src) {
|
||||||
|
const normalized = normalizeImageSrc(src);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDocumentLikeUrl(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSafeImageSrc(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThoughtBasedExpressionPortraitForCharacter(characterName) {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = normalizeName(characterName);
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exact = getThoughtBasedExpressionPortrait(target);
|
||||||
|
if (isUsableThoughtBasedExpressionSrc(exact)) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [storedName, src] of Object.entries(thoughtBasedExpressionPortraits)) {
|
||||||
|
if (namesMatch(storedName, target) && isUsableThoughtBasedExpressionSrc(src)) {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
.rpg-panel,
|
.rpg-panel,
|
||||||
#rpg-thought-panel,
|
#rpg-thought-panel,
|
||||||
#rpg-thought-icon,
|
#rpg-thought-icon,
|
||||||
|
#rpg-alt-present-characters,
|
||||||
.rpg-mobile-toggle {
|
.rpg-mobile-toggle {
|
||||||
--rpg-bg: var(--SmartThemeBlurTintColor, rgba(26, 26, 46, 0.9));
|
--rpg-bg: var(--SmartThemeBlurTintColor, rgba(26, 26, 46, 0.9));
|
||||||
--rpg-accent: var(--black30a, rgba(22, 33, 62, 0.9));
|
--rpg-accent: var(--black30a, rgba(22, 33, 62, 0.9));
|
||||||
@@ -3256,6 +3257,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
/* Apply sci-fi theme to thought panel */
|
/* Apply sci-fi theme to thought panel */
|
||||||
#rpg-thought-panel[data-theme="sci-fi"],
|
#rpg-thought-panel[data-theme="sci-fi"],
|
||||||
#rpg-thought-icon[data-theme="sci-fi"],
|
#rpg-thought-icon[data-theme="sci-fi"],
|
||||||
|
#rpg-alt-present-characters[data-theme="sci-fi"],
|
||||||
.rpg-mobile-toggle[data-theme="sci-fi"] {
|
.rpg-mobile-toggle[data-theme="sci-fi"] {
|
||||||
--rpg-bg: #0a0e27;
|
--rpg-bg: #0a0e27;
|
||||||
--rpg-accent: #1a1f3a;
|
--rpg-accent: #1a1f3a;
|
||||||
@@ -3304,6 +3306,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
/* Apply fantasy theme to thought panel */
|
/* Apply fantasy theme to thought panel */
|
||||||
#rpg-thought-panel[data-theme="fantasy"],
|
#rpg-thought-panel[data-theme="fantasy"],
|
||||||
#rpg-thought-icon[data-theme="fantasy"],
|
#rpg-thought-icon[data-theme="fantasy"],
|
||||||
|
#rpg-alt-present-characters[data-theme="fantasy"],
|
||||||
.rpg-mobile-toggle[data-theme="fantasy"] {
|
.rpg-mobile-toggle[data-theme="fantasy"] {
|
||||||
--rpg-bg: #2b1810;
|
--rpg-bg: #2b1810;
|
||||||
--rpg-accent: #3d2414;
|
--rpg-accent: #3d2414;
|
||||||
@@ -3361,6 +3364,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
/* Apply cyberpunk theme to thought panel */
|
/* Apply cyberpunk theme to thought panel */
|
||||||
#rpg-thought-panel[data-theme="cyberpunk"],
|
#rpg-thought-panel[data-theme="cyberpunk"],
|
||||||
#rpg-thought-icon[data-theme="cyberpunk"],
|
#rpg-thought-icon[data-theme="cyberpunk"],
|
||||||
|
#rpg-alt-present-characters[data-theme="cyberpunk"],
|
||||||
.rpg-mobile-toggle[data-theme="cyberpunk"] {
|
.rpg-mobile-toggle[data-theme="cyberpunk"] {
|
||||||
--rpg-bg: #000000;
|
--rpg-bg: #000000;
|
||||||
--rpg-accent: #0d0d0d;
|
--rpg-accent: #0d0d0d;
|
||||||
@@ -5025,6 +5029,159 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
BELOW-CHAT PRESENT CHARACTERS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
#rpg-alt-present-characters {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 8px 10px 8px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--rpg-border, rgba(255, 255, 255, 0.14));
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0) 100%),
|
||||||
|
linear-gradient(135deg, var(--rpg-accent, rgba(34, 40, 60, 0.94)) 0%, var(--rpg-bg, rgba(18, 21, 34, 0.96)) 100%);
|
||||||
|
box-shadow: 0 12px 28px var(--rpg-shadow, rgba(0, 0, 0, 0.24));
|
||||||
|
color: var(--rpg-text, var(--SmartThemeBodyColor, #ecf0f1));
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__title i {
|
||||||
|
color: var(--rpg-highlight, #e94560);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__count {
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid var(--rpg-border, rgba(255, 255, 255, 0.14));
|
||||||
|
color: var(--rpg-text, var(--SmartThemeBodyColor, #ecf0f1));
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.24) transparent;
|
||||||
|
transform: scaleY(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__scroll::-webkit-scrollbar {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__scroll::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.24);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__scroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-characters__track {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
transform: scaleY(-1);
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-character {
|
||||||
|
flex: 0 0 98px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 98px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-character__portrait {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 11 / 15;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
background: var(--rpg-bg, rgba(18, 21, 34, 0.96));
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.18);
|
||||||
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-character__portrait:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--rpg-highlight, #e94560);
|
||||||
|
box-shadow: 0 12px 20px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-character__portrait img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-character__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-character__name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-character__name {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#rpg-alt-present-characters {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 7px 9px 7px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-alt-present-character {
|
||||||
|
flex-basis: 84px;
|
||||||
|
min-width: 84px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
CHAT THOUGHT OVERLAYS
|
CHAT THOUGHT OVERLAYS
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -11684,3 +11841,70 @@ body:has(.rpg-panel[data-theme="light"]) .rpg-strip-widget {
|
|||||||
min-width: 2.5rem !important;
|
min-width: 2.5rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thoughts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin: 0.2em var(--mes-right-spacing, 30px) 0.35em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.documentstyle .rpg-inline-thoughts {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-left: 2px solid rgba(255, 120, 140, 0.6);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought[open] {
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45em;
|
||||||
|
padding: 0.45em 0.7em;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought-summary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought-icon {
|
||||||
|
opacity: 0.9;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought-name {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought-content {
|
||||||
|
padding: 0 0.7em 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-inline-thought-text {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.6em 0.75em;
|
||||||
|
border-radius: 0.4em;
|
||||||
|
background: rgba(0, 0, 0, 0.28);
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,33 @@
|
|||||||
Display character portraits with their current thoughts and status.
|
Display character portraits with their current thoughts and status.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-alt-present-characters" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.showBelowChatPresentCharacters">Show Below-Chat Present Characters</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.showBelowChatPresentCharactersNote">
|
||||||
|
Display a compact Present Characters panel below the chat.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-thought-based-expressions" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.thoughtBasedExpressions">Thought-Based Expressions</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.thoughtBasedExpressionsNote">
|
||||||
|
Use SillyTavern Character Expressions to classify each present character's thoughts for the below-chat panel. May increase token usage depending on the selected Classifier API.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-hide-default-expressions" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.hideDefaultExpressionDisplay">Hide Default Expression Display</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.hideDefaultExpressionDisplayNote">
|
||||||
|
Hide SillyTavern's built-in Character Expressions display.
|
||||||
|
</small>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-toggle-thoughts-in-chat" />
|
<input type="checkbox" id="rpg-toggle-thoughts-in-chat" />
|
||||||
<span data-i18n-key="template.settingsModal.display.showThoughtsInChat">Show Thoughts</span>
|
<span data-i18n-key="template.settingsModal.display.showThoughtsInChat">Show Thoughts</span>
|
||||||
@@ -367,6 +394,15 @@
|
|||||||
Display character thoughts as overlay bubbles next to their messages.
|
Display character thoughts as overlay bubbles next to their messages.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-inline-thoughts" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.showInlineThoughts">Show Thoughts Below Message Text</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.showInlineThoughtsNote">
|
||||||
|
Switch between the default corner thought bubbles and thought cards below the message text.
|
||||||
|
</small>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-toggle-inventory" />
|
<input type="checkbox" id="rpg-toggle-inventory" />
|
||||||
<span data-i18n-key="template.settingsModal.display.showInventory">Show Inventory</span>
|
<span data-i18n-key="template.settingsModal.display.showInventory">Show Inventory</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user