fix(pitch-deck): Waiting-Indicator in ChatFAB (richtiges Komponente)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 25s
CI / test-python-voice (push) Successful in 29s
CI / test-bqas (push) Successful in 26s

ChatInterface.tsx war falsch — der echte Investor Agent laeuft in
ChatFAB.tsx. Animierte Punkte + firstChunk-Logik dort implementiert.
Session-History laeuft bereits korrekt (FAB permanent gemountet).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-06 09:39:19 +01:00
parent 35aad9b169
commit f467db2ea0

View File

@@ -88,6 +88,7 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
const [messages, setMessages] = useState<ChatMessage[]>([]) const [messages, setMessages] = useState<ChatMessage[]>([])
const [input, setInput] = useState('') const [input, setInput] = useState('')
const [isStreaming, setIsStreaming] = useState(false) const [isStreaming, setIsStreaming] = useState(false)
const [isWaiting, setIsWaiting] = useState(false)
const [parsedResponses, setParsedResponses] = useState<Map<number, ParsedMessage>>(new Map()) const [parsedResponses, setParsedResponses] = useState<Map<number, ParsedMessage>>(new Map())
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
@@ -126,6 +127,7 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
setInput('') setInput('')
setMessages(prev => [...prev, { role: 'user', content: message }]) setMessages(prev => [...prev, { role: 'user', content: message }])
setIsStreaming(true) setIsStreaming(true)
setIsWaiting(true)
abortRef.current = new AbortController() abortRef.current = new AbortController()
@@ -152,14 +154,19 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
const reader = res.body!.getReader() const reader = res.body!.getReader()
const decoder = new TextDecoder() const decoder = new TextDecoder()
let content = '' let content = ''
let firstChunk = true
setMessages(prev => [...prev, { role: 'assistant', content: '' }])
while (true) { while (true) {
const { done, value } = await reader.read() const { done, value } = await reader.read()
if (done) break if (done) break
content += decoder.decode(value, { stream: true }) content += decoder.decode(value, { stream: true })
if (firstChunk) {
firstChunk = false
setIsWaiting(false)
setMessages(prev => [...prev, { role: 'assistant', content }])
} else {
const currentText = content const currentText = content
setMessages(prev => { setMessages(prev => {
const updated = [...prev] const updated = [...prev]
@@ -167,9 +174,11 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
return updated return updated
}) })
} }
}
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof Error && err.name === 'AbortError') return if (err instanceof Error && err.name === 'AbortError') return
console.error('Chat error:', err) console.error('Chat error:', err)
setIsWaiting(false)
setMessages(prev => [ setMessages(prev => [
...prev, ...prev,
{ role: 'assistant', content: lang === 'de' { role: 'assistant', content: lang === 'de'
@@ -179,6 +188,7 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
]) ])
} finally { } finally {
setIsStreaming(false) setIsStreaming(false)
setIsWaiting(false)
abortRef.current = null abortRef.current = null
} }
} }
@@ -344,6 +354,32 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
</div> </div>
)} )}
{/* Waiting indicator */}
<AnimatePresence>
{isWaiting && (
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="flex gap-2.5"
>
<div className="w-7 h-7 rounded-full bg-indigo-500/20 flex items-center justify-center shrink-0 mt-0.5">
<Bot className="w-3.5 h-3.5 text-indigo-400" />
</div>
<div className="bg-white/[0.06] rounded-2xl px-3.5 py-3 flex items-center gap-1">
{[0, 1, 2].map(i => (
<motion.span
key={i}
className="block w-1.5 h-1.5 rounded-full bg-indigo-400/70"
animate={{ opacity: [0.3, 1, 0.3], y: [0, -3, 0] }}
transition={{ duration: 0.7, repeat: Infinity, delay: i * 0.15 }}
/>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{messages.map((msg, idx) => ( {messages.map((msg, idx) => (
<div <div
key={idx} key={idx}