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>
113 lines
3.6 KiB
TypeScript
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
|
|
}
|