import { page } from "$app/state"; import { goto } from "$app/navigation"; import { base } from "$app/paths"; import { browser } from "$app/environment"; import superjson from "superjson"; import type { Message, MetacognitiveEvent } from "$lib/types/Message"; import type { MetacognitiveConfig, MetacognitivePromptData } from "$lib/types/Metacognitive"; import { determineMetacognitivePrompt } from "$lib/utils/metacognitiveLogic"; interface MetacognitiveSettings { activePersonas?: string[]; personas?: Array<{ id: string; name?: string }>; } interface UseMetacognitiveEngineProps { messages: Message[]; loading: boolean; pending: boolean; metacognitiveConfig?: MetacognitiveConfig; metacognitiveState?: { targetFrequency?: number; lastPromptedAtMessageId?: string | null; }; userSettings: MetacognitiveSettings; onmetacognitivebranch?: ( messageId: string, personaId: string, promptData: MetacognitivePromptData ) => void; } export function useMetacognitiveEngine(props: () => UseMetacognitiveEngineProps) { let metacognitiveTargetFrequency = $state(null); let metacognitiveLastPromptedAtMessageId = $state(null); let metacognitivePromptDismissedForMessageId = $state(null); let activeMetacognitivePrompt = $state(null); let lastProcessedMessageId = $state(null); let promptGenerationTimeout: ReturnType | undefined = $state(); // Cache generated prompts to prevent regeneration with different random values const promptCache = new Map(); let lastMessageCount = 0; // Initialize from server state $effect(() => { const { metacognitiveConfig, metacognitiveState, messages } = props(); if (!metacognitiveConfig?.enabled) { return; } if (metacognitiveState?.targetFrequency && metacognitiveTargetFrequency === null) { metacognitiveTargetFrequency = metacognitiveState.targetFrequency; } if ( metacognitiveState?.lastPromptedAtMessageId !== undefined && metacognitiveLastPromptedAtMessageId === null ) { metacognitiveLastPromptedAtMessageId = metacognitiveState.lastPromptedAtMessageId ?? null; } // Clear cache if messages array was replaced (e.g., navigation to different conversation) if (messages.length < lastMessageCount) { promptCache.clear(); } lastMessageCount = messages.length; // Populate cache from server-loaded events to prevent regeneration messages.forEach((msg) => { if (msg.from === "assistant" && msg.metacognitiveEvents?.length && !promptCache.has(msg.id)) { const event = msg.metacognitiveEvents[msg.metacognitiveEvents.length - 1]; promptCache.set(msg.id, { type: event.type, promptText: event.promptText, triggerFrequency: event.triggerFrequency, suggestedPersonaId: event.suggestedPersonaId, suggestedPersonaName: event.suggestedPersonaName, messageId: msg.id, }); } }); }); function persistPromptShownEvent(lastMessage: Message, prompt: MetacognitivePromptData) { if (!prompt) return; if (lastMessage.from !== "assistant") return; if (lastMessage.metacognitiveEvents?.length) return; const eventData: MetacognitiveEvent = { type: prompt.type, promptText: prompt.promptText, triggerFrequency: prompt.triggerFrequency, suggestedPersonaId: prompt.suggestedPersonaId, suggestedPersonaName: prompt.suggestedPersonaName, timestamp: new Date(), accepted: false, }; // Defensive copy using structuredClone const cleanEventData = structuredClone(eventData); // Immediately persist locally to prevent race conditions lastMessage.metacognitiveEvents = [cleanEventData]; metacognitiveLastPromptedAtMessageId = lastMessage.id; // Ensure cache reflects persisted state promptCache.set(lastMessage.id, prompt); // Force message update in parent if needed if (!browser || !page.params.id) return; (async () => { try { const response = await fetch( `${base}/api/v2/conversations/${page.params.id}/message/${lastMessage.id}/metacognitive-event`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: prompt.type, promptText: prompt.promptText, triggerFrequency: prompt.triggerFrequency, suggestedPersonaId: prompt.suggestedPersonaId, suggestedPersonaName: prompt.suggestedPersonaName, accepted: false, }), } ); if (!response.ok) { console.error("Failed to log metacognitive prompt shown event:", response.status); return; } const parsed = superjson.parse(await response.text()) as { nextTargetFrequency?: number; }; if (parsed.nextTargetFrequency) { metacognitiveTargetFrequency = parsed.nextTargetFrequency; } } catch (e) { console.error("Failed to log metacognitive prompt shown event:", e); } })(); } $effect(() => { const { messages, loading, pending, metacognitiveConfig, userSettings } = props(); if (!metacognitiveConfig?.enabled) { activeMetacognitivePrompt = null; return; } const lastMessage = messages[messages.length - 1]; if (!lastMessage || lastMessage.from !== "assistant") { activeMetacognitivePrompt = null; if (promptGenerationTimeout) { clearTimeout(promptGenerationTimeout); promptGenerationTimeout = undefined; } return; } if (activeMetacognitivePrompt && activeMetacognitivePrompt.messageId !== lastMessage.id) { activeMetacognitivePrompt = null; } if (loading || pending) { if (promptGenerationTimeout) { clearTimeout(promptGenerationTimeout); promptGenerationTimeout = undefined; } return; } if (lastMessage.metacognitiveEvents?.length) { lastProcessedMessageId = lastMessage.id; return; } if (lastProcessedMessageId === lastMessage.id) { return; } if (!promptGenerationTimeout) { promptGenerationTimeout = setTimeout(() => { promptGenerationTimeout = undefined; const currentMessages = props().messages; const currentLast = currentMessages[currentMessages.length - 1]; if (!currentLast || currentLast.id !== lastMessage.id) return; if (props().loading || props().pending) return; lastProcessedMessageId = currentLast.id; if (currentLast.metacognitiveEvents?.length) return; // Check cache first to ensure immutability let prompt = promptCache.get(currentLast.id); if (prompt === undefined) { // Only generate if not cached prompt = determineMetacognitivePrompt( currentMessages, metacognitiveConfig, { dismissedForMessageId: metacognitivePromptDismissedForMessageId || undefined, targetFrequency: metacognitiveTargetFrequency || undefined, }, { activePersonas: userSettings.activePersonas, personas: userSettings.personas, } ); // Cache the result (even if null) to prevent regeneration promptCache.set(currentLast.id, prompt); } if (prompt && prompt.messageId === currentLast.id) { persistPromptShownEvent(currentLast, prompt); activeMetacognitivePrompt = prompt; } else { activeMetacognitivePrompt = null; } }, 1000); } }); function handleMetacognitiveAction(messageId?: string, data?: MetacognitivePromptData) { const { messages, onmetacognitivebranch } = props(); let promptData = data; let targetMessageIndex = messages.length - 1; if (messageId) { const idx = messages.findIndex((m) => m.id === messageId); if (idx !== -1) { targetMessageIndex = idx; const msg = messages[idx]; if (msg.metacognitiveEvents?.length) { const event = msg.metacognitiveEvents.find((e) => e.type === "perspective"); if (event) { event.accepted = true; // Trigger update in UI? The object is mutated. promptData = { type: event.type, promptText: event.promptText, triggerFrequency: event.triggerFrequency, suggestedPersonaId: event.suggestedPersonaId, suggestedPersonaName: event.suggestedPersonaName, messageId: msg.id, linkedMessageId: event.linkedMessageId, }; } } } } if (!promptData) { if (activeMetacognitivePrompt) { if (!messageId || activeMetacognitivePrompt.messageId === messageId) { promptData = activeMetacognitivePrompt; } } } if (!promptData || promptData.type !== "perspective") { return; } if (promptData.linkedMessageId) { const url = new URL(window.location.href); url.searchParams.set("msgId", promptData.linkedMessageId); url.searchParams.set("scrollTo", "true"); goto(url.toString(), { replaceState: false, noScroll: true }); return; } const targetMessage = messages[targetMessageIndex]; if (!targetMessage) return; let previousUserMessageId: string | null = null; for (let i = targetMessageIndex; i >= 0; i--) { if (messages[i].from === "user") { previousUserMessageId = messages[i].id; break; } } if (!previousUserMessageId || !promptData.suggestedPersonaId) { return; } if (targetMessageIndex === messages.length - 1) { metacognitivePromptDismissedForMessageId = targetMessage.id; } onmetacognitivebranch?.(previousUserMessageId, promptData.suggestedPersonaId, promptData); } return { get activeMetacognitivePrompt() { return activeMetacognitivePrompt; }, handleMetacognitiveAction, }; }