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.
This commit is contained in:
Evandro Fonseca Junior committed 2026-04-04 01:06:16 -03:00
1 parent 8f32b677da
commit e839620537
1 file changed
+101
@@ -0,0 +1,101 @@
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>
);
});