feat(chat): TTS for chat responses + fix team FAQ with real founder names

- Chat answers are now read aloud via Edge TTS (auto, with mute toggle)
- FAQ team answer: vague text → Benjamin Boenisch (CEO) + Sharang (CTO)
- System prompt: explicit instruction to always cite team names from DB
- Speaker icon in chat header shows speaking state, click to mute/unmute
- Audio stops on new message, chat close, or mute

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-20 17:18:30 +01:00
parent f126b40574
commit 959986356b
3 changed files with 97 additions and 4 deletions

View File

@@ -2,7 +2,7 @@
import { useState, useRef, useEffect, useMemo } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { X, Send, Bot, User, Sparkles, Maximize2, Minimize2, ArrowRight } from 'lucide-react'
import { X, Send, Bot, User, Sparkles, Maximize2, Minimize2, ArrowRight, Volume2, VolumeX } from 'lucide-react'
import { ChatMessage, Language, SlideId } from '@/lib/types'
import { t } from '@/lib/i18n'
import { SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation'
@@ -116,9 +116,72 @@ export default function ChatFAB({
const [isStreaming, setIsStreaming] = useState(false)
const [isWaiting, setIsWaiting] = useState(false)
const [parsedResponses, setParsedResponses] = useState<Map<number, ParsedMessage>>(new Map())
const [chatTtsEnabled, setChatTtsEnabled] = useState(true)
const [isChatSpeaking, setIsChatSpeaking] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const abortRef = useRef<AbortController | null>(null)
const chatAudioRef = useRef<HTMLAudioElement | null>(null)
const ttsAbortRef = useRef<AbortController | null>(null)
const cancelChatAudio = () => {
if (chatAudioRef.current) {
chatAudioRef.current.pause()
chatAudioRef.current = null
}
if (ttsAbortRef.current) {
ttsAbortRef.current.abort()
ttsAbortRef.current = null
}
setIsChatSpeaking(false)
}
const speakResponse = async (text: string) => {
if (!chatTtsEnabled) return
cancelChatAudio()
// Clean text for TTS: remove markdown formatting, keep plain speech
const cleanText = text
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\[GOTO:[\w-]+\]/g, '')
.replace(/\[Q\]\s*/g, '')
.replace(/---/g, '')
.trim()
if (!cleanText) return
const controller = new AbortController()
ttsAbortRef.current = controller
try {
const res = await fetch('/api/presenter/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: cleanText, language: lang }),
signal: controller.signal,
})
if (!res.ok || controller.signal.aborted) return
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const audio = new Audio(url)
chatAudioRef.current = audio
setIsChatSpeaking(true)
audio.onended = () => {
setIsChatSpeaking(false)
chatAudioRef.current = null
}
audio.onerror = () => {
setIsChatSpeaking(false)
chatAudioRef.current = null
}
await audio.play()
} catch {
setIsChatSpeaking(false)
}
}
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
@@ -143,6 +206,9 @@ export default function ChatFAB({
const msg = messages[lastAssistantIndex]
const parsed = parseAgentResponse(msg.content, lang)
setParsedResponses(prev => new Map(prev).set(lastAssistantIndex, parsed))
// Speak the response via TTS
speakResponse(parsed.text)
}
}, [isStreaming, lastAssistantIndex, messages, parsedResponses, lang])
@@ -155,6 +221,7 @@ export default function ChatFAB({
onPresenterInterrupt()
}
cancelChatAudio()
setInput('')
setMessages(prev => [...prev, { role: 'user', content: message }])
setIsStreaming(true)
@@ -374,6 +441,26 @@ export default function ChatFAB({
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => {
setChatTtsEnabled(prev => {
if (prev) cancelChatAudio()
return !prev
})
}}
className={`w-7 h-7 rounded-full flex items-center justify-center transition-colors ${
isChatSpeaking ? 'bg-indigo-500/30' : 'bg-white/10 hover:bg-white/20'
}`}
title={chatTtsEnabled
? (lang === 'de' ? 'Sprachausgabe aus' : 'Mute voice')
: (lang === 'de' ? 'Sprachausgabe ein' : 'Unmute voice')
}
>
{chatTtsEnabled
? <Volume2 className={`w-3.5 h-3.5 ${isChatSpeaking ? 'text-indigo-400' : 'text-white/60'}`} />
: <VolumeX className="w-3.5 h-3.5 text-white/40" />
}
</button>
<button
onClick={() => setIsExpanded(prev => !prev)}
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors"
@@ -381,7 +468,10 @@ export default function ChatFAB({
{isExpanded ? <Minimize2 className="w-3.5 h-3.5 text-white/60" /> : <Maximize2 className="w-3.5 h-3.5 text-white/60" />}
</button>
<button
onClick={() => setIsOpen(false)}
onClick={() => {
cancelChatAudio()
setIsOpen(false)
}}
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors"
>
<X className="w-4 h-4 text-white/60" />