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:
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>
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user