feat: message interception
This commit is contained in:
@@ -155,6 +155,10 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
// console.log('[RPG Companion] Parsed data:', parsedData);
|
||||
// console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null');
|
||||
|
||||
// Legacy text parsing does not provide structured characters; clear stale structured data
|
||||
extensionSettings.charactersData = [];
|
||||
const parsedCharacterThoughts = parsedData.characterThoughts || '';
|
||||
|
||||
// DON'T update lastGeneratedData here - it should only reflect the data
|
||||
// from the assistant message the user replied to, not auto-generated updates
|
||||
// This ensures swipes/regenerations use consistent source data
|
||||
@@ -174,7 +178,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
||||
userStats: parsedData.userStats,
|
||||
infoBox: parsedData.infoBox,
|
||||
characterThoughts: parsedData.characterThoughts
|
||||
characterThoughts: parsedCharacterThoughts
|
||||
};
|
||||
|
||||
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
||||
@@ -190,9 +194,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
if (parsedData.infoBox) {
|
||||
lastGeneratedData.infoBox = parsedData.infoBox;
|
||||
}
|
||||
if (parsedData.characterThoughts) {
|
||||
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
||||
}
|
||||
lastGeneratedData.characterThoughts = parsedCharacterThoughts;
|
||||
// console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', {
|
||||
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
|
||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
||||
@@ -211,13 +213,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
if (!hasAnyCommittedContent) {
|
||||
committedTrackerData.userStats = parsedData.userStats;
|
||||
committedTrackerData.infoBox = parsedData.infoBox;
|
||||
committedTrackerData.characterThoughts = parsedData.characterThoughts;
|
||||
committedTrackerData.characterThoughts = parsedCharacterThoughts;
|
||||
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
|
||||
}
|
||||
|
||||
// Render the updated data
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
lastGeneratedData.characterThoughts = parsedCharacterThoughts;
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
@@ -228,6 +231,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
}
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
lastGeneratedData.characterThoughts = parsedCharacterThoughts;
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Supports both legacy text format and new JSON format
|
||||
*/
|
||||
|
||||
import { extensionSettings, FEATURE_FLAGS, addDebugLog, lastGeneratedData } from '../../core/state.js';
|
||||
import { extensionSettings, FEATURE_FLAGS, addDebugLog, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||
import { extractInventory } from './inventoryParser.js';
|
||||
import { validateTrackerData, mergeTrackerData } from '../../types/trackerData.js';
|
||||
@@ -317,47 +317,52 @@ export function parseJSONTrackerData(jsonData) {
|
||||
}
|
||||
|
||||
// Parse characters - store for UI rendering AND generate text format for thought bubbles
|
||||
if (jsonData.characters && Array.isArray(jsonData.characters)) {
|
||||
extensionSettings.charactersData = jsonData.characters;
|
||||
debugLog('[RPG Parser] Characters:', jsonData.characters.length);
|
||||
const parsedCharacters = Array.isArray(jsonData.characters) ? jsonData.characters : [];
|
||||
extensionSettings.charactersData = parsedCharacters;
|
||||
debugLog('[RPG Parser] Characters:', parsedCharacters.length);
|
||||
|
||||
// Generate text format for lastGeneratedData.characterThoughts (needed for thought bubbles)
|
||||
const config = extensionSettings.trackerConfig?.presentCharacters;
|
||||
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
|
||||
const lines = [];
|
||||
for (const char of parsedCharacters) {
|
||||
// Character name line
|
||||
lines.push(`- ${char.name || 'Unknown'}`);
|
||||
|
||||
// Generate text format for lastGeneratedData.characterThoughts (needed for thought bubbles)
|
||||
const config = extensionSettings.trackerConfig?.presentCharacters;
|
||||
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
|
||||
const lines = [];
|
||||
for (const char of jsonData.characters) {
|
||||
// Character name line
|
||||
lines.push(`- ${char.name || 'Unknown'}`);
|
||||
|
||||
// Details line with emoji and fields
|
||||
const details = [char.emoji || '😶'];
|
||||
const charFields = char.fields || {};
|
||||
for (const [key, value] of Object.entries(charFields)) {
|
||||
if (value) details.push(`${key}: ${value}`);
|
||||
}
|
||||
lines.push(`Details: ${details.join(' | ')}`);
|
||||
|
||||
// Relationship line
|
||||
if (char.relationship) {
|
||||
lines.push(`Relationship: ${char.relationship}`);
|
||||
}
|
||||
|
||||
// Stats line
|
||||
const charStats = char.stats || {};
|
||||
if (Object.keys(charStats).length > 0) {
|
||||
const statsStr = Object.entries(charStats).map(([k, v]) => `${k}: ${v}%`).join(' | ');
|
||||
lines.push(`Stats: ${statsStr}`);
|
||||
}
|
||||
|
||||
// Thoughts line
|
||||
if (char.thoughts) {
|
||||
lines.push(`${thoughtsFieldName}: ${char.thoughts}`);
|
||||
}
|
||||
// Details line with emoji and fields
|
||||
const details = [char.emoji || '😶'];
|
||||
const charFields = char.fields || {};
|
||||
for (const [key, value] of Object.entries(charFields)) {
|
||||
if (value) details.push(`${key}: ${value}`);
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
lastGeneratedData.characterThoughts = lines.join('\n');
|
||||
debugLog('[RPG Parser] Generated text format for characterThoughts');
|
||||
lines.push(`Details: ${details.join(' | ')}`);
|
||||
|
||||
// Relationship line
|
||||
if (char.relationship) {
|
||||
lines.push(`Relationship: ${char.relationship}`);
|
||||
}
|
||||
|
||||
// Stats line
|
||||
const charStats = char.stats || {};
|
||||
if (Object.keys(charStats).length > 0) {
|
||||
const statsStr = Object.entries(charStats).map(([k, v]) => `${k}: ${v}%`).join(' | ');
|
||||
lines.push(`Stats: ${statsStr}`);
|
||||
}
|
||||
|
||||
// Thoughts line
|
||||
if (char.thoughts) {
|
||||
lines.push(`${thoughtsFieldName}: ${char.thoughts}`);
|
||||
}
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
lastGeneratedData.characterThoughts = lines.join('\n');
|
||||
committedTrackerData.characterThoughts = lines.join('\n');
|
||||
debugLog('[RPG Parser] Generated text format for characterThoughts');
|
||||
} else {
|
||||
// No characters provided in the JSON response - clear any stale state/UI data
|
||||
lastGeneratedData.characterThoughts = '';
|
||||
committedTrackerData.characterThoughts = '';
|
||||
debugLog('[RPG Parser] No characters present; cleared characterThoughts state');
|
||||
}
|
||||
|
||||
// Parse inventory (structured format)
|
||||
|
||||
@@ -24,11 +24,17 @@ export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, an
|
||||
*/
|
||||
export const DEFAULT_JSON_TRACKER_PROMPT = `At the start of every reply, output a JSON object inside a markdown code fence (with \`\`\`json). This tracks {{user}}'s stats, inventory, skills, and scene information. Follow the exact schema shown below. Use concrete values - no placeholders or brackets. Update stats realistically based on actions and time (0% change for minutes, 1-5% normally, 5%+ only for major events). Items and skills have "name" and "description" fields. Items can grant skills via "grantsSkill", and skills show their source via "grantedBy".`;
|
||||
|
||||
/**
|
||||
* Default message interception prompt text
|
||||
* Guides the LLM to rewrite the user's message based on current RPG state and recent chat
|
||||
*/
|
||||
export const DEFAULT_MESSAGE_INTERCEPTION_PROMPT = `Act as an uncompromising Immersive Copy Editor who rewrites the user's draft to strictly adhere to {{user}}'s persona and RPG state (JSON). You must validate the feasibility of the user's intended actions against the JSON state; if the draft contradicts the state (e.g., acting smart while 'Intelligence' is low, or running while having a 'Leg Injury'), you are required to override the core intent, rewriting the action to portray immediate failure, struggle, or involuntary reaction instead of the user's desired success. Even further, if the intended course of action is physically impossible via the state or represents a thought process conceptually alien to the character's nature or current state, you are mandated to completely overwrite the user's intent. Aggressively rephrase vocabulary and syntax to match the character's specific cognitive capacity and tone. Keep the output concise and devoid of fluff; do not expand the narrative beyond the necessary state-enforced correction. Return ONLY the modified message text.`;
|
||||
|
||||
/**
|
||||
* Gets character card information for current chat (handles both single and group chats)
|
||||
* @returns {string} Formatted character information
|
||||
*/
|
||||
async function getCharacterCardsInfo() {
|
||||
export async function getCharacterCardsInfo() {
|
||||
let characterInfo = '';
|
||||
|
||||
// Check if in group chat
|
||||
@@ -196,6 +202,7 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
const showSkills = extensionSettings.showSkills;
|
||||
const showQuests = extensionSettings.showQuests;
|
||||
const enableItemSkillLinks = extensionSettings.enableItemSkillLinks;
|
||||
const deleteSkillWithItem = extensionSettings.deleteSkillWithItem;
|
||||
|
||||
const hasAnyTrackers = showStats || showInfoBox || showCharacters || showInventory || showSkills || showQuests;
|
||||
|
||||
@@ -400,8 +407,9 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
instructions += '- Level is a numeric value (typically 1+, represents character progression)\n';
|
||||
}
|
||||
|
||||
instructions += '- Characters should be removed as soon as they leave the scene\n';
|
||||
instructions += '- Your list of characters must never include {{user}}\n';
|
||||
instructions += '- Empty arrays [] for sections with no items\n';
|
||||
instructions += '- Items may be added or removed from all sections\n';
|
||||
instructions += '- null for main quest if none active\n';
|
||||
|
||||
// Add stat descriptions if any have descriptions
|
||||
@@ -438,7 +446,9 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
if (enableItemSkillLinks) {
|
||||
instructions += '- Items can grant skills: add {"grantsSkill": "Skill Name"} to the item object\n';
|
||||
instructions += '- When a skill comes from an item, add {"grantedBy": "Item Name"} to that skill object\n';
|
||||
instructions += '- If an item is removed/lost, also remove any skill it granted\n';
|
||||
if (deleteSkillWithItem) {
|
||||
instructions += '- If an item is removed/lost, also remove any skill it granted\n';
|
||||
}
|
||||
}
|
||||
|
||||
instructions += '\n';
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
*/
|
||||
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { chat, user_avatar, setExtensionPrompt, extension_prompt_types, updateMessageBlock } from '../../../../../../../script.js';
|
||||
import {
|
||||
chat,
|
||||
user_avatar,
|
||||
setExtensionPrompt,
|
||||
extension_prompt_types,
|
||||
updateMessageBlock,
|
||||
generateRaw
|
||||
} from '../../../../../../../script.js';
|
||||
|
||||
// Core modules
|
||||
import {
|
||||
@@ -14,15 +21,14 @@ import {
|
||||
lastActionWasSwipe,
|
||||
isPlotProgression,
|
||||
setLastActionWasSwipe,
|
||||
setIsPlotProgression,
|
||||
updateLastGeneratedData,
|
||||
updateCommittedTrackerData
|
||||
setIsPlotProgression
|
||||
} from '../../core/state.js';
|
||||
import { saveChatData, loadChatData } from '../../core/persistence.js';
|
||||
|
||||
// Generation & Parsing
|
||||
import { parseResponse, parseUserStats, parseSkills, tryParseJSONResponse } from '../generation/parser.js';
|
||||
import { updateRPGData } from '../generation/apiClient.js';
|
||||
import { generateContextualSummary, DEFAULT_MESSAGE_INTERCEPTION_PROMPT } from '../generation/promptBuilder.js';
|
||||
|
||||
// Rendering
|
||||
import { renderUserStats } from '../rendering/userStats.js';
|
||||
@@ -76,13 +82,22 @@ export function commitTrackerData() {
|
||||
* Sets the flag to indicate this is NOT a swipe.
|
||||
* In separate mode with auto-update disabled, commits the displayed tracker data.
|
||||
*/
|
||||
export function onMessageSent() {
|
||||
export async function onMessageSent() {
|
||||
if (!extensionSettings.enabled) return;
|
||||
|
||||
// User sent a new message - NOT a swipe
|
||||
setLastActionWasSwipe(false);
|
||||
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent - lastActionWasSwipe =', lastActionWasSwipe);
|
||||
|
||||
// Optionally intercept and rewrite the user message via LLM
|
||||
if (extensionSettings.enableMessageInterception && extensionSettings.messageInterceptionActive !== false) {
|
||||
try {
|
||||
await interceptAndModifyUserMessage();
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Message interception failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// In separate mode with auto-update disabled, commit displayed tracker when user sends a message
|
||||
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
||||
// Commit whatever is currently displayed in lastGeneratedData
|
||||
@@ -99,6 +114,84 @@ export function onMessageSent() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts the last user message, asks the LLM to rewrite it using RPG state and recent chat,
|
||||
* and updates the chat/DOM with the modified content.
|
||||
*/
|
||||
async function interceptAndModifyUserMessage() {
|
||||
const context = getContext();
|
||||
const chatHistory = context.chat || chat;
|
||||
|
||||
if (!chatHistory || chatHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMessage = chatHistory[chatHistory.length - 1];
|
||||
if (!lastMessage || !lastMessage.is_user) {
|
||||
return; // Only rewrite user messages
|
||||
}
|
||||
|
||||
const originalText = lastMessage.mes || '';
|
||||
const stateJson = generateContextualSummary();
|
||||
const depth = extensionSettings.messageInterceptionContextDepth || extensionSettings.updateDepth || 4;
|
||||
const startIndex = Math.max(0, chatHistory.length - 1 - depth);
|
||||
const recentMessages = chatHistory.slice(startIndex, chatHistory.length - 1);
|
||||
|
||||
const recentContext = recentMessages
|
||||
.map((m) => {
|
||||
const role = m.is_system ? 'system' : m.is_user ? '{{user}}' : '{{char}}';
|
||||
const content = (m.mes || '').replace(/\s+/g, ' ').trim();
|
||||
return `- ${role}: ${content}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const basePrompt =
|
||||
(extensionSettings.customMessageInterceptionPrompt || '').trim() ||
|
||||
DEFAULT_MESSAGE_INTERCEPTION_PROMPT;
|
||||
|
||||
const promptMessages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: basePrompt
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
content: `{{user}}'s persona definition:\n{{persona}}`
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
content: `Current RPG state (JSON):\n${stateJson ? `\`\`\`json\n${stateJson}\n\`\`\`` : 'None'}`
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
content: `Recent messages (newest last):\n${recentContext || 'None'}`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `User draft message:\n${originalText}\n\nReturn only the modified message text.`
|
||||
}
|
||||
];
|
||||
|
||||
const response = await generateRaw({
|
||||
prompt: promptMessages,
|
||||
quietToLoud: false
|
||||
});
|
||||
|
||||
if (!response || typeof response !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleaned = response.trim();
|
||||
if (!cleaned) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update chat history and DOM
|
||||
lastMessage.mes = cleaned;
|
||||
const messageId = chatHistory.length - 1;
|
||||
updateMessageBlock(messageId, lastMessage, { rerenderMessage: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for when a message is generated.
|
||||
*/
|
||||
@@ -137,6 +230,10 @@ export async function onMessageReceived(data) {
|
||||
const parsedData = parseResponse(responseText);
|
||||
// console.log('[RPG Companion] Parsed data:', parsedData);
|
||||
|
||||
// Legacy text parsing does not produce structured characters; clear old state to avoid stale UI/state
|
||||
extensionSettings.charactersData = [];
|
||||
const parsedCharacterThoughts = parsedData.characterThoughts || '';
|
||||
|
||||
// Update stored data
|
||||
if (parsedData.userStats) {
|
||||
lastGeneratedData.userStats = parsedData.userStats;
|
||||
@@ -148,9 +245,9 @@ export async function onMessageReceived(data) {
|
||||
if (parsedData.infoBox) {
|
||||
lastGeneratedData.infoBox = parsedData.infoBox;
|
||||
}
|
||||
if (parsedData.characterThoughts) {
|
||||
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
||||
}
|
||||
|
||||
// Response omitted characters section - clear any previous thoughts to reflect removal
|
||||
lastGeneratedData.characterThoughts = parsedCharacterThoughts;
|
||||
|
||||
// Store RPG data for this specific swipe in the message's extra field
|
||||
if (!lastMessage.extra) {
|
||||
@@ -164,7 +261,7 @@ export async function onMessageReceived(data) {
|
||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
||||
userStats: parsedData.userStats,
|
||||
infoBox: parsedData.infoBox,
|
||||
characterThoughts: parsedData.characterThoughts
|
||||
characterThoughts: parsedCharacterThoughts
|
||||
};
|
||||
|
||||
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
|
||||
@@ -173,7 +270,7 @@ export async function onMessageReceived(data) {
|
||||
if (!committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts) {
|
||||
committedTrackerData.userStats = parsedData.userStats;
|
||||
committedTrackerData.infoBox = parsedData.infoBox;
|
||||
committedTrackerData.characterThoughts = parsedData.characterThoughts;
|
||||
committedTrackerData.characterThoughts = parsedCharacterThoughts;
|
||||
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
|
||||
} else {
|
||||
// console.log('[RPG Companion] Data will be committed when user replies');
|
||||
|
||||
@@ -163,7 +163,8 @@ export function renderThoughts() {
|
||||
const hasRelationshipEnabled = relationshipFields.length > 0;
|
||||
|
||||
// Convert structured character data to text format for the original fancy renderer
|
||||
let characterThoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts || '';
|
||||
// Use nullish coalescing so an empty string from the latest response clears UI
|
||||
let characterThoughtsData = lastGeneratedData.characterThoughts ?? committedTrackerData.characterThoughts ?? '';
|
||||
|
||||
// If we have structured data, convert it to text format
|
||||
if (extensionSettings.charactersData && Array.isArray(extensionSettings.charactersData) && extensionSettings.charactersData.length > 0) {
|
||||
|
||||
@@ -234,6 +234,13 @@ function renderUserStatsTab() {
|
||||
html += `<label for="rpg-show-rpg-attrs">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.enableRpgAttributes')}</label>`;
|
||||
html += '</div>';
|
||||
|
||||
// Allow AI to update attributes toggle
|
||||
const allowAIUpdateAttributes = config.allowAIUpdateAttributes !== undefined ? config.allowAIUpdateAttributes : true;
|
||||
html += '<div class="rpg-editor-toggle-row">';
|
||||
html += `<input type="checkbox" id="rpg-allow-ai-update-attrs" ${allowAIUpdateAttributes ? 'checked' : ''}>`;
|
||||
html += `<label for="rpg-allow-ai-update-attrs">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes')}</label>`;
|
||||
html += '</div>';
|
||||
|
||||
// Always send attributes toggle
|
||||
const alwaysSendAttributes = config.alwaysSendAttributes !== undefined ? config.alwaysSendAttributes : false;
|
||||
html += '<div class="rpg-editor-toggle-row">';
|
||||
@@ -242,14 +249,6 @@ function renderUserStatsTab() {
|
||||
html += '</div>';
|
||||
html += `<small class="rpg-editor-note">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote')}</small>`;
|
||||
|
||||
// Allow AI to update attributes toggle
|
||||
const allowAIUpdateAttributes = config.allowAIUpdateAttributes !== undefined ? config.allowAIUpdateAttributes : true;
|
||||
html += '<div class="rpg-editor-toggle-row">';
|
||||
html += `<input type="checkbox" id="rpg-allow-ai-update-attrs" ${allowAIUpdateAttributes ? 'checked' : ''}>`;
|
||||
html += `<label for="rpg-allow-ai-update-attrs">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes')}</label>`;
|
||||
html += '</div>';
|
||||
html += `<small class="rpg-editor-note">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.allowAIUpdateAttributesNote')}</small>`;
|
||||
|
||||
html += '<div class="rpg-editor-stats-list" id="rpg-editor-attrs-list">';
|
||||
|
||||
// Ensure rpgAttributes exists in the actual config (not just local fallback)
|
||||
|
||||
Reference in New Issue
Block a user