Files
ForgeLM/tauri-app/src/components/StreamingBubble.tsx
T
Evandro Fonseca Junior e839620537 fix(ui): restore live streaming of visible text in StreamingBubble
The redesign of the chat UI to use StreamingBubble mistakenly omitted the actual rendering of the visible 'content' (and fallback raw text). It only showed the thinking/reasoning block and the loading dots, leaving the main content invisible until the generation finished. Added the markdown rendering block back to StreamingBubble to show text in real-time.
2026-04-04 01:06:16 -03:00

102 lines
2.9 KiB
TypeScript

import { useEffect, useRef, memo } from "react";
import { renderMarkdown } from "../lib/markdown";
import "./StreamingBubble.css";
interface Props {
content: string;
reasoning: string;
thinkingOpen: boolean;
raw?: string;
}
const StreamingBubble = memo(function StreamingBubble({
content,
reasoning,
thinkingOpen,
raw = "",
}: Props) {
const reasoningRef = useRef<HTMLDivElement>(null);
const bottomRef = useRef<HTMLDivElement>(null);
const hasReasoning = reasoning.trim().length > 0 || thinkingOpen;
const hasContent = content.trim().length > 0;
const fallbackVisible = !hasContent && !hasReasoning && raw.trim().length > 0 ? raw : "";
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
}, [content, reasoning, raw]);
return (
<div className="msg-wrapper assistant streaming-wrapper" aria-live="polite" aria-label="AI is generating a response">
{(hasReasoning || (!hasContent && !fallbackVisible)) && (
<div className="stream-thinking-block">
<div className="stream-thinking-header">
<span className="stream-thinking-pulse" aria-hidden="true" />
<span className="stream-thinking-label">
{(thinkingOpen || (!hasContent && !fallbackVisible)) ? "Thinking…" : "Thinking"}
</span>
</div>
<div ref={reasoningRef} className="stream-thinking-body">
{reasoning.trim().length > 0 ? (
<WordStream text={reasoning} />
) : (
<span className="stream-thinking-placeholder">Generating reasoning…</span>
)}
</div>
</div>
)}
{!hasReasoning && !hasContent && !fallbackVisible && (
<div className="stream-loading-dots" aria-label="Waiting for response">
<span />
<span />
<span />
</div>
)}
{hasContent && (
<div
className="msg-content msg-content--assistant manus-markdown"
dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
/>
)}
{fallbackVisible && !hasContent && (
<div
className="msg-content msg-content--assistant manus-markdown"
dangerouslySetInnerHTML={{ __html: renderMarkdown(fallbackVisible) }}
/>
)}
<div ref={bottomRef} />
</div>
);
});
export default StreamingBubble;
interface WordStreamProps {
text: string;
}
const WordStream = memo(function WordStream({ text }: WordStreamProps) {
const tokens = text.split(/(\s+)/);
return (
<span className="word-stream">
{tokens.map((token, i) => {
if (/^\s+$/.test(token)) return <span key={i}>{token}</span>;
return (
<span
key={i}
className="word-stream-token"
style={{ animationDelay: `${Math.min(i * 8, 400)}ms` }}
>
{token}
</span>
);
})}
</span>
);
});