// Parser für Lyras LLM-JSON-Antworten + Emotion-Detection. import { EMPATHY_RE, HAPPY_RE } from './sosConstants'; import type { Emotion } from '../components/RiveAvatar'; export type LyraEmotion = Emotion; export type ChipSpec = { label: string; action: string }; // Parse LLM JSON response — robust gegen Markdown-Fences UND abgeschnittenes JSON export function parseLyraResponse(raw: string): { message: string; chips: ChipSpec[] } { if (!raw) return { message: '', chips: [] }; // Strip ALL markdown fences (auch wenn nur am Anfang) const text = raw.trim() .replace(/^```(?:json)?\s*/i, '') .replace(/```\s*$/i, '') .trim(); const start = text.indexOf('{'); if (start === -1) return { message: raw.trim(), chips: [] }; // Erst echtes JSON-Parse versuchen (komplett) const end = text.lastIndexOf('}'); if (end > start) { try { const obj = JSON.parse(text.slice(start, end + 1)); const message = typeof obj.message === 'string' ? obj.message.trim() : ''; const chipsRaw = Array.isArray(obj.chips) ? obj.chips : []; const chips: ChipSpec[] = chipsRaw .filter((c: { label?: unknown; action?: unknown }) => c && typeof c.label === 'string' && typeof c.action === 'string') .slice(0, 5) .map((c: { label: string; action: string }) => ({ label: c.label.trim(), action: c.action.trim() })); if (message) return { message, chips }; } catch {/* fall through to recovery */} } // RECOVERY: JSON ist abgeschnitten (z.B. max_tokens hit). // 1) message-Feld per Regex extrahieren const msgMatch = text.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"/); let message = ''; if (msgMatch) { try { message = JSON.parse('"' + msgMatch[1] + '"'); } catch { message = msgMatch[1]; } } // 2) Chips: alle vollständigen {label,action}-Objekte einsammeln const chips: ChipSpec[] = []; const chipRe = /\{\s*"label"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"action"\s*:\s*"((?:[^"\\]|\\.)*)"\s*\}/g; let m: RegExpExecArray | null; while ((m = chipRe.exec(text)) !== null && chips.length < 5) { try { chips.push({ label: JSON.parse('"' + m[1] + '"').trim(), action: JSON.parse('"' + m[2] + '"').trim(), }); } catch { chips.push({ label: m[1].trim(), action: m[2].trim() }); } } if (message) return { message, chips }; return { message: raw.trim(), chips: [] }; } export function detectEmotion(text: string): LyraEmotion { if (HAPPY_RE.test(text)) return 'happy'; if (EMPATHY_RE.test(text)) return 'empathy'; return 'idle'; }