Files
breakpilot-core/pitch-deck/components/ChatFAB.helpers.ts
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:09:30 +02:00

113 lines
3.6 KiB
TypeScript

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<number>
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
}