Merge branch 'main' into main
This commit is contained in:
@@ -24,3 +24,4 @@ node_modules/
|
|||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
yarn.lock
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
setMusicPlayerContainer,
|
setMusicPlayerContainer,
|
||||||
clearSessionAvatarPrompts
|
clearSessionAvatarPrompts
|
||||||
} from './src/core/state.js';
|
} from './src/core/state.js';
|
||||||
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js';
|
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData, commitTrackerDataFromPriorMessage } from './src/core/persistence.js';
|
||||||
import { registerAllEvents } from './src/core/events.js';
|
import { registerAllEvents } from './src/core/events.js';
|
||||||
|
|
||||||
// Generation & Parsing modules
|
// Generation & Parsing modules
|
||||||
@@ -151,6 +151,7 @@ import {
|
|||||||
onMessageReceived,
|
onMessageReceived,
|
||||||
onCharacterChanged,
|
onCharacterChanged,
|
||||||
onMessageSwiped,
|
onMessageSwiped,
|
||||||
|
onMessageDeleted,
|
||||||
updatePersonaAvatar,
|
updatePersonaAvatar,
|
||||||
clearExtensionPrompts,
|
clearExtensionPrompts,
|
||||||
onGenerationEnded,
|
onGenerationEnded,
|
||||||
@@ -799,6 +800,17 @@ async function initUI() {
|
|||||||
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const currentChat = getContext().chat;
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastAssistantIndex !== -1) {
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -807,6 +819,17 @@ async function initUI() {
|
|||||||
if (!extensionSettings.enabled) {
|
if (!extensionSettings.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const currentChat = getContext().chat;
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastAssistantIndex !== -1) {
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1354,6 +1377,7 @@ jQuery(async () => {
|
|||||||
[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.MESSAGE_SWIPED]: onMessageSwiped,
|
||||||
|
[event_types.MESSAGE_DELETED]: onMessageDeleted,
|
||||||
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
||||||
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"js": "index.js",
|
"js": "index.js",
|
||||||
"css": "style.css",
|
"css": "style.css",
|
||||||
"author": "Marinara",
|
"author": "Marinara",
|
||||||
"version": "3.7.2",
|
"version": "3.7.3",
|
||||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "rpg-companion-sillytavern",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "rpg-complanion-sillytavern",
|
||||||
|
"version": "3.7.3",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"validate_locale": "node src/i18n/validator.js --watch",
|
||||||
|
"validate_locale_once": "node src/i18n/validator.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"chokidar": "^5.0.0",
|
||||||
|
"fs-extra": "^11.3.3",
|
||||||
|
"glob": "^13.0.6"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
+142
-1
@@ -224,6 +224,26 @@ export function saveChatData() {
|
|||||||
saveChatDebounced();
|
saveChatDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors a tracker data entry into message.swipe_info so it survives page reloads.
|
||||||
|
* ST only serializes swipe_info to disk; message.extra is in-memory only.
|
||||||
|
* Guard: skips silently if swipe_info[swipeId] doesn't exist yet
|
||||||
|
*
|
||||||
|
* @param {Object} message - The chat message object
|
||||||
|
* @param {number} swipeId - The swipe index to mirror into
|
||||||
|
* @param {Object} swipeEntry - { userStats, infoBox, characterThoughts }
|
||||||
|
*/
|
||||||
|
export function mirrorToSwipeInfo(message, swipeId, swipeEntry) {
|
||||||
|
if (!message.swipe_info || !message.swipe_info[swipeId]) return;
|
||||||
|
if (!message.swipe_info[swipeId].extra) {
|
||||||
|
message.swipe_info[swipeId].extra = {};
|
||||||
|
}
|
||||||
|
if (!message.swipe_info[swipeId].extra.rpg_companion_swipes) {
|
||||||
|
message.swipe_info[swipeId].extra.rpg_companion_swipes = {};
|
||||||
|
}
|
||||||
|
message.swipe_info[swipeId].extra.rpg_companion_swipes[swipeId] = swipeEntry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the last assistant message's swipe data with current tracker data.
|
* Updates the last assistant message's swipe data with current tracker data.
|
||||||
* This ensures user edits are preserved across swipes and included in generation context.
|
* This ensures user edits are preserved across swipes and included in generation context.
|
||||||
@@ -247,11 +267,15 @@ export function updateMessageSwipeData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
message.extra.rpg_companion_swipes[swipeId] = {
|
const swipeEntry = {
|
||||||
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;
|
||||||
@@ -259,6 +283,123 @@ export function updateMessageSwipeData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads RPG tracker data for a specific swipe from a message.
|
||||||
|
* Checks message.extra first (in-memory, current session), then message.swipe_info
|
||||||
|
* (serialized by SillyTavern on save, available after page reload).
|
||||||
|
*
|
||||||
|
* @param {Object} message - The chat message object
|
||||||
|
* @param {number} swipeId - The swipe index to read
|
||||||
|
* @returns {{userStats, infoBox, characterThoughts}|null} The swipe data or null
|
||||||
|
*/
|
||||||
|
export function getSwipeData(message, swipeId) {
|
||||||
|
// Primary: in-memory extra (current session or after a recent write)
|
||||||
|
const fromExtra = message.extra?.rpg_companion_swipes?.[swipeId];
|
||||||
|
if (fromExtra) return fromExtra;
|
||||||
|
|
||||||
|
// Fallback: swipe_info (populated by ST when loading from disk)
|
||||||
|
const fromSwipeInfo = message.swipe_info?.[swipeId]?.extra?.rpg_companion_swipes?.[swipeId];
|
||||||
|
if (fromSwipeInfo) return fromSwipeInfo;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits tracker data from the assistant message immediately before currentMessageIndex.
|
||||||
|
* Walks backward through the chat skipping the current message, user messages, and system
|
||||||
|
* messages until it finds the prior assistant message, then loads its active swipe data.
|
||||||
|
* If no prior assistant message exists or exists without a tracker state, nulls out all fields so
|
||||||
|
* the AI generates from an empty context rather than a ghost state.
|
||||||
|
*
|
||||||
|
* @param {number} currentMessageIndex - Index of the message to start searching before
|
||||||
|
*/
|
||||||
|
export function commitTrackerDataFromPriorMessage(currentMessageIndex) {
|
||||||
|
const chat = getContext().chat;
|
||||||
|
if (!chat || chat.length === 0) {
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('[RPG Companion] commitTrackerDataFromPriorMessage called with index', currentMessageIndex, '| chat.length =', chat.length);
|
||||||
|
|
||||||
|
for (let i = currentMessageIndex - 1; i >= 0; i--) {
|
||||||
|
const message = chat[i];
|
||||||
|
if (message.is_user || message.is_system) continue;
|
||||||
|
|
||||||
|
// Found the prior assistant message — commit its active swipe data
|
||||||
|
const swipeId = message.swipe_id || 0;
|
||||||
|
const swipeData = getSwipeData(message, swipeId);
|
||||||
|
// console.log('[RPG Companion] Committing from chat[' + i + '] swipe', swipeId, '| has swipe data:', !!swipeData);
|
||||||
|
committedTrackerData.userStats = swipeData?.userStats || null;
|
||||||
|
committedTrackerData.infoBox = swipeData?.infoBox || null;
|
||||||
|
const rawCharacterThoughts = swipeData?.characterThoughts;
|
||||||
|
committedTrackerData.characterThoughts =
|
||||||
|
rawCharacterThoughts == null
|
||||||
|
? null
|
||||||
|
: (typeof rawCharacterThoughts === 'string'
|
||||||
|
? rawCharacterThoughts
|
||||||
|
: JSON.stringify(rawCharacterThoughts));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No prior assistant message found — use empty context
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates a message's current swipe slot with tracker data inherited from the
|
||||||
|
* nearest prior assistant message, when no tracker data has been generated for
|
||||||
|
* this swipe yet (e.g. auto-update is disabled).
|
||||||
|
*
|
||||||
|
* This ensures that commitTrackerDataFromPriorMessage can always find a tracker
|
||||||
|
* state to commit when the user sends the next message, rather than nulling
|
||||||
|
* everything out and resetting the tracker display to empty.
|
||||||
|
*
|
||||||
|
* Does nothing if the current swipe already has its own tracker data.
|
||||||
|
*
|
||||||
|
* @param {Object} message - The assistant message object to inherit into
|
||||||
|
* @param {number} messageIndex - Index of that message in chat
|
||||||
|
* @returns {boolean} True if inheritance was written, false otherwise
|
||||||
|
*/
|
||||||
|
export function inheritSwipeDataFromPriorMessage(message, messageIndex) {
|
||||||
|
const chat = getContext().chat;
|
||||||
|
if (!chat) return false;
|
||||||
|
|
||||||
|
const currentSwipeId = message.swipe_id || 0;
|
||||||
|
|
||||||
|
// Don't overwrite if this swipe already has its own tracker data.
|
||||||
|
if (getSwipeData(message, currentSwipeId)) return false;
|
||||||
|
|
||||||
|
// Walk backward to find the nearest prior assistant message with swipe data.
|
||||||
|
for (let i = messageIndex - 1; i >= 0; i--) {
|
||||||
|
const msg = chat[i];
|
||||||
|
if (msg.is_user || msg.is_system) continue;
|
||||||
|
|
||||||
|
const swipeId = msg.swipe_id || 0;
|
||||||
|
const swipeData = getSwipeData(msg, swipeId);
|
||||||
|
if (!swipeData) continue; // No data on this assistant message; keep searching further back
|
||||||
|
|
||||||
|
// Write inherited data into this swipe slot.
|
||||||
|
if (!message.extra) message.extra = {};
|
||||||
|
if (!message.extra.rpg_companion_swipes) message.extra.rpg_companion_swipes = {};
|
||||||
|
|
||||||
|
const inherited = {
|
||||||
|
userStats: swipeData.userStats,
|
||||||
|
infoBox: swipeData.infoBox,
|
||||||
|
characterThoughts: swipeData.characterThoughts
|
||||||
|
};
|
||||||
|
message.extra.rpg_companion_swipes[currentSwipeId] = inherited;
|
||||||
|
mirrorToSwipeInfo(message, currentSwipeId, inherited);
|
||||||
|
// console.log('[RPG Companion] Inherited tracker data from chat[' + i + '] into current swipe slot', currentSwipeId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads RPG data from the current chat's metadata.
|
* Loads RPG data from the current chat's metadata.
|
||||||
* Automatically migrates v1 inventory to v2 format if needed.
|
* Automatically migrates v1 inventory to v2 format if needed.
|
||||||
|
|||||||
@@ -381,6 +381,32 @@ export let isPlotProgression = false;
|
|||||||
*/
|
*/
|
||||||
export let isAwaitingNewMessage = false;
|
export let isAwaitingNewMessage = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monotonically-increasing counter used to detect stale separate-mode tracker
|
||||||
|
* generation results. Incremented each time a new automated generation is
|
||||||
|
* triggered or a message deletion occurs so any in-flight (or pending) call
|
||||||
|
* from a previous generation can recognise that its result is no longer valid.
|
||||||
|
*/
|
||||||
|
let separateGenerationId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current separate generation ID.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function getSeparateGenerationId() {
|
||||||
|
return separateGenerationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments and returns the new separate generation ID.
|
||||||
|
* Call this when starting a new generation or when a deletion
|
||||||
|
* invalidates any pending/in-flight generation.
|
||||||
|
* @returns {number} The new ID
|
||||||
|
*/
|
||||||
|
export function incrementSeparateGenerationId() {
|
||||||
|
return ++separateGenerationId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary storage for pending dice roll (not saved until user clicks "Save Roll")
|
* Temporary storage for pending dice roll (not saved until user clicks "Save Roll")
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
const glob = require('glob');
|
||||||
|
|
||||||
|
const COMPILED_DIR = __dirname // path.join(__dirname, 'compiled');
|
||||||
|
|
||||||
|
function findUnlocalizedText() {
|
||||||
|
const srcArg = process.argv.find(arg => arg.startsWith('--src='));
|
||||||
|
const srcDir = srcArg ? srcArg.split('=')[1] : '.';
|
||||||
|
|
||||||
|
console.log(`\n🔎 Scanning for unlocalized text in ${srcDir}...`);
|
||||||
|
|
||||||
|
const files = glob.sync(`${srcDir}/**/*.{html,js,jsx}`, {
|
||||||
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('⚠️ No .html/.js/.jsx files found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalFound = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const relPath = path.relative(process.cwd(), file);
|
||||||
|
|
||||||
|
// Searching for string number
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
let match;
|
||||||
|
const localPattern = /<([a-zA-Z][a-zA-Z0-9]*)(?:\s(?:[^>](?!data-i18n-key))*)?>([\p{L}\p{N}\s\-.,!?:'"()]+)<\/\1>/gu;
|
||||||
|
|
||||||
|
while ((match = localPattern.exec(line)) !== null) {
|
||||||
|
const text = match[2].trim();
|
||||||
|
if (!text) continue;
|
||||||
|
|
||||||
|
// Passing JSX expressions like {someVar}
|
||||||
|
if (text.includes('{') || text.includes('}')) continue;
|
||||||
|
|
||||||
|
// Passing if tag has data-i18n-key
|
||||||
|
if (match[0].includes('data-i18n-key')) continue;
|
||||||
|
|
||||||
|
console.log(` - ${relPath}:${index + 1} — <${match[1]}> "${text}"`);
|
||||||
|
totalFound++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalFound === 0) {
|
||||||
|
console.log('✅ No unlocalized text found!');
|
||||||
|
} else {
|
||||||
|
console.log(`\n📋 Found ${totalFound} potentially unlocalized text node(s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Function to validate translations
|
||||||
|
function validateTranslations() {
|
||||||
|
console.log('🔍 Validating translation files...');
|
||||||
|
|
||||||
|
// Parse --locales=en,fr argument
|
||||||
|
const localesArg = process.argv.find(arg => arg.startsWith('--locales='));
|
||||||
|
const selectedLocales = localesArg
|
||||||
|
? localesArg.split('=')[1].split(',').map(l => l.trim())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(COMPILED_DIR)
|
||||||
|
.filter(file => file.endsWith('.json'))
|
||||||
|
.filter(file => {
|
||||||
|
const locale = path.basename(file, '.json');
|
||||||
|
return !selectedLocales || selectedLocales.includes(locale);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('⚠️ No compiled translation files found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all translation data
|
||||||
|
const translations = {};
|
||||||
|
for (const file of files) {
|
||||||
|
const locale = path.basename(file, '.json');
|
||||||
|
const filePath = path.join(COMPILED_DIR, file);
|
||||||
|
translations[locale] = fs.readJsonSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all locales
|
||||||
|
const locales = Object.keys(translations);
|
||||||
|
console.log(`📁 Found ${locales.length} locales: ${locales.join(', ')}`);
|
||||||
|
|
||||||
|
if (locales.length < 2) {
|
||||||
|
console.log('⚠️ Need at least 2 locales to compare');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose the first locale as reference
|
||||||
|
const referenceLocale = locales[0];
|
||||||
|
console.log(`🔑 Using ${referenceLocale} as reference locale`);
|
||||||
|
|
||||||
|
// Get all keys from reference locale
|
||||||
|
const referenceKeys = Object.keys(translations[referenceLocale]);
|
||||||
|
console.log(`🔢 Reference locale has ${referenceKeys.size} unique keys`);
|
||||||
|
|
||||||
|
// Track statistics
|
||||||
|
const stats = {
|
||||||
|
missingKeys: {},
|
||||||
|
extraKeys: {},
|
||||||
|
typeErrors: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize stats for each locale
|
||||||
|
for (const locale of locales) {
|
||||||
|
if (locale !== referenceLocale) {
|
||||||
|
stats.missingKeys[locale] = [];
|
||||||
|
stats.extraKeys[locale] = [];
|
||||||
|
stats.typeErrors[locale] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each locale against the reference
|
||||||
|
for (const locale of locales) {
|
||||||
|
if (locale === referenceLocale) continue;
|
||||||
|
|
||||||
|
const localeKeys = Object.keys(translations[locale]);
|
||||||
|
|
||||||
|
// Check for missing keys
|
||||||
|
for (const key of referenceKeys) {
|
||||||
|
if (!key in translations[locale]) {
|
||||||
|
stats.missingKeys[locale].push(key);
|
||||||
|
} else {
|
||||||
|
// Check for type mismatches
|
||||||
|
const refValue = translations[referenceLocale][key];
|
||||||
|
const localeValue = translations[locale][key];
|
||||||
|
|
||||||
|
if (typeof refValue !== typeof localeValue) {
|
||||||
|
stats.typeErrors[locale].push({
|
||||||
|
key,
|
||||||
|
refType: typeof refValue,
|
||||||
|
localeType: typeof localeValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for extra keys
|
||||||
|
for (const key of localeKeys) {
|
||||||
|
if (!key in translations[referenceLocale]) {
|
||||||
|
stats.extraKeys[locale].push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print results
|
||||||
|
let hasIssues = false;
|
||||||
|
|
||||||
|
// Print missing keys
|
||||||
|
for (const locale in stats.missingKeys) {
|
||||||
|
const missing = stats.missingKeys[locale];
|
||||||
|
if (missing.length > 0) {
|
||||||
|
hasIssues = true;
|
||||||
|
console.log(`❌ ${locale} is missing ${missing.length} keys:`);
|
||||||
|
missing.forEach(key => {
|
||||||
|
console.log(` - ${key}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print extra keys
|
||||||
|
for (const locale in stats.extraKeys) {
|
||||||
|
const extra = stats.extraKeys[locale];
|
||||||
|
if (extra.length > 0) {
|
||||||
|
hasIssues = true;
|
||||||
|
console.log(`⚠️ ${locale} has ${extra.length} extra keys:`);
|
||||||
|
extra.forEach(key => {
|
||||||
|
console.log(` - ${key}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print type errors
|
||||||
|
for (const locale in stats.typeErrors) {
|
||||||
|
const typeErrors = stats.typeErrors[locale];
|
||||||
|
if (typeErrors.length > 0) {
|
||||||
|
hasIssues = true;
|
||||||
|
console.log(`⚠️ ${locale} has ${typeErrors.length} type mismatches:`);
|
||||||
|
typeErrors.forEach(err => {
|
||||||
|
console.log(` - ${err.key}: expected ${err.refType}, got ${err.localeType}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print empty values check if needed
|
||||||
|
console.log('\n📊 Checking for empty values...');
|
||||||
|
for (const locale of locales) {
|
||||||
|
checkEmptyValues(translations[locale], locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasIssues) {
|
||||||
|
console.log('✅ All locales have consistent structure!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasIssues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check for empty values
|
||||||
|
function checkEmptyValues(obj, locale, prefix = '') {
|
||||||
|
for (const key in obj) {
|
||||||
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
if (value === '') {
|
||||||
|
console.log(`⚠️ ${locale} has empty string at ${fullKey}`);
|
||||||
|
} else if (value === null) {
|
||||||
|
console.log(`⚠️ ${locale} has null value at ${fullKey}`);
|
||||||
|
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
checkEmptyValues(value, locale, fullKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
function main() {
|
||||||
|
// Create compiled directory if it doesn't exist
|
||||||
|
fs.ensureDirSync(COMPILED_DIR);
|
||||||
|
|
||||||
|
// Run validation
|
||||||
|
validateTranslations();
|
||||||
|
|
||||||
|
// Find unlocalized text
|
||||||
|
findUnlocalizedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch mode
|
||||||
|
if (process.argv.includes('--watch')) {
|
||||||
|
console.log('👀 Watching for changes...');
|
||||||
|
|
||||||
|
let debounceTimer;
|
||||||
|
const debounceDelay = 100;
|
||||||
|
|
||||||
|
// Initial validation
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
main();
|
||||||
|
}, debounceDelay);
|
||||||
|
|
||||||
|
// Watch for changes in the compiled directory
|
||||||
|
chokidar.watch(COMPILED_DIR, {
|
||||||
|
ignoreInitial: true,
|
||||||
|
ignored: /.*~$/, // Игнорировать скрытые файлы
|
||||||
|
}).on('all', (event, path) => {
|
||||||
|
if (event === 'change' || event === 'add' || event === 'unlink') {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
console.log(`🔁 Detected changes in ${path} (${event}), revalidating...`);
|
||||||
|
main();
|
||||||
|
}, debounceDelay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Run once
|
||||||
|
main();
|
||||||
|
}
|
||||||
@@ -18,9 +18,10 @@ import {
|
|||||||
lastActionWasSwipe,
|
lastActionWasSwipe,
|
||||||
setIsGenerating,
|
setIsGenerating,
|
||||||
setLastActionWasSwipe,
|
setLastActionWasSwipe,
|
||||||
$musicPlayerContainer
|
$musicPlayerContainer,
|
||||||
|
getSeparateGenerationId
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData } from '../../core/persistence.js';
|
import { saveChatData, mirrorToSwipeInfo } from '../../core/persistence.js';
|
||||||
import {
|
import {
|
||||||
generateSeparateUpdatePrompt
|
generateSeparateUpdatePrompt
|
||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
@@ -218,7 +219,7 @@ export async function switchToPreset(presetName) {
|
|||||||
* @param {Function} renderThoughts - UI function to render character thoughts
|
* @param {Function} renderThoughts - UI function to render character thoughts
|
||||||
* @param {Function} renderInventory - UI function to render inventory
|
* @param {Function} renderInventory - UI function to render inventory
|
||||||
*/
|
*/
|
||||||
export async function updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory) {
|
export async function updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, generationId = null) {
|
||||||
if (isGenerating) {
|
if (isGenerating) {
|
||||||
// console.log('[RPG Companion] Already generating, skipping...');
|
// console.log('[RPG Companion] Already generating, skipping...');
|
||||||
return;
|
return;
|
||||||
@@ -262,6 +263,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a generationId was provided and the counter has since been incremented
|
||||||
|
// (by a deletion or a newer generation), discard this result entirely.
|
||||||
|
// The finally block still runs to restore button state.
|
||||||
|
if (generationId !== null && getSeparateGenerationId() !== generationId) {
|
||||||
|
// console.log('[RPG Companion] ⚠️ Separate generation result discarded — superseded (genId', generationId, '!= current', getSeparateGenerationId(), ')');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
// console.log('[RPG Companion] Raw AI response:', response);
|
// console.log('[RPG Companion] Raw AI response:', response);
|
||||||
const parsedData = parseResponse(response);
|
const parsedData = parseResponse(response);
|
||||||
@@ -317,11 +326,15 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = lastMessage.swipe_id || 0;
|
const currentSwipeId = lastMessage.swipe_id || 0;
|
||||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
const swipeEntry = {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
SPOTIFY_FORMAT_INSTRUCTION
|
SPOTIFY_FORMAT_INSTRUCTION
|
||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||||
|
import { commitTrackerDataFromPriorMessage } from '../../core/persistence.js';
|
||||||
|
|
||||||
// Track suppression state for event handler
|
// Track suppression state for event handler
|
||||||
let currentSuppressionState = false;
|
let currentSuppressionState = false;
|
||||||
@@ -622,25 +623,15 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
|
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
|
||||||
|
|
||||||
if (shouldCommit) {
|
if (shouldCommit) {
|
||||||
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing data from BEFORE user message');
|
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing from N-1 assistant message');
|
||||||
// console.log('[RPG Companion] Chat length:', currentChatLength, 'Last committed:', lastCommittedChatLength);
|
// console.log('[RPG Companion] Chat length:', currentChatLength, 'Last committed:', lastCommittedChatLength);
|
||||||
// console.log('[RPG Companion] BEFORE: committedTrackerData =', {
|
|
||||||
// userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : 'null'
|
|
||||||
// // });
|
|
||||||
// console.log('[RPG Companion] BEFORE: lastGeneratedData =', {
|
|
||||||
// userStats: lastGeneratedData.userStats ? `${lastGeneratedData.userStats.substring(0, 50)}...` : 'null',
|
|
||||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: lastGeneratedData.characterThoughts ? `${lastGeneratedData.characterThoughts.substring(0, 100)}...` : 'null'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Commit displayed data (from before user sent message)
|
// Commit from the prior assistant message's swipe store (N-1 rule).
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
// currentChatLength - 1 is the new AI placeholder; the function walks backward
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
// past it and the user message to find the previous AI message's tracker state.
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
commitTrackerDataFromPriorMessage(currentChatLength - 1);
|
||||||
|
|
||||||
// Track chat length to prevent duplicate commits
|
// Track chat length to prevent duplicate commits from streaming
|
||||||
lastCommittedChatLength = currentChatLength;
|
lastCommittedChatLength = currentChatLength;
|
||||||
|
|
||||||
// console.log('[RPG Companion] AFTER: committedTrackerData =', {
|
// console.log('[RPG Companion] AFTER: committedTrackerData =', {
|
||||||
@@ -668,38 +659,14 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
|
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
|
||||||
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
|
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
|
||||||
if (!lastActionWasSwipe) {
|
if (!lastActionWasSwipe) {
|
||||||
// User sent a new message - commit lastGeneratedData before generation
|
// User sent a new message - commit from the prior assistant message's swipe store
|
||||||
// console.log('[RPG Companion] 📝 COMMIT: New message - committing lastGeneratedData');
|
// (N-1 rule) rather than lastGeneratedData, which may reflect a sibling swipe's
|
||||||
// console.log('[RPG Companion] BEFORE commit - committedTrackerData:', {
|
// outcome and would poison the context for the new generation.
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
// currentChatLength - 1 is the new AI placeholder; search starts before it.
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
commitTrackerDataFromPriorMessage(currentChatLength - 1);
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// // });
|
|
||||||
// console.log('[RPG Companion] BEFORE commit - lastGeneratedData:', {
|
|
||||||
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
|
||||||
// console.log('[RPG Companion] AFTER commit - committedTrackerData:', {
|
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Reset flag after committing (ready for next cycle)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// console.log('[RPG Companion] 🔄 SWIPE: Using existing committedTrackerData (no commit)');
|
|
||||||
// console.log('[RPG Companion] committedTrackerData:', {
|
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
// Reset flag after using it (swipe generation complete, ready for next action)
|
|
||||||
}
|
}
|
||||||
|
// If lastActionWasSwipe, context was already committed by commitTrackerDataFromPriorMessage
|
||||||
|
// in onMessageSwiped before generation started.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the committed tracker data as source for generation
|
// Use the committed tracker data as source for generation
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ import {
|
|||||||
setIsAwaitingNewMessage,
|
setIsAwaitingNewMessage,
|
||||||
updateLastGeneratedData,
|
updateLastGeneratedData,
|
||||||
updateCommittedTrackerData,
|
updateCommittedTrackerData,
|
||||||
$musicPlayerContainer
|
$musicPlayerContainer,
|
||||||
|
incrementSeparateGenerationId
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData, loadChatData, autoSwitchPresetForEntity } from '../../core/persistence.js';
|
import { saveChatData, loadChatData, autoSwitchPresetForEntity, getSwipeData, commitTrackerDataFromPriorMessage, inheritSwipeDataFromPriorMessage, mirrorToSwipeInfo } from '../../core/persistence.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
// Generation & Parsing
|
// Generation & Parsing
|
||||||
@@ -51,6 +52,45 @@ 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the swipe store of the last assistant message in `currentChat` and
|
||||||
|
* writes its data into `lastGeneratedData`, including syncing stat bars via
|
||||||
|
* `parseUserStats`. If no assistant message exists, or none has stored swipe
|
||||||
|
* data, `lastGeneratedData` is left unchanged.
|
||||||
|
*
|
||||||
|
* Use this wherever the displayed tracker state must be re-derived from the
|
||||||
|
* authoritative swipe store rather than from chat_metadata (e.g. after a
|
||||||
|
* CHAT_CHANGED caused by branching, or after a message deletion).
|
||||||
|
*
|
||||||
|
* @param {Array} currentChat - Live chat array from getContext().chat
|
||||||
|
* @returns {boolean} True if swipe data was found and applied
|
||||||
|
*/
|
||||||
|
function syncLastGeneratedDataFromSwipeStore(currentChat) {
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
const msg = currentChat[i];
|
||||||
|
if (!msg.is_user && !msg.is_system) {
|
||||||
|
const swipeId = msg.swipe_id || 0;
|
||||||
|
const swipeData = getSwipeData(msg, swipeId);
|
||||||
|
if (swipeData) {
|
||||||
|
lastGeneratedData.userStats = swipeData.userStats || null;
|
||||||
|
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||||||
|
// Normalize characterThoughts to string (backward compat with old object format).
|
||||||
|
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
|
||||||
|
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
|
||||||
|
} else {
|
||||||
|
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||||||
|
}
|
||||||
|
if (swipeData.userStats) {
|
||||||
|
parseUserStats(swipeData.userStats);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false; // Last assistant message exists but has no swipe data yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // No assistant messages in chat
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commits the tracker data from the last assistant message to be used as source for next generation.
|
* Commits the tracker data from the last assistant message to be used as source for next generation.
|
||||||
* This should be called when the user has replied to a message, ensuring all swipes of the next
|
* This should be called when the user has replied to a message, ensuring all swipes of the next
|
||||||
@@ -65,22 +105,28 @@ export function commitTrackerData() {
|
|||||||
// 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 - commit its tracker data
|
// Found last assistant message - commit its tracker data
|
||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
const swipeData = message.extra.rpg_companion_swipes[swipeId];
|
const swipeData = getSwipeData(message, swipeId);
|
||||||
|
|
||||||
if (swipeData) {
|
if (swipeData) {
|
||||||
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
|
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
|
||||||
committedTrackerData.userStats = swipeData.userStats || null;
|
committedTrackerData.userStats = swipeData.userStats || null;
|
||||||
committedTrackerData.infoBox = swipeData.infoBox || null;
|
committedTrackerData.infoBox = swipeData.infoBox || null;
|
||||||
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
|
const rawCharacterThoughts = swipeData.characterThoughts;
|
||||||
|
if (rawCharacterThoughts == null) {
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
} else if (typeof rawCharacterThoughts === 'object') {
|
||||||
|
committedTrackerData.characterThoughts = JSON.stringify(rawCharacterThoughts);
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] No swipe data found for swipe', swipeId);
|
committedTrackerData.characterThoughts = String(rawCharacterThoughts);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] No RPG data found in last assistant message');
|
// No saved swipe data — treat as empty (e.g. first message, no prior generation)
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -118,17 +164,6 @@ export function onMessageSent() {
|
|||||||
// Note: FAB spinning is NOT shown for together mode since no extra API request is made
|
// Note: FAB spinning is NOT shown for together mode since no extra API request is made
|
||||||
// The RPG data comes embedded in the main response
|
// The RPG data comes embedded in the main response
|
||||||
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
||||||
|
|
||||||
// For separate mode with auto-update disabled, commit displayed tracker
|
|
||||||
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
|
||||||
if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) {
|
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] 💾 SEPARATE MODE: Committed displayed tracker (auto-update disabled)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -193,11 +228,15 @@ export async function onMessageReceived(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = lastMessage.swipe_id || 0;
|
const currentSwipeId = lastMessage.swipe_id || 0;
|
||||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
const swipeEntry = {
|
||||||
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);
|
||||||
|
|
||||||
@@ -261,13 +300,26 @@ export async function onMessageReceived(data) {
|
|||||||
// Just render the music player
|
// Just render the music player
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When auto-update is disabled, no tracker API call will run for this message.
|
||||||
|
// Inherit the prior assistant message's tracker data into this swipe slot so that
|
||||||
|
// commitTrackerDataFromPriorMessage can find a valid state next turn instead of nulling everything.
|
||||||
|
// Inheritance does not overwrite existing data, so it's safe to call even if the condition misses an edge case.
|
||||||
|
if (!extensionSettings.autoUpdate || !isAwaitingNewMessage) {
|
||||||
|
inheritSwipeDataFromPriorMessage(lastMessage, chat.length - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger auto-update if enabled (for both separate and external modes)
|
// Trigger auto-update if enabled (for both separate and external modes)
|
||||||
// Only trigger if this is a newly generated message, not loading chat history
|
// Only trigger if this is a newly generated message, not loading chat history
|
||||||
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
||||||
|
// Capture the current generation ID before the async gap so that any
|
||||||
|
// message deletion (or a newer generation) that increments the counter
|
||||||
|
// while the 500ms timer or the API call is in-flight will cause
|
||||||
|
// updateRPGData to discard its result rather than stomping the UI.
|
||||||
|
const genId = incrementSeparateGenerationId();
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, genId);
|
||||||
// Update FAB widgets and strip widgets after separate/external mode update completes
|
// Update FAB widgets and strip widgets after separate/external mode update completes
|
||||||
setFabLoadingState(false);
|
setFabLoadingState(false);
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
@@ -323,6 +375,22 @@ export function onCharacterChanged() {
|
|||||||
// Load chat-specific data when switching chats
|
// Load chat-specific data when switching chats
|
||||||
loadChatData();
|
loadChatData();
|
||||||
|
|
||||||
|
// chat_metadata may not reflect the actual chat tail for branches, so
|
||||||
|
// loadChatData() may have just restored stale data from the parent chat.
|
||||||
|
// Override lastGeneratedData from the swipe store of the last assistant message.
|
||||||
|
// The message objects in the branch already carry their full swipe stores, making this authoritative.
|
||||||
|
// If no swipe data exists (e.g. branching at message 0, or a chat with no generations yet),
|
||||||
|
// null out lastGeneratedData and committedTrackerData so we don't display stale values from the parent chat.
|
||||||
|
const hadSwipeData = syncLastGeneratedDataFromSwipeStore(getContext().chat);
|
||||||
|
if (!hadSwipeData) {
|
||||||
|
lastGeneratedData.userStats = null;
|
||||||
|
lastGeneratedData.infoBox = null;
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't call commitTrackerData() here - it would overwrite the loaded committedTrackerData
|
// Don't call commitTrackerData() here - it would overwrite the loaded committedTrackerData
|
||||||
// with data from the last message, which may be null/empty. The loaded committedTrackerData
|
// with data from the last message, which may be null/empty. The loaded committedTrackerData
|
||||||
// already contains the committed state from when we last left this chat.
|
// already contains the committed state from when we last left this chat.
|
||||||
@@ -378,6 +446,9 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
// This is a NEW swipe that will trigger generation
|
// This is a NEW swipe that will trigger generation
|
||||||
setLastActionWasSwipe(true);
|
setLastActionWasSwipe(true);
|
||||||
setIsAwaitingNewMessage(true);
|
setIsAwaitingNewMessage(true);
|
||||||
|
// Immediately commit context from the prior assistant message (N-1) so generation
|
||||||
|
// uses the world state before this message, not the last-viewed sibling swipe.
|
||||||
|
commitTrackerDataFromPriorMessage(messageIndex);
|
||||||
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
|
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
|
||||||
} else {
|
} else {
|
||||||
// This is navigating to an EXISTING swipe - don't change the flag
|
// This is navigating to an EXISTING swipe - don't change the flag
|
||||||
@@ -386,12 +457,12 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
|
|
||||||
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
||||||
|
|
||||||
// IMPORTANT: onMessageSwiped is for DISPLAY only!
|
// Load saved swipe data into both display (lastGeneratedData) and extensionSettings.
|
||||||
// lastGeneratedData is for DISPLAY, committedTrackerData is for GENERATION
|
// Safe to call parseUserStats() unconditionally because updateMessageSwipeData() is called
|
||||||
// It's safe to load swipe data into lastGeneratedData - it won't be committed due to !lastActionWasSwipe check
|
// on every manual edit, so the swipe store always reflects the latest user changes before
|
||||||
if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) {
|
// any navigation can overwrite them.
|
||||||
const swipeData = message.extra.rpg_companion_swipes[currentSwipeId];
|
const swipeData = getSwipeData(message, currentSwipeId);
|
||||||
|
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;
|
||||||
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||||||
@@ -403,13 +474,12 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DON'T parse user stats when loading swipe data
|
// Sync extensionSettings.userStats so stat bars reflect this swipe
|
||||||
// This would overwrite manually edited fields (like Conditions) with old swipe data
|
if (swipeData.userStats) {
|
||||||
// The lastGeneratedData is loaded for display purposes only
|
parseUserStats(swipeData.userStats);
|
||||||
// parseUserStats() updates extensionSettings.userStats which should only be modified
|
}
|
||||||
// by new generations or manual edits, not by swipe navigation
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
|
// console.log('[RPG Companion] 🔄 Loaded swipe data for swipe:', currentSwipeId);
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
||||||
}
|
}
|
||||||
@@ -422,10 +492,85 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
|
// Update widget strips with the newly loaded swipe data
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
// Update chat thought overlays
|
// Update chat thought overlays
|
||||||
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() {
|
||||||
|
if (!extensionSettings.enabled) return;
|
||||||
|
|
||||||
|
// console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted');
|
||||||
|
|
||||||
|
// Invalidate any pending or in-flight separate-mode generation so
|
||||||
|
// its result is not applied to the (now-changed) chat tail.
|
||||||
|
incrementSeparateGenerationId();
|
||||||
|
|
||||||
|
const currentChat = getContext().chat;
|
||||||
|
|
||||||
|
// Walk backward to find the new last assistant message.
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastAssistantIndex === -1) {
|
||||||
|
// 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,
|
||||||
|
// so any subsequent generation uses the correct N-1 world state.
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render all panels.
|
||||||
|
renderUserStats();
|
||||||
|
renderInfoBox();
|
||||||
|
renderThoughts();
|
||||||
|
renderInventory();
|
||||||
|
renderQuests();
|
||||||
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
|
// Update widget strips.
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
|
// Persist updated state.
|
||||||
|
saveChatData();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the persona avatar image when user switches personas
|
* Update the persona avatar image when user switches personas
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { renderUserStats } from '../rendering/userStats.js';
|
|||||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||||
import { renderThoughts } from '../rendering/thoughts.js';
|
import { renderThoughts } from '../rendering/thoughts.js';
|
||||||
import { updateFabWidgets } from './mobile.js';
|
import { updateFabWidgets } from './mobile.js';
|
||||||
|
import { safeToSnake } from '../../utils/transformations.js';
|
||||||
|
|
||||||
let $editorModal = null;
|
let $editorModal = null;
|
||||||
let activeTab = 'userStats';
|
let activeTab = 'userStats';
|
||||||
@@ -38,6 +39,36 @@ let tempConfig = null; // Temporary config for cancel functionality
|
|||||||
let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null }
|
let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null }
|
||||||
let originalAssociation = null; // Original association when editor opened
|
let originalAssociation = null; // Original association when editor opened
|
||||||
|
|
||||||
|
|
||||||
|
function set_ids_names(list_with_stats, index, value) {
|
||||||
|
list_with_stats[index].name = value;
|
||||||
|
const item = list_with_stats[index];
|
||||||
|
const oldId = item?.id;
|
||||||
|
|
||||||
|
item.name = value;
|
||||||
|
const ids = list_with_stats.filter((_, i) => i !== index).map(stat => stat.id);
|
||||||
|
const snake_value = safeToSnake(value); // new id format
|
||||||
|
if (snake_value !== value && !ids.includes(snake_value)) { // check if this id already exists
|
||||||
|
item.id = snake_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newId = item.id;
|
||||||
|
// If the ID changed, migrate any stored values keyed by the old ID
|
||||||
|
if (oldId && newId && oldId !== newId) {
|
||||||
|
if (extensionSettings.userStats && Object.prototype.hasOwnProperty.call(extensionSettings.userStats, oldId)) {
|
||||||
|
extensionSettings.userStats[newId] = extensionSettings.userStats[oldId];
|
||||||
|
delete extensionSettings.userStats[oldId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.classicStats && Object.prototype.hasOwnProperty.call(extensionSettings.classicStats, oldId)) {
|
||||||
|
extensionSettings.classicStats[newId] = extensionSettings.classicStats[oldId];
|
||||||
|
delete extensionSettings.classicStats[oldId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list_with_stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the tracker editor modal
|
* Initialize the tracker editor modal
|
||||||
*/
|
*/
|
||||||
@@ -885,7 +916,9 @@ function setupUserStatsListeners() {
|
|||||||
// Rename stat
|
// Rename stat
|
||||||
$('.rpg-stat-name').off('blur').on('blur', function () {
|
$('.rpg-stat-name').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
const index = $(this).data('index');
|
||||||
extensionSettings.trackerConfig.userStats.customStats[index].name = $(this).val();
|
const value = $(this).val();
|
||||||
|
const list_with_stats = extensionSettings.trackerConfig.userStats.customStats
|
||||||
|
set_ids_names(list_with_stats, index, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Change stat max value
|
// Change stat max value
|
||||||
@@ -943,7 +976,9 @@ function setupUserStatsListeners() {
|
|||||||
// Rename attribute
|
// Rename attribute
|
||||||
$('.rpg-attr-name').off('blur').on('blur', function () {
|
$('.rpg-attr-name').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
const index = $(this).data('index');
|
||||||
extensionSettings.trackerConfig.userStats.rpgAttributes[index].name = $(this).val();
|
const value = $(this).val();
|
||||||
|
const list_with_stats = extensionSettings.trackerConfig.userStats.rpgAttributes
|
||||||
|
set_ids_names(list_with_stats, index, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable/disable RPG Attributes section toggle
|
// Enable/disable RPG Attributes section toggle
|
||||||
@@ -1394,7 +1429,9 @@ function setupPresentCharactersListeners() {
|
|||||||
// Rename field
|
// Rename field
|
||||||
$('.rpg-field-label').off('blur').on('blur', function () {
|
$('.rpg-field-label').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
const index = $(this).data('index');
|
||||||
extensionSettings.trackerConfig.presentCharacters.customFields[index].name = $(this).val();
|
const value = $(this).val();
|
||||||
|
const list_with_stats = extensionSettings.trackerConfig.presentCharacters.customFields
|
||||||
|
set_ids_names(list_with_stats, index, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update description
|
// Update description
|
||||||
@@ -1443,7 +1480,9 @@ function setupPresentCharactersListeners() {
|
|||||||
// Rename character stat
|
// Rename character stat
|
||||||
$('.rpg-char-stat-label').off('blur').on('blur', function () {
|
$('.rpg-char-stat-label').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
const index = $(this).data('index');
|
||||||
extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].name = $(this).val();
|
const value = $(this).val();
|
||||||
|
const list_with_stats = extensionSettings.trackerConfig.presentCharacters.characterStats.customStats
|
||||||
|
set_ids_names(list_with_stats, index, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const toSnake = (str) => str
|
||||||
|
// replace any sequence of non-alphanumeric characters with a single underscore
|
||||||
|
.replace(/[^0-9A-Za-z]+/g, '_')
|
||||||
|
// insert underscore between a lower-case letter/digit and an upper-case letter (but not between consecutive uppers)
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||||
|
// collapse multiple underscores
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
// trim leading/trailing underscores
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
// finally, lowercase the result
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
export const safeToSnake = (str) => {
|
||||||
|
const res = toSnake(str);
|
||||||
|
return (res.length >= 2) ? res : str; // considering element with one symbol is too short to be safe
|
||||||
|
};
|
||||||
@@ -10736,7 +10736,10 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
|
|||||||
|
|
||||||
/* Features row container */
|
/* Features row container */
|
||||||
.rpg-features-row {
|
.rpg-features-row {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: min-content;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -10747,11 +10750,11 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Center items when they fit, allow scrolling when they don't */
|
/* Center items when they fit, allow scrolling when they don't */
|
||||||
.rpg-features-row::before,
|
/*.rpg-features-row::before,*/
|
||||||
.rpg-features-row::after {
|
/*.rpg-features-row::after {*/
|
||||||
content: '';
|
/* content: '';*/
|
||||||
margin: auto;
|
/* margin: auto;*/
|
||||||
}
|
/*}*/
|
||||||
|
|
||||||
/* Hide scrollbar for cleaner look while maintaining functionality */
|
/* Hide scrollbar for cleaner look while maintaining functionality */
|
||||||
.rpg-features-row::-webkit-scrollbar {
|
.rpg-features-row::-webkit-scrollbar {
|
||||||
|
|||||||
Reference in New Issue
Block a user