First attempt at adding expression sync
This commit is contained in:
@@ -135,6 +135,14 @@ import {
|
|||||||
updateStripWidgets
|
updateStripWidgets
|
||||||
} from './src/systems/ui/desktop.js';
|
} from './src/systems/ui/desktop.js';
|
||||||
import { removeAlternatePresentCharactersPanel } from './src/systems/ui/alternatePresentCharacters.js';
|
import { removeAlternatePresentCharactersPanel } from './src/systems/ui/alternatePresentCharacters.js';
|
||||||
|
import {
|
||||||
|
initExpressionSync,
|
||||||
|
queueExpressionCaptureForSpeaker,
|
||||||
|
onExpressionSyncSettingChanged,
|
||||||
|
onHideDefaultExpressionDisplaySettingChanged,
|
||||||
|
clearExpressionSyncCache,
|
||||||
|
onExpressionSyncChatChanged
|
||||||
|
} from './src/systems/integration/expressionSync.js';
|
||||||
|
|
||||||
// Feature modules
|
// Feature modules
|
||||||
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
|
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
|
||||||
@@ -220,6 +228,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
|
||||||
|
clearExpressionSyncCache();
|
||||||
|
|
||||||
// Disable dynamic weather effects
|
// Disable dynamic weather effects
|
||||||
toggleDynamicWeather(false);
|
toggleDynamicWeather(false);
|
||||||
@@ -235,6 +244,7 @@ async function addExtensionSettings() {
|
|||||||
await initUI();
|
await initUI();
|
||||||
loadChatData(); // Load chat data for current chat
|
loadChatData(); // Load chat data for current chat
|
||||||
scheduleChatStateRehydration();
|
scheduleChatStateRehydration();
|
||||||
|
initExpressionSync();
|
||||||
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
|
||||||
@@ -350,6 +360,18 @@ async function initUI() {
|
|||||||
renderThoughts();
|
renderThoughts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-sync-expressions').on('change', function() {
|
||||||
|
extensionSettings.syncExpressionsToPresentCharacters = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
onExpressionSyncSettingChanged(extensionSettings.syncExpressionsToPresentCharacters);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#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() {
|
||||||
extensionSettings.showInventory = $(this).prop('checked');
|
extensionSettings.showInventory = $(this).prop('checked');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -1063,6 +1085,8 @@ async function initUI() {
|
|||||||
$('#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-alt-present-characters').prop('checked', extensionSettings.showAlternatePresentCharactersPanel ?? false);
|
||||||
|
$('#rpg-toggle-sync-expressions').prop('checked', extensionSettings.syncExpressionsToPresentCharacters === 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);
|
||||||
@@ -1305,6 +1329,7 @@ jQuery(async () => {
|
|||||||
try {
|
try {
|
||||||
loadChatData();
|
loadChatData();
|
||||||
scheduleChatStateRehydration();
|
scheduleChatStateRehydration();
|
||||||
|
initExpressionSync();
|
||||||
// Initialize FAB widgets and strip widgets with any loaded data
|
// Initialize FAB widgets and strip widgets with any loaded data
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
updateStripWidgets();
|
updateStripWidgets();
|
||||||
@@ -1382,6 +1407,53 @@ jQuery(async () => {
|
|||||||
[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) {
|
||||||
|
queueExpressionCaptureForSpeaker(renderedMessage.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_UPDATED, (messageId) => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMessage = chat[messageId];
|
||||||
|
if (updatedMessage && !updatedMessage.is_user && !updatedMessage.is_system) {
|
||||||
|
queueExpressionCaptureForSpeaker(updatedMessage.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_SWIPED, (messageIndex) => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipedMessage = chat[messageIndex];
|
||||||
|
if (swipedMessage && !swipedMessage.is_user && !swipedMessage.is_system) {
|
||||||
|
queueExpressionCaptureForSpeaker(swipedMessage.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||||
|
clearExpressionSyncCache();
|
||||||
|
setTimeout(() => onExpressionSyncChatChanged(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_DELETED, () => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearExpressionSyncCache();
|
||||||
|
setTimeout(() => onExpressionSyncChatChanged(), 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
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export const defaultSettings = {
|
|||||||
showInfoBox: true,
|
showInfoBox: true,
|
||||||
showCharacterThoughts: true,
|
showCharacterThoughts: true,
|
||||||
showAlternatePresentCharactersPanel: false,
|
showAlternatePresentCharactersPanel: false,
|
||||||
|
syncExpressionsToPresentCharacters: 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
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import {
|
|||||||
extensionSettings,
|
extensionSettings,
|
||||||
lastGeneratedData,
|
lastGeneratedData,
|
||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
|
syncedExpressionPortraits,
|
||||||
setExtensionSettings,
|
setExtensionSettings,
|
||||||
updateExtensionSettings,
|
updateExtensionSettings,
|
||||||
setLastGeneratedData,
|
setLastGeneratedData,
|
||||||
setCommittedTrackerData,
|
setCommittedTrackerData,
|
||||||
|
setSyncedExpressionPortraits,
|
||||||
|
clearSyncedExpressionPortraits,
|
||||||
FEATURE_FLAGS
|
FEATURE_FLAGS
|
||||||
} from './state.js';
|
} from './state.js';
|
||||||
import { migrateInventory } from '../utils/migration.js';
|
import { migrateInventory } from '../utils/migration.js';
|
||||||
@@ -381,6 +384,16 @@ export function loadSettings() {
|
|||||||
settingsChanged = true;
|
settingsChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.syncExpressionsToPresentCharacters === undefined) {
|
||||||
|
extensionSettings.syncExpressionsToPresentCharacters = false;
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.hideDefaultExpressionDisplay === undefined) {
|
||||||
|
extensionSettings.hideDefaultExpressionDisplay = false;
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Save migrated settings
|
// Save migrated settings
|
||||||
if (settingsChanged) {
|
if (settingsChanged) {
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -465,6 +478,7 @@ export function saveChatData() {
|
|||||||
quests: extensionSettings.quests,
|
quests: extensionSettings.quests,
|
||||||
lastGeneratedData: lastGeneratedData,
|
lastGeneratedData: lastGeneratedData,
|
||||||
committedTrackerData: committedTrackerData,
|
committedTrackerData: committedTrackerData,
|
||||||
|
syncedExpressionPortraits: syncedExpressionPortraits,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -548,6 +562,7 @@ export function loadChatData() {
|
|||||||
infoBox: null,
|
infoBox: null,
|
||||||
characterThoughts: null
|
characterThoughts: null
|
||||||
});
|
});
|
||||||
|
clearSyncedExpressionPortraits();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore stats
|
// Restore stats
|
||||||
@@ -596,6 +611,12 @@ export function loadChatData() {
|
|||||||
// console.log('[RPG Companion] ⚠️ No lastGeneratedData found in save');
|
// console.log('[RPG Companion] ⚠️ No lastGeneratedData found in save');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedData?.syncedExpressionPortraits && typeof savedData.syncedExpressionPortraits === 'object') {
|
||||||
|
setSyncedExpressionPortraits(savedData.syncedExpressionPortraits);
|
||||||
|
} else {
|
||||||
|
clearSyncedExpressionPortraits();
|
||||||
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export let extensionSettings = {
|
|||||||
showInfoBox: true,
|
showInfoBox: true,
|
||||||
showCharacterThoughts: true,
|
showCharacterThoughts: true,
|
||||||
showAlternatePresentCharactersPanel: false,
|
showAlternatePresentCharactersPanel: false,
|
||||||
|
syncExpressionsToPresentCharacters: 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
|
||||||
@@ -361,6 +363,40 @@ export function clearSessionAvatarPrompts() {
|
|||||||
sessionAvatarPrompts = {};
|
sessionAvatarPrompts = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-chat storage for synced Character Expressions portraits.
|
||||||
|
* Maps normalized character names to the last captured expression image URL.
|
||||||
|
*/
|
||||||
|
export let syncedExpressionPortraits = {};
|
||||||
|
|
||||||
|
export function setSyncedExpressionPortrait(characterName, src) {
|
||||||
|
if (!characterName || !src) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncedExpressionPortraits[characterName] = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSyncedExpressionPortrait(characterName) {
|
||||||
|
if (!characterName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete syncedExpressionPortraits[characterName];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSyncedExpressionPortraits(portraits) {
|
||||||
|
syncedExpressionPortraits = portraits && typeof portraits === 'object' ? { ...portraits } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSyncedExpressionPortrait(characterName) {
|
||||||
|
return syncedExpressionPortraits[characterName] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSyncedExpressionPortraits() {
|
||||||
|
syncedExpressionPortraits = {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,10 @@
|
|||||||
"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.showBelowChatPresentCharacters": "Show Below-Chat Present Characters",
|
||||||
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Display a compact Present Characters panel below the chat.",
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Display a compact Present Characters panel below the chat.",
|
||||||
|
"template.settingsModal.display.syncBelowChatPresentCharactersExpressions": "Sync Expressions in Below-Chat Panel",
|
||||||
|
"template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote": "Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.",
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
"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.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.showBelowChatPresentCharactersNote": "Afficher un panneau compact des personnages présents sous le chat.",
|
||||||
|
"template.settingsModal.display.syncBelowChatPresentCharactersExpressions": "Sync Expressions in Below-Chat Panel",
|
||||||
|
"template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote": "Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
|
||||||
"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",
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
"template.settingsModal.display.showPresentCharactersNote": "Показывать портреты персонажей с их текущимы мыслями и статусом.",
|
"template.settingsModal.display.showPresentCharactersNote": "Показывать портреты персонажей с их текущимы мыслями и статусом.",
|
||||||
"template.settingsModal.display.showBelowChatPresentCharacters": "Показывать персонажей под чатом",
|
"template.settingsModal.display.showBelowChatPresentCharacters": "Показывать персонажей под чатом",
|
||||||
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Показывать компактную панель персонажей под чатом.",
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Показывать компактную панель персонажей под чатом.",
|
||||||
|
"template.settingsModal.display.syncBelowChatPresentCharactersExpressions": "Sync Expressions in Below-Chat Panel",
|
||||||
|
"template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote": "Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
|
||||||
"template.settingsModal.display.narratorMode": "Режим расказчика",
|
"template.settingsModal.display.narratorMode": "Режим расказчика",
|
||||||
"template.settingsModal.display.narratorModeNote": "Использовать карточку персонажа в качестве расказчика. Персонажи берутся из контекста вместо фиксированных отсылок.",
|
"template.settingsModal.display.narratorModeNote": "Использовать карточку персонажа в качестве расказчика. Персонажи берутся из контекста вместо фиксированных отсылок.",
|
||||||
"template.settingsModal.display.showInventory": "Показывать инвентарь",
|
"template.settingsModal.display.showInventory": "Показывать инвентарь",
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
|
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
|
||||||
"template.settingsModal.display.showBelowChatPresentCharacters": "顯示聊天下方的在場角色",
|
"template.settingsModal.display.showBelowChatPresentCharacters": "顯示聊天下方的在場角色",
|
||||||
"template.settingsModal.display.showBelowChatPresentCharactersNote": "在聊天下方顯示精簡的在場角色面板。",
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "在聊天下方顯示精簡的在場角色面板。",
|
||||||
|
"template.settingsModal.display.syncBelowChatPresentCharactersExpressions": "Sync Expressions in Below-Chat Panel",
|
||||||
|
"template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote": "Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
|
||||||
"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 修改它們。",
|
||||||
|
|||||||
@@ -0,0 +1,632 @@
|
|||||||
|
/**
|
||||||
|
* Character Expressions -> below-chat Present Characters portrait sync.
|
||||||
|
*
|
||||||
|
* Mirrors SillyTavern's currently displayed Character Expressions image into
|
||||||
|
* the alternate Present Characters panel, persisting the last known
|
||||||
|
* expression for each character until they speak again.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { chat } from '../../../../../../../script.js';
|
||||||
|
import {
|
||||||
|
extensionSettings,
|
||||||
|
syncedExpressionPortraits,
|
||||||
|
setSyncedExpressionPortrait,
|
||||||
|
getSyncedExpressionPortrait,
|
||||||
|
removeSyncedExpressionPortrait
|
||||||
|
} from '../../core/state.js';
|
||||||
|
import { saveChatData } from '../../core/persistence.js';
|
||||||
|
import { renderAlternatePresentCharacters } from '../ui/alternatePresentCharacters.js';
|
||||||
|
|
||||||
|
let expressionContainerObserver = null;
|
||||||
|
let expressionImageObserver = null;
|
||||||
|
let observedExpressionImage = null;
|
||||||
|
let pendingSpeakerName = null;
|
||||||
|
let pendingSpeakerBaselineSignature = null;
|
||||||
|
let pendingSpeakerQueuedAt = 0;
|
||||||
|
let lastCapturedExpressionSrc = null;
|
||||||
|
let scheduledCaptureTimers = [];
|
||||||
|
let hiddenExpressionStyleElement = null;
|
||||||
|
let pendingCaptureRequestId = 0;
|
||||||
|
|
||||||
|
function normalizeName(name) {
|
||||||
|
return String(name || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExpressionSrc(src) {
|
||||||
|
return String(src || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExpressionUrl(src) {
|
||||||
|
const normalized = normalizeExpressionSrc(src);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(normalized, window.location.href);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDocumentLikeUrl(src) {
|
||||||
|
const candidate = resolveExpressionUrl(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUsableExpressionSrc(src) {
|
||||||
|
const normalized = normalizeExpressionSrc(src);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = normalized.toLowerCase();
|
||||||
|
if (lower.includes('/img/default-expressions/') || lower.includes('/default-expressions/')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDocumentLikeUrl(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function purgeInvalidSyncedExpressionPortraits() {
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
for (const [storedName, src] of Object.entries(syncedExpressionPortraits)) {
|
||||||
|
if (!isUsableExpressionSrc(src)) {
|
||||||
|
removeSyncedExpressionPortrait(storedName);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
saveChatData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpressionPortraitForCharacter(characterName) {
|
||||||
|
const target = normalizeName(characterName);
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exact = getSyncedExpressionPortrait(target);
|
||||||
|
if (isUsableExpressionSrc(exact)) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [storedName, src] of Object.entries(syncedExpressionPortraits)) {
|
||||||
|
if (namesMatch(storedName, target) && isUsableExpressionSrc(src)) {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestAssistantSpeakerName() {
|
||||||
|
for (let i = chat.length - 1; i >= 0; i--) {
|
||||||
|
const message = chat[i];
|
||||||
|
if (!message || message.is_user || message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.name || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldHideNativeExpressionDisplay() {
|
||||||
|
return extensionSettings.enabled === true && extensionSettings.hideDefaultExpressionDisplay === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRunExpressionObservers() {
|
||||||
|
return extensionSettings.enabled === true && (
|
||||||
|
extensionSettings.syncExpressionsToPresentCharacters === true
|
||||||
|
|| extensionSettings.hideDefaultExpressionDisplay === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpressionContainerNode(node) {
|
||||||
|
if (!(node instanceof Element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!node.closest('#expression-wrapper, #expression-holder, .expression-holder, [data-expression-container], #visual-novel-wrapper');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExpressionImageState(img) {
|
||||||
|
if (!(img instanceof HTMLImageElement)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawSrc = normalizeExpressionSrc(img.getAttribute('src'));
|
||||||
|
const resolvedSrc = normalizeExpressionSrc(img.currentSrc || img.src || '');
|
||||||
|
const src = rawSrc || '';
|
||||||
|
const spriteFolderName = String(img.getAttribute('data-sprite-folder-name') || '').trim();
|
||||||
|
const spriteFileName = String(img.getAttribute('data-sprite-filename') || '').trim();
|
||||||
|
const expression = String(img.getAttribute('data-expression') || img.getAttribute('title') || '').trim();
|
||||||
|
const isDefault = img.classList.contains('default')
|
||||||
|
|| rawSrc.toLowerCase().includes('/img/default-expressions/')
|
||||||
|
|| resolvedSrc.toLowerCase().includes('/img/default-expressions/');
|
||||||
|
|
||||||
|
return {
|
||||||
|
src,
|
||||||
|
resolvedSrc,
|
||||||
|
spriteFolderName,
|
||||||
|
spriteFileName,
|
||||||
|
expression,
|
||||||
|
isDefault,
|
||||||
|
signature: JSON.stringify({
|
||||||
|
src,
|
||||||
|
resolvedSrc,
|
||||||
|
spriteFolderName,
|
||||||
|
spriteFileName,
|
||||||
|
expression,
|
||||||
|
isDefault
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMeaningfulMetadataValue(value) {
|
||||||
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
|
return Boolean(normalized && normalized !== 'null' && normalized !== 'undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeFallbackExpressionAsset(state) {
|
||||||
|
if (!state) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined = [state.src, state.spriteFolderName, state.spriteFileName, state.expression]
|
||||||
|
.map(value => String(value || '').trim().toLowerCase())
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'/img/default-expressions/',
|
||||||
|
'/default-expressions/',
|
||||||
|
'/emote/',
|
||||||
|
'/emotes/',
|
||||||
|
'/emoji/',
|
||||||
|
'/emotion/',
|
||||||
|
'/emotions/',
|
||||||
|
' default ',
|
||||||
|
' fallback ',
|
||||||
|
' placeholder '
|
||||||
|
].some(fragment => combined.includes(fragment.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRealSyncedSprite(state) {
|
||||||
|
if (!state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!state.src || state.isDefault) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isUsableExpressionSrc(state.src)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasMeaningfulMetadataValue(state.spriteFolderName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasMeaningfulMetadataValue(state.spriteFileName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasMeaningfulMetadataValue(state.expression)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (looksLikeFallbackExpressionAsset(state)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProbablyExpressionImage(img) {
|
||||||
|
if (!(img instanceof HTMLImageElement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getExpressionImageState(img);
|
||||||
|
if (!state?.src) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExpressionClass = img.classList.contains('expression') || img.id === 'expression-image';
|
||||||
|
const hasExpressionMetadata = Boolean(state.expression || state.spriteFolderName || state.spriteFileName);
|
||||||
|
|
||||||
|
if (!hasExpressionClass && !hasExpressionMetadata && !isExpressionContainerNode(img)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreferredVisualNovelImage(speakerName) {
|
||||||
|
const target = normalizeName(speakerName);
|
||||||
|
const candidates = Array.from(document.querySelectorAll('#visual-novel-wrapper .expression-holder img'))
|
||||||
|
.filter(node => isProbablyExpressionImage(node) && hasRealSyncedSprite(getExpressionImageState(node)));
|
||||||
|
|
||||||
|
if (!candidates.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!target) {
|
||||||
|
return candidates.find(node => node.offsetParent !== null) || candidates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exactMatch = candidates.find(node => {
|
||||||
|
const state = getExpressionImageState(node);
|
||||||
|
const folderRoot = String(state?.spriteFolderName || '').split('/')[0];
|
||||||
|
return namesMatch(folderRoot, target);
|
||||||
|
});
|
||||||
|
if (exactMatch) {
|
||||||
|
return exactMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates.find(node => node.offsetParent !== null) || candidates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findExpressionImageElement(speakerName = null) {
|
||||||
|
if (observedExpressionImage && observedExpressionImage.isConnected && isProbablyExpressionImage(observedExpressionImage)) {
|
||||||
|
return observedExpressionImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferredSelectors = [
|
||||||
|
'#expression-wrapper img.expression',
|
||||||
|
'#expression-holder > img.expression',
|
||||||
|
'#expression-image'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of preferredSelectors) {
|
||||||
|
const nodes = Array.from(document.querySelectorAll(selector));
|
||||||
|
const visibleMatch = nodes.find(node => isProbablyExpressionImage(node)
|
||||||
|
&& hasRealSyncedSprite(getExpressionImageState(node))
|
||||||
|
&& node.offsetParent !== null);
|
||||||
|
if (visibleMatch) {
|
||||||
|
return visibleMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anyRealMatch = nodes.find(node =>
|
||||||
|
isProbablyExpressionImage(node) && hasRealSyncedSprite(getExpressionImageState(node)));
|
||||||
|
if (anyRealMatch) {
|
||||||
|
return anyRealMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const visualNovelMatch = getPreferredVisualNovelImage(speakerName);
|
||||||
|
if (visualNovelMatch) {
|
||||||
|
return visualNovelMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allImages = Array.from(document.querySelectorAll('img.expression, #visual-novel-wrapper .expression-holder img'));
|
||||||
|
return allImages.find(node => isProbablyExpressionImage(node) && hasRealSyncedSprite(getExpressionImageState(node))) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshExpressionConsumers() {
|
||||||
|
renderAlternatePresentCharacters({ useCommittedFallback: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 syncNativeExpressionDisplayVisibility() {
|
||||||
|
if (shouldHideNativeExpressionDisplay()) {
|
||||||
|
hideNativeExpressionDisplay();
|
||||||
|
} else {
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardownExpressionObservers() {
|
||||||
|
if (expressionContainerObserver) {
|
||||||
|
expressionContainerObserver.disconnect();
|
||||||
|
expressionContainerObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expressionImageObserver) {
|
||||||
|
expressionImageObserver.disconnect();
|
||||||
|
expressionImageObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
observedExpressionImage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureExpressionForSpeaker(speakerName, expectedRequestId = null) {
|
||||||
|
if (!extensionSettings.enabled || !extensionSettings.syncExpressionsToPresentCharacters) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (expectedRequestId !== null && expectedRequestId !== pendingCaptureRequestId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = normalizeName(speakerName || pendingSpeakerName || getLatestAssistantSpeakerName());
|
||||||
|
if (!name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = getSyncedExpressionPortrait(name);
|
||||||
|
const img = findExpressionImageElement(name);
|
||||||
|
const state = getExpressionImageState(img);
|
||||||
|
if (!hasRealSyncedSprite(state)) {
|
||||||
|
const elapsed = pendingSpeakerQueuedAt ? (Date.now() - pendingSpeakerQueuedAt) : 0;
|
||||||
|
if (previous && elapsed >= 1200) {
|
||||||
|
removeSyncedExpressionPortrait(name);
|
||||||
|
saveChatData();
|
||||||
|
refreshExpressionConsumers();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingSpeakerName = name;
|
||||||
|
|
||||||
|
// After a speaker switch, SillyTavern may briefly keep showing the previous
|
||||||
|
// speaker's expression. Wait for the widget to actually change before storing.
|
||||||
|
if (pendingSpeakerBaselineSignature && state.signature === pendingSpeakerBaselineSignature && previous !== state.src) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous === state.src && lastCapturedExpressionSrc === state.src) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCapturedExpressionSrc = state.src;
|
||||||
|
pendingSpeakerBaselineSignature = null;
|
||||||
|
setSyncedExpressionPortrait(name, state.src);
|
||||||
|
saveChatData();
|
||||||
|
refreshExpressionConsumers();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeExpressionImage(img) {
|
||||||
|
if (!shouldRunExpressionObservers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!img || observedExpressionImage === img) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expressionImageObserver) {
|
||||||
|
expressionImageObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
observedExpressionImage = img;
|
||||||
|
expressionImageObserver = new MutationObserver(() => {
|
||||||
|
captureExpressionForSpeaker(pendingSpeakerName, pendingCaptureRequestId);
|
||||||
|
});
|
||||||
|
|
||||||
|
expressionImageObserver.observe(img, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['src', 'class', 'style', 'title', 'data-expression', 'data-sprite-folder-name', 'data-sprite-filename']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureExpressionObservers() {
|
||||||
|
syncNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (!shouldRunExpressionObservers()) {
|
||||||
|
teardownExpressionObservers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentImg = findExpressionImageElement(pendingSpeakerName);
|
||||||
|
if (currentImg) {
|
||||||
|
observeExpressionImage(currentImg);
|
||||||
|
} else if (expressionImageObserver) {
|
||||||
|
expressionImageObserver.disconnect();
|
||||||
|
expressionImageObserver = null;
|
||||||
|
observedExpressionImage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expressionContainerObserver) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expressionContainerObserver = new MutationObserver(() => {
|
||||||
|
if (!shouldRunExpressionObservers()) {
|
||||||
|
teardownExpressionObservers();
|
||||||
|
syncNativeExpressionDisplayVisibility();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = findExpressionImageElement(pendingSpeakerName);
|
||||||
|
if (img) {
|
||||||
|
observeExpressionImage(img);
|
||||||
|
captureExpressionForSpeaker(pendingSpeakerName, pendingCaptureRequestId);
|
||||||
|
} else if (expressionImageObserver) {
|
||||||
|
expressionImageObserver.disconnect();
|
||||||
|
expressionImageObserver = null;
|
||||||
|
observedExpressionImage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncNativeExpressionDisplayVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
expressionContainerObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearScheduledCaptures() {
|
||||||
|
for (const timer of scheduledCaptureTimers) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledCaptureTimers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queueExpressionCaptureForSpeaker(speakerName) {
|
||||||
|
if (!extensionSettings.enabled || !extensionSettings.syncExpressionsToPresentCharacters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingSpeakerName = normalizeName(speakerName || getLatestAssistantSpeakerName());
|
||||||
|
if (!pendingSpeakerName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentImg = findExpressionImageElement(pendingSpeakerName);
|
||||||
|
const currentState = getExpressionImageState(currentImg);
|
||||||
|
pendingSpeakerBaselineSignature = currentState?.signature || null;
|
||||||
|
pendingSpeakerQueuedAt = Date.now();
|
||||||
|
pendingCaptureRequestId += 1;
|
||||||
|
const requestId = pendingCaptureRequestId;
|
||||||
|
|
||||||
|
ensureExpressionObservers();
|
||||||
|
clearScheduledCaptures();
|
||||||
|
|
||||||
|
for (const delay of [50, 200, 500, 900, 1500, 2200]) {
|
||||||
|
const timer = setTimeout(() => captureExpressionForSpeaker(pendingSpeakerName, requestId), delay);
|
||||||
|
scheduledCaptureTimers.push(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncExpressionFromLatestMessage() {
|
||||||
|
if (!extensionSettings.enabled || !extensionSettings.syncExpressionsToPresentCharacters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueExpressionCaptureForSpeaker(getLatestAssistantSpeakerName());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initExpressionSync() {
|
||||||
|
if (purgeInvalidSyncedExpressionPortraits()) {
|
||||||
|
refreshExpressionConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureExpressionObservers();
|
||||||
|
|
||||||
|
if (extensionSettings.syncExpressionsToPresentCharacters) {
|
||||||
|
syncExpressionFromLatestMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onExpressionSyncChatChanged() {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const purged = purgeInvalidSyncedExpressionPortraits();
|
||||||
|
if (purged) {
|
||||||
|
refreshExpressionConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryDelays = [0, 80, 220, 500];
|
||||||
|
for (const delay of retryDelays) {
|
||||||
|
setTimeout(() => {
|
||||||
|
ensureExpressionObservers();
|
||||||
|
syncNativeExpressionDisplayVisibility();
|
||||||
|
if (extensionSettings.syncExpressionsToPresentCharacters) {
|
||||||
|
syncExpressionFromLatestMessage();
|
||||||
|
} else {
|
||||||
|
refreshExpressionConsumers();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onExpressionSyncSettingChanged(enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
const purged = purgeInvalidSyncedExpressionPortraits();
|
||||||
|
initExpressionSync();
|
||||||
|
if (!purged) {
|
||||||
|
refreshExpressionConsumers();
|
||||||
|
}
|
||||||
|
syncExpressionFromLatestMessage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureExpressionObservers();
|
||||||
|
clearScheduledCaptures();
|
||||||
|
pendingCaptureRequestId += 1;
|
||||||
|
pendingSpeakerName = null;
|
||||||
|
pendingSpeakerBaselineSignature = null;
|
||||||
|
pendingSpeakerQueuedAt = 0;
|
||||||
|
lastCapturedExpressionSrc = null;
|
||||||
|
refreshExpressionConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onHideDefaultExpressionDisplaySettingChanged(enabled) {
|
||||||
|
extensionSettings.hideDefaultExpressionDisplay = enabled === true;
|
||||||
|
ensureExpressionObservers();
|
||||||
|
syncNativeExpressionDisplayVisibility();
|
||||||
|
setTimeout(() => syncNativeExpressionDisplayVisibility(), 0);
|
||||||
|
setTimeout(() => syncNativeExpressionDisplayVisibility(), 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearExpressionSyncCache() {
|
||||||
|
clearScheduledCaptures();
|
||||||
|
pendingCaptureRequestId += 1;
|
||||||
|
pendingSpeakerName = null;
|
||||||
|
pendingSpeakerBaselineSignature = null;
|
||||||
|
pendingSpeakerQueuedAt = 0;
|
||||||
|
lastCapturedExpressionSrc = null;
|
||||||
|
teardownExpressionObservers();
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { extensionSettings } from '../../core/state.js';
|
import { extensionSettings } from '../../core/state.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { getExpressionPortraitForCharacter } from '../integration/expressionSync.js';
|
||||||
import {
|
import {
|
||||||
getPresentCharactersTrackerData,
|
getPresentCharactersTrackerData,
|
||||||
parsePresentCharacters,
|
parsePresentCharacters,
|
||||||
@@ -132,7 +133,9 @@ export function renderAlternatePresentCharacters({ useCommittedFallback = true }
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
for (const character of presentCharacters) {
|
for (const character of presentCharacters) {
|
||||||
const portrait = resolvePresentCharacterPortrait(character.name);
|
const portrait = (extensionSettings.syncExpressionsToPresentCharacters
|
||||||
|
? getExpressionPortraitForCharacter(character.name)
|
||||||
|
: null) || resolvePresentCharacterPortrait(character.name);
|
||||||
const name = escapeHtml(character.name || '');
|
const name = escapeHtml(character.name || '');
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
$infoBoxContainer,
|
$infoBoxContainer,
|
||||||
$thoughtsContainer,
|
$thoughtsContainer,
|
||||||
$userStatsContainer,
|
$userStatsContainer,
|
||||||
|
clearSyncedExpressionPortraits,
|
||||||
setPendingDiceRoll,
|
setPendingDiceRoll,
|
||||||
getPendingDiceRoll,
|
getPendingDiceRoll,
|
||||||
clearSessionAvatarPrompts
|
clearSessionAvatarPrompts
|
||||||
@@ -370,6 +371,7 @@ export function setupSettingsPopup() {
|
|||||||
|
|
||||||
// Clear session avatar prompts
|
// Clear session avatar prompts
|
||||||
clearSessionAvatarPrompts();
|
clearSessionAvatarPrompts();
|
||||||
|
clearSyncedExpressionPortraits();
|
||||||
|
|
||||||
// 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();
|
||||||
|
|||||||
@@ -367,6 +367,24 @@
|
|||||||
Display a compact Present Characters panel below the chat.
|
Display a compact Present Characters panel below the chat.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-sync-expressions" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.syncBelowChatPresentCharactersExpressions">Sync Expressions in Below-Chat Panel</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote">
|
||||||
|
Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.
|
||||||
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user