From e839620537104e1c71675559c9d200582e5f5a2f Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sat, 4 Apr 2026 01:06:16 -0300 Subject: [PATCH] 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. --- tauri-app/src/components/StreamingBubble.tsx | 101 +++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tauri-app/src/components/StreamingBubble.tsx diff --git a/tauri-app/src/components/StreamingBubble.tsx b/tauri-app/src/components/StreamingBubble.tsx new file mode 100644 index 0000000..c2c9417 --- /dev/null +++ b/tauri-app/src/components/StreamingBubble.tsx @@ -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(null); + const bottomRef = useRef(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 ( +
+ {(hasReasoning || (!hasContent && !fallbackVisible)) && ( +
+
+
+
+ {reasoning.trim().length > 0 ? ( + + ) : ( + Generating reasoning… + )} +
+
+ )} + + {!hasReasoning && !hasContent && !fallbackVisible && ( +
+ + + +
+ )} + + {hasContent && ( +
+ )} + + {fallbackVisible && !hasContent && ( +
+ )} + +
+
+ ); +}); + +export default StreamingBubble; + +interface WordStreamProps { + text: string; +} + +const WordStream = memo(function WordStream({ text }: WordStreamProps) { + const tokens = text.split(/(\s+)/); + + return ( + + {tokens.map((token, i) => { + if (/^\s+$/.test(token)) return {token}; + return ( + + {token} + + ); + })} + + ); +});