From 393bb9c842345100e68071eedcd378fbaf35058a Mon Sep 17 00:00:00 2001 From: Suraj Jha Date: Tue, 19 Aug 2025 00:04:43 +0545 Subject: [PATCH 1/2] update: add thinking badge before outline stream --- .../outline/components/OutlineContent.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx index ed150d01..6f2043ad 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx @@ -15,7 +15,7 @@ import { } from "@dnd-kit/sortable"; import { OutlineItem } from "./OutlineItem"; import { Button } from "@/components/ui/button"; -import { FileText } from "lucide-react"; +import { FileText, Loader2 } from "lucide-react"; import { usePathname } from "next/navigation"; import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; @@ -45,6 +45,14 @@ const OutlineContent: React.FC = ({ return (
+ {isLoading && (!outlines || outlines.length === 0) && ( +
+ + + Thinking + +
+ )} {/*
Presentation Outline From 6f483953e773e658667fd75041f5827d912a0188 Mon Sep 17 00:00:00 2001 From: Suraj Jha Date: Tue, 19 Aug 2025 01:03:29 +0545 Subject: [PATCH 2/2] fix: render outline item while stream --- .../outline/components/OutlineContent.tsx | 8 ++ .../outline/components/OutlineItem.tsx | 73 ++++++++++++++++--- .../outline/components/OutlinePage.tsx | 2 + .../outline/hooks/useOutlineStreaming.ts | 61 +++++++++++++++- 4 files changed, 131 insertions(+), 13 deletions(-) diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx index 6f2043ad..2d3c5d4f 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx @@ -23,6 +23,8 @@ interface OutlineContentProps { outlines: { content: string }[] | null; isLoading: boolean; isStreaming: boolean; + activeSlideIndex: number | null; + highestActiveIndex: number; onDragEnd: (event: any) => void; onAddSlide: () => void; } @@ -31,6 +33,8 @@ const OutlineContent: React.FC = ({ outlines, isLoading, isStreaming, + activeSlideIndex, + highestActiveIndex, onDragEnd, onAddSlide }) => { @@ -102,6 +106,8 @@ const OutlineContent: React.FC = ({ index={index + 1} slideOutline={item} isStreaming={isStreaming} + isActiveStreaming={activeSlideIndex === index} + isStableStreaming={highestActiveIndex >= 0 && index < highestActiveIndex} /> )) ) : @@ -115,6 +121,8 @@ const OutlineContent: React.FC = ({ index={index + 1} slideOutline={item} isStreaming={isStreaming} + isActiveStreaming={false} + isStableStreaming={false} /> ))} } diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx index 95045cc8..e4e8c97e 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx @@ -6,7 +6,8 @@ import { useDispatch, useSelector } from "react-redux" import { deleteSlideOutline, setOutlines } from "@/store/slices/presentationGeneration" import ToolTip from "@/components/ToolTip" import MarkdownEditor from "../../components/MarkdownEditor" -import { useEffect } from "react" +import { useEffect, useMemo, useRef, useState } from "react" +import { marked } from "marked" interface OutlineItemProps { @@ -15,12 +16,16 @@ interface OutlineItemProps { }, index: number isStreaming: boolean + isActiveStreaming?: boolean + isStableStreaming?: boolean } export function OutlineItem({ index, slideOutline, isStreaming, + isActiveStreaming = false, + isStableStreaming = false, }: OutlineItemProps) { const { outlines, @@ -73,6 +78,42 @@ export function OutlineItem({ dispatch(deleteSlideOutline({ index: index - 1 })) } + + // Throttled markdown rendering only for the active streaming item to avoid flicker + const [renderedHtml, setRenderedHtml] = useState("") + const throttleRef = useRef(null) + useEffect(() => { + if (!isStreaming || !isActiveStreaming) return + const content = slideOutline.content || "" + // Throttle updates to ~60ms to reduce reflows/flicker + if (throttleRef.current) { + window.clearTimeout(throttleRef.current) + } + throttleRef.current = window.setTimeout(() => { + try { + setRenderedHtml(marked.parse(content) as string) + } catch { + setRenderedHtml("") + } + }, 60) + return () => { + if (throttleRef.current) { + window.clearTimeout(throttleRef.current) + } + } + }, [isStreaming, isActiveStreaming, slideOutline.content]) + + // Memoized stable HTML for previous (already completed) items during streaming + const stableHtml = useMemo(() => { + if (!isStreaming || isActiveStreaming) return null + if (!isStableStreaming) return null + try { + return marked.parse(slideOutline.content || "") as string + } catch { + return null + } + }, [isStreaming, isActiveStreaming, isStableStreaming, slideOutline.content]) + return (
{/* Main Title Row */} @@ -99,15 +140,27 @@ export function OutlineItem({ {/* Main Title Input - Add onFocus handler */}
{/* Editable Markdown Content */} - {isStreaming ?

- {slideOutline.content || ''} -

: handleSlideChange(content)} - />} + {isStreaming ? ( + isActiveStreaming ? ( +
+ ) : stableHtml ? ( +
+ ) : ( +

{slideOutline.content || ''}

+ ) + ) : ( + handleSlideChange(content)} + /> + )}
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index c11a5a59..235328d1 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -61,6 +61,8 @@ const OutlinePage: React.FC = () => { outlines={outlines} isLoading={streamState.isLoading} isStreaming={streamState.isStreaming} + activeSlideIndex={streamState.activeSlideIndex} + highestActiveIndex={streamState.highestActiveIndex} onDragEnd={handleDragEnd} onAddSlide={handleAddSlide} /> diff --git a/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts index d53cab9e..3f3f1057 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts +++ b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { toast } from "sonner"; import { setOutlines } from "@/store/slices/presentationGeneration"; @@ -12,6 +12,11 @@ export const useOutlineStreaming = (presentationId: string | null) => { const { outlines } = useSelector((state: RootState) => state.presentationGeneration); const [isStreaming, setIsStreaming] = useState(true); const [isLoading, setIsLoading] = useState(true); + const [activeSlideIndex, setActiveSlideIndex] = useState(null); + const [highestActiveIndex, setHighestActiveIndex] = useState(-1); + const prevSlidesRef = useRef<{ content: string }[]>([]); + const activeIndexRef = useRef(-1); + const highestIndexRef = useRef(-1); useEffect(() => { if (!presentationId || outlines.length > 0) return; @@ -39,7 +44,36 @@ export const useOutlineStreaming = (presentationId: string | null) => { const partialData = JSON.parse(repairedJson); if (partialData.slides) { - dispatch(setOutlines(partialData.slides)); + const nextSlides: { content: string }[] = partialData.slides || []; + // Determine which slide index changed to minimize live parsing + try { + const prev = prevSlidesRef.current || []; + let changedIndex: number | null = null; + const maxLen = Math.max(prev.length, nextSlides.length); + for (let i = 0; i < maxLen; i++) { + const prevContent = prev[i]?.content; + const nextContent = nextSlides[i]?.content; + if (nextContent !== prevContent) { + changedIndex = i; + } + } + // Keep active index stable if no change detected; and ensure non-decreasing + const prevActive = activeIndexRef.current; + let nextActive = changedIndex ?? prevActive; + if (nextActive < prevActive) { + nextActive = prevActive; + } + activeIndexRef.current = nextActive; + setActiveSlideIndex(nextActive); + + if (nextActive > highestIndexRef.current) { + highestIndexRef.current = nextActive; + setHighestActiveIndex(nextActive); + } + } catch {} + + prevSlidesRef.current = nextSlides; + dispatch(setOutlines(nextSlides)); setIsLoading(false) } } catch (error) { @@ -54,6 +88,11 @@ export const useOutlineStreaming = (presentationId: string | null) => { dispatch(setOutlines(outlinesData)); setIsStreaming(false) setIsLoading(false) + setActiveSlideIndex(null) + setHighestActiveIndex(-1) + prevSlidesRef.current = outlinesData; + activeIndexRef.current = -1; + highestIndexRef.current = -1; eventSource.close(); } catch (error) { console.error("Error parsing accumulated chunks:", error); @@ -67,12 +106,20 @@ export const useOutlineStreaming = (presentationId: string | null) => { setIsStreaming(false) setIsLoading(false) + setActiveSlideIndex(null) + setHighestActiveIndex(-1) + activeIndexRef.current = -1; + highestIndexRef.current = -1; eventSource.close(); break; case "error": setIsStreaming(false) setIsLoading(false) + setActiveSlideIndex(null) + setHighestActiveIndex(-1) + activeIndexRef.current = -1; + highestIndexRef.current = -1; eventSource.close(); toast.error('Error in outline streaming', { @@ -87,6 +134,10 @@ export const useOutlineStreaming = (presentationId: string | null) => { setIsStreaming(false) setIsLoading(false) + setActiveSlideIndex(null) + setHighestActiveIndex(-1) + activeIndexRef.current = -1; + highestIndexRef.current = -1; eventSource.close(); toast.error("Failed to connect to the server. Please try again."); }; @@ -94,6 +145,10 @@ export const useOutlineStreaming = (presentationId: string | null) => { setIsStreaming(false) setIsLoading(false) + setActiveSlideIndex(null) + setHighestActiveIndex(-1) + activeIndexRef.current = -1; + highestIndexRef.current = -1; toast.error("Failed to initialize connection"); } }; @@ -105,5 +160,5 @@ export const useOutlineStreaming = (presentationId: string | null) => { }; }, [presentationId, dispatch]); - return { isStreaming, isLoading }; + return { isStreaming, isLoading, activeSlideIndex, highestActiveIndex }; }; \ No newline at end of file