import { Language, SlideId } from '@/lib/types' import { SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation' import { PresenterState } from '@/lib/presenter/types' export interface ChatFABProps { lang: Language currentSlide: SlideId currentIndex: number visitedSlides: Set onGoToSlide: (index: number) => void presenterState?: PresenterState onPresenterInterrupt?: () => void } export interface ParsedMessage { text: string followUps: string[] gotos: { index: number; label: string }[] } export function parseAgentResponse(content: string, lang: Language): ParsedMessage { const followUps: string[] = [] const gotos: { index: number; label: string }[] = [] // Split on the follow-up separator — flexible: "---", "- - -", "___", or multiple dashes const parts = content.split(/\n\s*[-_]{3,}\s*\n/) let text = parts[0] // Parse follow-up questions from second part if (parts.length > 1) { const qSection = parts.slice(1).join('\n') // Match [Q], **[Q]**, or numbered/bulleted question patterns const qMatches = qSection.matchAll(/(?:\[Q\]|\*\*\[Q\]\*\*)\s*(.+?)(?:\n|$)/g) for (const m of qMatches) { const q = m[1].trim().replace(/^\*\*|\*\*$/g, '') if (q.length > 5) followUps.push(q) } // Fallback: if no [Q] markers found, look for numbered or bulleted questions in the section if (followUps.length === 0) { const lineMatches = qSection.matchAll(/(?:^|\n)\s*(?:\d+[\.\)]\s*|[-•]\s*)(.+?\?)\s*$/gm) for (const m of lineMatches) { const q = m[1].trim() if (q.length > 5 && followUps.length < 3) followUps.push(q) } } } // Also look for [Q] questions anywhere in the text (sometimes model puts them without ---) if (followUps.length === 0) { const inlineMatches = content.matchAll(/\[Q\]\s*(.+?)(?:\n|$)/g) const inlineQs: string[] = [] for (const m of inlineMatches) { inlineQs.push(m[1].trim()) } if (inlineQs.length >= 2) { followUps.push(...inlineQs) // Remove [Q] lines from main text text = text.replace(/\n?\s*\[Q\]\s*.+?(?:\n|$)/g, '\n').trim() } } // Parse GOTO markers — support both [GOTO:N] (numeric) and [GOTO:slide-id] (string) const gotoRegex = /\[GOTO:([\w-]+)\]/g let gotoMatch while ((gotoMatch = gotoRegex.exec(text)) !== null) { const target = gotoMatch[1] let slideIndex: number // Try numeric index first const numericIndex = parseInt(target) if (!isNaN(numericIndex) && numericIndex >= 0 && numericIndex < SLIDE_ORDER.length) { slideIndex = numericIndex } else { // Try slide ID lookup slideIndex = SLIDE_ORDER.indexOf(target as SlideId) } if (slideIndex >= 0) { gotos.push({ index: slideIndex, label: lang === 'de' ? `Zu Slide ${slideIndex + 1} springen` : `Jump to slide ${slideIndex + 1}`, }) } } // Remove GOTO markers from visible text text = text.replace(/\s*\[GOTO:[\w-]+\]/g, '') // Clean up trailing reminder instruction that might leak through text = text.replace(/\n*\(Erinnerung:.*?\)\s*$/s, '').trim() return { text: text.trim(), followUps, gotos } } /** * Clean text for TTS: remove markdown formatting, keep plain speech */ export function cleanTextForTts(text: string): string { return text .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\[GOTO:[\w-]+\]/g, '') .replace(/\[Q\]\s*/g, '') .replace(/---/g, '') .trim() } /** * Detect language heuristically for TTS */ export function detectTtsLanguage(text: string, fallback: Language): string { return /[äöüÄÖÜß]|(?:^|\s)(?:das|die|der|und|ist|wir|ein|für|mit|auf|von|den|des)\s/i.test(text) ? 'de' : fallback }