From 3b5f28f018ad7f14bfc7b9f8a0df9a0739a293bf Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Sun, 26 Apr 2026 23:24:30 +0545 Subject: [PATCH] fix: Stream scrolling effect --- .../components/PresentationPage.tsx | 52 ++++++++++++++++-- .../presentation/components/SidePanel.tsx | 20 +++---- .../presentation/components/SlideContent.tsx | 24 --------- .../components/SlideThumbnailCard.tsx | 54 +++++++++++++++++++ .../presentation/components/SortableSlide.tsx | 43 +++------------ 5 files changed, 116 insertions(+), 77 deletions(-) create mode 100644 servers/nextjs/app/(presentation-generator)/presentation/components/SlideThumbnailCard.tsx diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx index 039fe7aa..50577490 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useEffect, useLayoutEffect, useState } from "react"; +import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; import { RootState } from "@/store/store"; import "../../utils/prism-languages"; @@ -34,6 +34,7 @@ const PresentationPage: React.FC = ({ const [selectedSlide, setSelectedSlide] = useState(0); const [isFullscreen, setIsFullscreen] = useState(false); const [error, setError] = useState(false); + const slidesScrollContainerRef = useRef(null); const router = useRouter(); @@ -41,6 +42,11 @@ const PresentationPage: React.FC = ({ const { presentationData, isStreaming } = useSelector( (state: RootState) => state.presentationGeneration ); + const slidesLength = presentationData?.slides?.length ?? 0; + const lastStreamingSlideIndex = + slidesLength > 0 + ? presentationData?.slides?.[slidesLength - 1]?.index + : undefined; // Auto-save functionality const { isSaving } = useAutoSave({ @@ -81,6 +87,39 @@ const PresentationPage: React.FC = ({ usePresentationUndoRedo(); + useEffect(() => { + if (!isStreaming) return; + + const scrollContainer = slidesScrollContainerRef.current; + if (!scrollContainer) return; + + const frame = window.requestAnimationFrame(() => { + if (slidesLength <= 1) { + scrollContainer.scrollTo({ top: 0, behavior: "auto" }); + return; + } + + if (lastStreamingSlideIndex === undefined) return; + + const slideElement = document.getElementById( + `slide-${lastStreamingSlideIndex}` + ); + if (!slideElement) return; + + const containerRect = scrollContainer.getBoundingClientRect(); + const slideRect = slideElement.getBoundingClientRect(); + const slideTop = + slideRect.top - containerRect.top + scrollContainer.scrollTop; + + scrollContainer.scrollTo({ + top: Math.max(slideTop, 0), + behavior: "smooth", + }); + }); + + return () => window.cancelAnimationFrame(frame); + }, [isStreaming, lastStreamingSlideIndex, slidesLength]); + useEffect(() => { trackEvent(MixpanelEvent.Presentation_Editor_Viewed, { pathname, @@ -151,8 +190,8 @@ const PresentationPage: React.FC = ({ className="relative flex h-full flex-col overflow-hidden" > -
-
+
+
= ({ loading={loading} />
-
-
+
+
{!presentationData || loading || diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx index 2584d6d2..f9c23f40 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx @@ -19,11 +19,11 @@ import { } from "@dnd-kit/sortable"; import { setPresentationData } from "@/store/slices/presentationGeneration"; import { SortableSlide } from "./SortableSlide"; -import SlideScale from "../../components/PresentationRender"; import { Separator } from "@/components/ui/separator"; import { usePathname } from "next/navigation"; import NewSlide from "./NewSlide"; import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; +import { SlideThumbnailCard } from "./SlideThumbnailCard"; interface SidePanelProps { selectedSlide: number; @@ -153,19 +153,13 @@ const SidePanel = ({ {isStreaming ? ( presentationData && presentationData?.slides.map((slide: any, index: number) => ( -
onSlideClick(index)} - className={` cursor-pointer ring-2 rounded-[12px] transition-all duration-200 ${selectedSlide === index ? ' ring-[#5141e5]' : 'ring-gray-200' - }`} - > -
-
-
- -
-
-
+ slide={slide} + index={index} + selected={selectedSlide === index} + onClick={() => onSlideClick(slide.index ?? index)} + /> )) ) : ( { }); } }; - // Scroll to the new slide when streaming and new slides are being generated - useEffect(() => { - if ( - presentationData && - presentationData?.slides && - presentationData.slides.length > 1 && - isStreaming - ) { - // Scroll to the last slide (newly generated during streaming) - const lastSlideIndex = presentationData.slides.length - 1; - const slideElement = document.getElementById( - `slide-${presentationData.slides[lastSlideIndex].index}` - ); - if (slideElement) { - slideElement.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } - } - }, [presentationData?.slides?.length, isStreaming]); - - - useEffect(() => { if (slide.layout.includes("custom")) { diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideThumbnailCard.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideThumbnailCard.tsx new file mode 100644 index 00000000..262df279 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideThumbnailCard.tsx @@ -0,0 +1,54 @@ +import React, { forwardRef } from "react"; +import type { Slide } from "../../types/slide"; +import { V1ContentRender } from "../../components/V1ContentRender"; + +interface SlideThumbnailCardProps extends React.HTMLAttributes { + slide: Slide; + index: number; + selected: boolean; +} + +const SCALE = 0.061; + +export const SlideThumbnailCard = forwardRef< + HTMLDivElement, + SlideThumbnailCardProps +>(({ slide, index, selected, className = "", style, ...props }, ref) => { + return ( +
+

+ {index + 1} +

+ +
+
+ +
+
+
+ ); +}); + +SlideThumbnailCard.displayName = "SlideThumbnailCard"; diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx index 3a8dd5f4..74f9a3e2 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx @@ -1,15 +1,14 @@ import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { Slide } from '../../types/slide'; +import type { Slide } from '../../types/slide'; import { useRef } from 'react'; -import { V1ContentRender } from '../../components/V1ContentRender'; +import { SlideThumbnailCard } from './SlideThumbnailCard'; interface SortableSlideProps { slide: Slide; index: number; selectedSlide: number; onSlideClick: (index: any) => void; } -const SCALE = 0.0625; export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: SortableSlideProps) { const lastClickTime = useRef(0); @@ -26,8 +25,6 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, - backgroundColor: `var(--card-color, #ffffff)`, - borderColor: selectedSlide === index ? `#5141e5` : `var(--stroke, #e5e7eb)` }; const handleClick = (e: React.MouseEvent) => { @@ -46,39 +43,15 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor }; return ( -
-

- {index + 1} -

- - -
- -
- -
-
- -
+ /> ); -} \ No newline at end of file +}