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} + + ); + })} + + ); +});