diff options
| author | MaheshtheDev <[email protected]> | 2025-08-29 19:45:47 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2025-08-29 19:45:47 +0000 |
| commit | 5b7f9ceb44decc088c7db7c50756bae55f019558 (patch) | |
| tree | 63f2503bc519acba0f5a169be50edd749ca06747 /apps/web | |
| parent | Add claude GitHub actions 1756491853286 (#397) (diff) | |
| download | supermemory-mahesh/streamdown-integration.tar.xz supermemory-mahesh/streamdown-integration.zip | |
feat: migrate from react-markdown to streamdown (#394)mahesh/streamdown-integration
| Before (react-markdown) | After (streamdown) |
| --- | --- |
| <img width="300" height="863" alt="Before: react-markdown rendering" src="https://github.com/user-attachments/assets/807d0d3c-8634-45f3-a34f-79dd8139bef2" /> | <img width="300" height="814" alt="After: streamdown rendering" src="https://github.com/user-attachments/assets/8a718a9c-d842-424b-8679-15036076b142" /> |
## Changes Made
- **Dependencies**: Removed `react-markdown` and `remark-gfm`, added `streamdown@^1.1.6`
- **Component Updates**:
- Updated chat message rendering to use `<Streamdown>` component
- Maintained all existing functionality for tool state rendering
- Preserved prose styling classes for consistent appearance
- **Code Quality Improvements**:
- Fixed TypeScript type issues with message parts
- Improved switch case structure with proper default cases
- Replaced array index-based keys with stable message-based keys
- Added `useCallback` for performance optimization
- Fixed biome linting issues and switch case fallthrough warnings
Diffstat (limited to 'apps/web')
| -rw-r--r-- | apps/web/biome.json | 5 | ||||
| -rw-r--r-- | apps/web/components/views/chat/chat-messages.tsx | 96 | ||||
| -rw-r--r-- | apps/web/package.json | 3 |
3 files changed, 49 insertions, 55 deletions
diff --git a/apps/web/biome.json b/apps/web/biome.json index ea994ee6..48649190 100644 --- a/apps/web/biome.json +++ b/apps/web/biome.json @@ -1,6 +1,7 @@ { "root": false, - "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "extends": "//", + "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", "linter": { "rules": { "nursery": { @@ -8,4 +9,4 @@ } } } -} +}
\ No newline at end of file diff --git a/apps/web/components/views/chat/chat-messages.tsx b/apps/web/components/views/chat/chat-messages.tsx index ab228ce3..cff7e1b7 100644 --- a/apps/web/components/views/chat/chat-messages.tsx +++ b/apps/web/components/views/chat/chat-messages.tsx @@ -7,9 +7,8 @@ import { Input } from "@ui/components/input"; import { DefaultChatTransport } from "ai"; import { ArrowUp, Check, Copy, RotateCcw, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; import { toast } from "sonner"; +import { Streamdown } from "streamdown"; import { TextShimmer } from "@/components/text-shimmer"; import { usePersistentChat, useProject } from "@/stores"; import { useGraphHighlights } from "@/stores/highlights"; @@ -21,10 +20,10 @@ function useStickyAutoScroll(triggerKeys: ReadonlyArray<unknown>) { const [isAutoScroll, setIsAutoScroll] = useState(true); const [isFarFromBottom, setIsFarFromBottom] = useState(false); - function scrollToBottom(behavior: ScrollBehavior = "auto") { + const scrollToBottom = (behavior: ScrollBehavior = "auto") => { const node = bottomRef.current; if (node) node.scrollIntoView({ behavior, block: "end" }); - } + }; useEffect(function observeBottomVisibility() { const container = scrollContainerRef.current; @@ -67,20 +66,20 @@ function useStickyAutoScroll(triggerKeys: ReadonlyArray<unknown>) { function autoScrollOnNewContent() { if (isAutoScroll) scrollToBottom("auto"); }, - [isAutoScroll, ...triggerKeys], + [isAutoScroll, scrollToBottom, ...triggerKeys], ); - function recomputeDistanceFromBottom() { + const recomputeDistanceFromBottom = () => { const container = scrollContainerRef.current; if (!container) return; const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; setIsFarFromBottom(distanceFromBottom > 100); - } + }; useEffect(() => { recomputeDistanceFromBottom(); - }, [...triggerKeys]); + }, [recomputeDistanceFromBottom, ...triggerKeys]); function onScroll() { recomputeDistanceFromBottom(); @@ -154,7 +153,6 @@ export function ChatMessages() { const msgs = getCurrentConversation(); setMessages(msgs ?? []); setInput(""); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentChatId]); useEffect(() => { @@ -208,7 +206,7 @@ export function ChatMessages() { currentSummary?.title && currentSummary.title.trim().length > 0, ); shouldGenerateTitleRef.current = !hasTitle; - }, [currentChatId, id, getCurrentChat]); + }, [getCurrentChat]); const { scrollContainerRef, bottomRef, @@ -222,17 +220,17 @@ export function ChatMessages() { <> <div className="relative grow"> <div - ref={scrollContainerRef} - onScroll={onScroll} className="flex flex-col gap-2 absolute inset-0 overflow-y-auto px-4 pt-4 pb-7 scroll-pb-7" + onScroll={onScroll} + ref={scrollContainerRef} > {messages.map((message) => ( <div - key={message.id} className={cn( "flex flex-col", message.role === "user" ? "items-end" : "items-start", )} + key={message.id} > <div className="flex flex-col gap-2 max-w-4/5 bg-white/10 py-3 px-4 rounded-lg"> {message.parts @@ -241,27 +239,22 @@ export function ChatMessages() { part.type, ), ) - .map((part, index) => { + .map((part) => { switch (part.type) { case "text": return ( - <div - key={index} - className="prose prose-sm prose-invert max-w-none" - > - <ReactMarkdown remarkPlugins={[remarkGfm]}> - {(part as any).text} - </ReactMarkdown> + <div key={message.id + part.type}> + <Streamdown>{part.text}</Streamdown> </div> ); - case "tool-searchMemories": + case "tool-searchMemories": { switch (part.state) { case "input-available": case "input-streaming": return ( <div - key={index} className="text-sm flex items-center gap-2 text-muted-foreground" + key={message.id + part.type} > <Spinner className="size-4" /> Searching memories... @@ -270,44 +263,42 @@ export function ChatMessages() { case "output-error": return ( <div - key={index} className="text-sm flex items-center gap-2 text-muted-foreground" + key={message.id + part.type} > <X className="size-4" /> Error recalling memories </div> ); case "output-available": { - const output = (part as any).output; + const output = part.output; const foundCount = typeof output === "object" && output !== null && "count" in output ? Number(output.count) || 0 : 0; - const ids = Array.isArray(output?.results) - ? ((output.results as any[]) - .map((r) => r?.documentId) - .filter(Boolean) as string[]) - : []; return ( <div - key={index} className="text-sm flex items-center gap-2 text-muted-foreground" + key={message.id + part.type} > <Check className="size-4" /> Found {foundCount}{" "} memories </div> ); } + default: + return null; } - case "tool-addMemory": + } + case "tool-addMemory": { switch (part.state) { case "input-available": return ( <div - key={index} className="text-sm flex items-center gap-2 text-muted-foreground" + key={message.id + part.type} > <Spinner className="size-4" /> Adding memory... </div> @@ -315,8 +306,8 @@ export function ChatMessages() { case "output-error": return ( <div - key={index} className="text-sm flex items-center gap-2 text-muted-foreground" + key={message.id + part.type} > <X className="size-4" /> Error adding memory </div> @@ -324,8 +315,8 @@ export function ChatMessages() { case "output-available": return ( <div - key={index} className="text-sm flex items-center gap-2 text-muted-foreground" + key={message.id + part.type} > <Check className="size-4" /> Memory added </div> @@ -333,23 +324,24 @@ export function ChatMessages() { case "input-streaming": return ( <div - key={index} className="text-sm flex items-center gap-2 text-muted-foreground" + key={message.id + part.type} > <Spinner className="size-4" /> Adding memory... </div> ); + default: + return null; } + } + default: + return null; } - - return null; })} </div> {message.role === "assistant" && ( <div className="flex items-center gap-0.5 mt-0.5"> <Button - variant="ghost" - size="icon" className="size-7 text-muted-foreground hover:text-foreground" onClick={() => { navigator.clipboard.writeText( @@ -360,14 +352,16 @@ export function ChatMessages() { ); toast.success("Copied to clipboard"); }} + size="icon" + variant="ghost" > <Copy className="size-3.5" /> </Button> <Button - variant="ghost" - size="icon" className="size-6 text-muted-foreground hover:text-foreground" onClick={() => regenerate({ messageId: message.id })} + size="icon" + variant="ghost" > <RotateCcw className="size-3.5" /> </Button> @@ -387,11 +381,6 @@ export function ChatMessages() { </div> <Button - type="button" - onClick={() => { - enableAutoScroll(); - scrollToBottom("smooth"); - }} className={cn( "rounded-full w-fit mx-auto shadow-md z-10 absolute inset-x-0 bottom-4 flex justify-center", "transition-all duration-200 ease-out", @@ -399,8 +388,13 @@ export function ChatMessages() { ? "opacity-100 scale-100 pointer-events-auto" : "opacity-0 scale-95 pointer-events-none", )} - variant="default" + onClick={() => { + enableAutoScroll(); + scrollToBottom("smooth"); + }} size="sm" + type="button" + variant="default" > Scroll to bottom </Button> @@ -426,12 +420,12 @@ export function ChatMessages() { <div className="absolute top-0 left-0 -mt-7 w-full h-7 bg-gradient-to-t from-background to-transparent" /> <Input className="w-full" - value={input} - onChange={(e) => setInput(e.target.value)} disabled={status === "submitted"} + onChange={(e) => setInput(e.target.value)} placeholder="Say something..." + value={input} /> - <Button type="submit" disabled={status === "submitted"}> + <Button disabled={status === "submitted"} type="submit"> {status === "ready" ? ( <ArrowUp className="size-4" /> ) : status === "submitted" ? ( diff --git a/apps/web/package.json b/apps/web/package.json index cd1a545c..674b6a47 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -71,13 +71,12 @@ "react-dom": "^19.1.0", "react-dropzone": "^14.3.8", "react-error-boundary": "^6.0.0", - "react-markdown": "^10.1.0", "recharts": "2", - "remark-gfm": "^4.0.1", "shadcn-dropzone": "^0.2.1", "slate": "^0.118.0", "slate-react": "^0.117.4", "sonner": "^2.0.5", + "streamdown": "^1.1.6", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.4", "valibot": "^1.1.0", |