[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>
This commit is contained in:
112
pitch-deck/components/ChatFAB.helpers.ts
Normal file
112
pitch-deck/components/ChatFAB.helpers.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user