fix: Stream scrolling effect

This commit is contained in:
shiva raj badu 2026-04-26 23:24:30 +05:45
parent d1aeff6f82
commit 3b5f28f018
No known key found for this signature in database
5 changed files with 116 additions and 77 deletions

View file

@ -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<PresentationPageProps> = ({
const [selectedSlide, setSelectedSlide] = useState(0);
const [isFullscreen, setIsFullscreen] = useState(false);
const [error, setError] = useState(false);
const slidesScrollContainerRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();
@ -41,6 +42,11 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
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<PresentationPageProps> = ({
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<PresentationPageProps> = ({
className="relative flex h-full flex-col overflow-hidden"
>
<PresentationHeader presentation_id={presentation_id} isPresentationSaving={isSaving} currentSlide={selectedSlide} />
<div className="flex flex-1 min-h-0 gap-6 ">
<div className="w-[120px] h-full shrink-0 self-start sticky top-0 mt-[18px]">
<div className="flex flex-1 min-h-0 gap-6 overflow-hidden">
<div className="w-[120px] h-full shrink-0 self-start sticky top-0 pt-[18px]">
<SidePanel
selectedSlide={selectedSlide}
onSlideClick={handleSlideClick}
@ -160,8 +199,11 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
loading={loading}
/>
</div>
<div className="w-full min-w-0 h-full flex-1 mt-[18px]">
<div className="font-inter h-full overflow-y-auto hide-scrollbar">
<div className="w-full min-w-0 h-full flex-1 pt-[18px]">
<div
ref={slidesScrollContainerRef}
className="font-inter h-full overflow-y-auto hide-scrollbar scroll-pt-[18px]"
>
<div className="w-full max-w-[1280px] min-h-full mx-auto flex flex-col items-center pb-8">
{!presentationData ||
loading ||

View file

@ -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) => (
<div
<SlideThumbnailCard
key={`${slide.id}-${index}`}
onClick={() => onSlideClick(index)}
className={` cursor-pointer ring-2 rounded-[12px] transition-all duration-200 ${selectedSlide === index ? ' ring-[#5141e5]' : 'ring-gray-200'
}`}
>
<div className=" bg-white pointer-events-none relative overflow-hidden aspect-video">
<div className="absolute bg-gray-100/5 z-50 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<SlideScale slide={slide} />
</div>
</div>
</div>
slide={slide}
index={index}
selected={selectedSlide === index}
onClick={() => onSlideClick(slide.index ?? index)}
/>
))
) : (
<SortableContext

View file

@ -103,30 +103,6 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
});
}
};
// 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")) {

View file

@ -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<HTMLDivElement> {
slide: Slide;
index: number;
selected: boolean;
}
const SCALE = 0.061;
export const SlideThumbnailCard = forwardRef<
HTMLDivElement,
SlideThumbnailCardProps
>(({ slide, index, selected, className = "", style, ...props }, ref) => {
return (
<div
ref={ref}
style={{
backgroundColor: "var(--card-color, #ffffff)",
borderColor: selected ? "#5141e5" : "var(--stroke, #e5e7eb)",
...style,
}}
className={`cursor-pointer border relative p-1.5 rounded-[12px] overflow-hidden transition-all duration-200 ${
selected ? "border-[#BDB4FE]" : "border-[#EDEEEF]"
} ${className}`}
{...props}
>
<p className="pointer-events-none absolute -left-1 top-1/2 z-50 flex h-[18px] min-w-[18px] -translate-y-1/2 items-center justify-center rounded-full border border-[#EDEEEF] bg-white px-1 text-[10px] font-medium text-[#191919] shadow-sm">
{index + 1}
</p>
<div
className="relative"
style={{ height: `${720 * SCALE}px`, overflow: "hidden" }}
>
<div
className="absolute top-0 left-0 rounded-[10px] overflow-hidden pointer-events-none"
style={{
width: 1280,
height: 720,
transformOrigin: "top left",
transform: `scale(${SCALE})`,
}}
>
<V1ContentRender slide={slide} isEditMode={true} />
</div>
</div>
</div>
);
});
SlideThumbnailCard.displayName = "SlideThumbnailCard";

View file

@ -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 (
<div
<SlideThumbnailCard
ref={setNodeRef}
slide={slide}
index={index}
selected={selectedSlide === index}
style={style}
{...attributes}
{...listeners}
onClick={handleClick}
className={` cursor-pointer border relative p-1 rounded-[12px] transition-all duration-200 ${selectedSlide === index ? ' border-[#BDB4FE]' : 'border-[#EDEEEF]'
}`}
>
<p className='absolute top-1/2 translate-y-1/2 -left-3 bg-white border border-[#EDEEEF] rounded-[40px] text-[#191919] text-[10px] font-medium px-1 z-50
'>
{index + 1}
</p>
<div
className="relative"
style={{ height: `${720 * SCALE}px`, overflow: "hidden" }}
>
<div
className="absolute top-0 left-0 pointer-events-none"
style={{
width: 1280,
height: 720,
transformOrigin: "top left",
transform: `scale(${SCALE})`,
}}
>
<V1ContentRender slide={slide} isEditMode={true} />
</div>
</div>
</div>
/>
);
}
}