mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-11 16:52:38 +02:00
refactor: simplify video presentation UI components and enhance loading/error states
This commit is contained in:
parent
06b242c8f1
commit
d0dcb8a98b
2 changed files with 119 additions and 177 deletions
|
|
@ -119,7 +119,7 @@ export function CombinedPlayer({ slides }: CombinedPlayerProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-xl border shadow-2xl shadow-purple-500/5">
|
<div className="overflow-hidden rounded-xl">
|
||||||
<Player
|
<Player
|
||||||
component={CompositionWithScenes}
|
component={CompositionWithScenes}
|
||||||
durationInFrames={totalFrames}
|
durationInFrames={totalFrames}
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,12 @@
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
|
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
|
||||||
import {
|
import { Dot, Download, Loader2, Presentation, X } from "lucide-react";
|
||||||
AlertCircleIcon,
|
|
||||||
Download,
|
|
||||||
Film,
|
|
||||||
Loader2,
|
|
||||||
Presentation,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check";
|
import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check";
|
||||||
|
|
@ -84,30 +79,10 @@ function parseStatusResponse(data: unknown): VideoPresentationStatusResponse | n
|
||||||
|
|
||||||
function GeneratingState({ title }: { title: string }) {
|
function GeneratingState({ title }: { title: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border border-primary/20 bg-linear-to-br from-primary/5 to-primary/10 p-4 sm:p-6">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="relative shrink-0">
|
<p className="text-sm font-semibold text-foreground line-clamp-2">{title}</p>
|
||||||
<div className="flex size-12 sm:size-16 items-center justify-center rounded-full bg-primary/20">
|
<TextShimmerLoader text="Generating video presentation" size="sm" />
|
||||||
<Film className="size-6 sm:size-8 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div className="absolute inset-1 animate-ping rounded-full bg-primary/20" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-foreground text-sm sm:text-lg leading-tight">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-1.5 sm:mt-2 flex items-center gap-1.5 sm:gap-2 text-muted-foreground">
|
|
||||||
<Spinner size="sm" className="size-3 sm:size-4" />
|
|
||||||
<span className="text-xs sm:text-sm">
|
|
||||||
Generating video presentation. This may take a few minutes.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 sm:mt-3">
|
|
||||||
<div className="h-1 sm:h-1.5 w-full overflow-hidden rounded-full bg-primary/10">
|
|
||||||
<div className="h-full w-1/3 animate-pulse rounded-full bg-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -115,20 +90,14 @@ function GeneratingState({ title }: { title: string }) {
|
||||||
|
|
||||||
function ErrorState({ title, error }: { title: string; error: string }) {
|
function ErrorState({ title, error }: { title: string; error: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border border-destructive/20 bg-destructive/5 p-4 sm:p-6">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="flex size-12 sm:size-16 shrink-0 items-center justify-center rounded-full bg-destructive/10">
|
<p className="text-sm font-semibold text-destructive">Video Generation Failed</p>
|
||||||
<AlertCircleIcon className="size-6 sm:size-8 text-destructive" />
|
</div>
|
||||||
</div>
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="px-5 py-4">
|
||||||
<h3 className="font-semibold text-foreground text-sm sm:text-base leading-tight">
|
<p className="text-sm font-medium text-foreground line-clamp-2">{title}</p>
|
||||||
{title}
|
<p className="text-sm text-muted-foreground mt-1">{error}</p>
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-destructive text-xs sm:text-sm">
|
|
||||||
Failed to generate video presentation
|
|
||||||
</p>
|
|
||||||
<p className="mt-1.5 sm:mt-2 text-muted-foreground text-xs sm:text-sm">{error}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -136,20 +105,10 @@ function ErrorState({ title, error }: { title: string; error: string }) {
|
||||||
|
|
||||||
function CompilationLoadingState({ title }: { title: string }) {
|
function CompilationLoadingState({ title }: { title: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border bg-muted/30 p-4 sm:p-6">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="flex size-12 sm:size-16 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
<p className="text-sm font-semibold text-foreground line-clamp-2">{title}</p>
|
||||||
<Film className="size-6 sm:size-8 text-primary/50" />
|
<TextShimmerLoader text="Compiling scenes" size="sm" />
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-foreground text-sm sm:text-base leading-tight">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-1.5 sm:mt-2 flex items-center gap-1.5 sm:gap-2 text-muted-foreground">
|
|
||||||
<Spinner size="sm" className="size-3 sm:size-4" />
|
|
||||||
<span className="text-xs sm:text-sm">Compiling scenes...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -170,7 +129,6 @@ function VideoPresentationPlayer({
|
||||||
|
|
||||||
const [isRendering, setIsRendering] = useState(false);
|
const [isRendering, setIsRendering] = useState(false);
|
||||||
const [renderProgress, setRenderProgress] = useState<number | null>(null);
|
const [renderProgress, setRenderProgress] = useState<number | null>(null);
|
||||||
const [renderError, setRenderError] = useState<string | null>(null);
|
|
||||||
const [renderFormat, setRenderFormat] = useState<string | null>(null);
|
const [renderFormat, setRenderFormat] = useState<string | null>(null);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
|
@ -292,7 +250,6 @@ function VideoPresentationPlayer({
|
||||||
|
|
||||||
setIsRendering(true);
|
setIsRendering(true);
|
||||||
setRenderProgress(0);
|
setRenderProgress(0);
|
||||||
setRenderError(null);
|
|
||||||
setRenderFormat(null);
|
setRenderFormat(null);
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -363,10 +320,10 @@ function VideoPresentationPlayer({
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if ((err as Error).name === "AbortError") {
|
if ((err as Error).name !== "AbortError") {
|
||||||
// User cancelled
|
toast.error("Download Failed", {
|
||||||
} else {
|
description: err instanceof Error ? err.message : "Failed to render video",
|
||||||
setRenderError(err instanceof Error ? err.message : "Failed to render video");
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsRendering(false);
|
setIsRendering(false);
|
||||||
|
|
@ -384,7 +341,6 @@ function VideoPresentationPlayer({
|
||||||
|
|
||||||
setIsPptxExporting(true);
|
setIsPptxExporting(true);
|
||||||
setPptxProgress("Preparing...");
|
setPptxProgress("Preparing...");
|
||||||
setRenderError(null);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { exportToPptx } = await import("dom-to-pptx");
|
const { exportToPptx } = await import("dom-to-pptx");
|
||||||
|
|
@ -437,10 +393,12 @@ function VideoPresentationPlayer({
|
||||||
fileName: "presentation.pptx",
|
fileName: "presentation.pptx",
|
||||||
});
|
});
|
||||||
|
|
||||||
roots.forEach((r) => r.unmount());
|
for (const r of roots) r.unmount();
|
||||||
document.body.removeChild(offscreen);
|
document.body.removeChild(offscreen);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setRenderError(err instanceof Error ? err.message : "Failed to export PPTX");
|
toast.error("PPTX Export Failed", {
|
||||||
|
description: err instanceof Error ? err.message : "Failed to export PPTX",
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsPptxExporting(false);
|
setIsPptxExporting(false);
|
||||||
setPptxProgress(null);
|
setPptxProgress(null);
|
||||||
|
|
@ -456,97 +414,86 @@ function VideoPresentationPlayer({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4 space-y-3">
|
<div className="my-4 max-w-2xl overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
{/* Title bar with actions */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<p className="text-sm font-semibold text-foreground line-clamp-2">{title}</p>
|
||||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
<p className="text-xs text-muted-foreground mt-0.5 flex items-center">
|
||||||
<Film className="size-4 text-primary" />
|
{compiledSlides.length} slides <Dot className="size-4" /> {totalDuration.toFixed(1)}s <Dot className="size-4" /> {FPS}fps
|
||||||
</div>
|
</p>
|
||||||
<div className="min-w-0">
|
|
||||||
<h3 className="text-sm font-semibold text-foreground truncate">{title}</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{compiledSlides.length} slides · {totalDuration.toFixed(1)}s ·{" "}
|
|
||||||
{FPS}fps
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isRendering ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2 rounded-lg border bg-card px-3 py-1.5">
|
|
||||||
<Loader2 className="size-3.5 animate-spin text-primary" />
|
|
||||||
<span className="text-xs font-medium">
|
|
||||||
Rendering {renderFormat ?? ""}{" "}
|
|
||||||
{renderProgress !== null
|
|
||||||
? `${Math.round(renderProgress * 100)}%`
|
|
||||||
: "..."}
|
|
||||||
</span>
|
|
||||||
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-secondary">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full bg-primary transition-all duration-300"
|
|
||||||
style={{ width: `${(renderProgress ?? 0) * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleCancelRender}
|
|
||||||
className="rounded-lg border p-1.5 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
|
||||||
title="Cancel render"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<X className="size-3.5" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={handleDownload}
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-lg border bg-card px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Download className="size-3.5" />
|
|
||||||
Download MP4
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleDownloadPPTX}
|
|
||||||
disabled={isPptxExporting}
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-lg border bg-card px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{isPptxExporting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="size-3.5 animate-spin" />
|
|
||||||
{pptxProgress ?? "Exporting..."}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Presentation className="size-3.5" />
|
|
||||||
Download PPTX
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Render error */}
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
{renderError && (
|
|
||||||
<div className="flex items-start gap-3 rounded-xl border border-destructive/20 bg-destructive/5 p-3">
|
{/* Remotion Player */}
|
||||||
<AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />
|
<div className="px-5 pt-3">
|
||||||
<div>
|
<CombinedPlayer slides={compiledSlides} />
|
||||||
<p className="text-sm font-medium text-destructive">Download Failed</p>
|
</div>
|
||||||
<p className="mt-1 text-xs text-destructive/70 whitespace-pre-wrap">
|
|
||||||
{renderError}
|
<div className="mx-5 mt-3 h-px bg-border/50" />
|
||||||
</p>
|
|
||||||
</div>
|
{/* Action buttons */}
|
||||||
</div>
|
<div className="px-5 py-3 flex items-center gap-2 flex-wrap">
|
||||||
)}
|
{isRendering ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="size-3.5 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
|
Rendering {renderFormat ?? ""}{" "}
|
||||||
|
{renderProgress !== null
|
||||||
|
? `${Math.round(renderProgress * 100)}%`
|
||||||
|
: "..."}
|
||||||
|
</span>
|
||||||
|
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-muted-foreground/60 transition-all duration-300"
|
||||||
|
style={{ width: `${(renderProgress ?? 0) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleCancelRender}
|
||||||
|
className="size-7 text-muted-foreground"
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="gap-1.5 h-7 px-2.5 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
<Download className="size-3.5" />
|
||||||
|
Download MP4
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDownloadPPTX}
|
||||||
|
disabled={isPptxExporting}
|
||||||
|
className="gap-1.5 h-7 px-2.5 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
{isPptxExporting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-3.5 animate-spin" />
|
||||||
|
{pptxProgress ?? "Exporting..."}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Presentation className="size-3.5" />
|
||||||
|
Download PPTX
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Combined Remotion Player */}
|
|
||||||
<CombinedPlayer slides={compiledSlides} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -636,11 +583,13 @@ export const GenerateVideoPresentationToolUI = ({ args, result, status }: ToolCa
|
||||||
if (status.type === "incomplete") {
|
if (status.type === "incomplete") {
|
||||||
if (status.reason === "cancelled") {
|
if (status.reason === "cancelled") {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 rounded-xl border border-muted p-3 sm:p-4 text-muted-foreground">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<p className="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<Film className="size-3.5 sm:size-4" />
|
<p className="text-sm font-semibold text-muted-foreground">Presentation Cancelled</p>
|
||||||
<span className="line-through">Presentation generation cancelled</span>
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
</p>
|
Presentation generation was cancelled
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -664,19 +613,12 @@ export const GenerateVideoPresentationToolUI = ({ args, result, status }: ToolCa
|
||||||
|
|
||||||
if (result.status === "generating") {
|
if (result.status === "generating") {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border border-amber-500/20 bg-amber-500/5 p-3 sm:p-4">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-2.5 sm:gap-3">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="flex size-8 sm:size-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
|
<p className="text-sm font-semibold text-foreground">Presentation already in progress</p>
|
||||||
<Film className="size-4 sm:size-5 text-amber-500" />
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
</div>
|
Please wait for the current presentation to complete.
|
||||||
<div className="min-w-0">
|
</p>
|
||||||
<p className="text-amber-600 dark:text-amber-400 text-xs sm:text-sm font-medium">
|
|
||||||
Presentation already in progress
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-[10px] sm:text-xs mt-0.5">
|
|
||||||
Please wait for the current presentation to complete.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue