feat: init video presentation agent

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-03-21 22:13:41 -07:00
parent 40d949b7d5
commit b28f135a96
37 changed files with 3567 additions and 24 deletions

View file

@ -472,21 +472,21 @@ export function DocumentsTableShell({
setBulkDeleteConfirmOpen(false);
}, [deletableSelectedIds, bulkDeleteDocuments, deleteDocument]);
const bulkDeleteBar = hasDeletableSelection ? (
<div className="flex items-center justify-center py-1.5 border-b border-border/50 bg-destructive/5 shrink-0 animate-in fade-in slide-in-from-top-1 duration-150">
<button
type="button"
onClick={() => setBulkDeleteConfirmOpen(true)}
className="flex items-center gap-1.5 px-3 py-1 rounded-md bg-destructive text-destructive-foreground shadow-sm text-xs font-medium hover:bg-destructive/90 transition-colors"
>
<Trash2 size={12} />
Delete ({deletableSelectedIds.length} selected)
</button>
</div>
) : null;
return (
<div className="bg-sidebar overflow-hidden select-none border-t border-border/50 flex-1 flex flex-col min-h-0 relative">
{/* Floating bulk delete pill */}
{hasDeletableSelection && (
<div className="absolute left-1/2 -translate-x-1/2 top-2 md:top-9 z-20 animate-in fade-in slide-in-from-top-1 duration-150">
<button
type="button"
onClick={() => setBulkDeleteConfirmOpen(true)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-destructive text-destructive-foreground shadow-md text-xs font-medium"
>
<Trash2 size={12} />
Delete ({deletableSelectedIds.length} selected)
</button>
</div>
)}
<div className="bg-sidebar overflow-hidden select-none border-t border-border/50 flex-1 flex flex-col min-h-0">
{/* Desktop Table View */}
<div className="hidden md:flex md:flex-col flex-1 min-h-0">
<Table className="table-fixed w-full">
@ -526,6 +526,7 @@ export function DocumentsTableShell({
</TableRow>
</TableHeader>
</Table>
{bulkDeleteBar}
{loading ? (
<div className="flex-1 overflow-auto">
<Table className="table-fixed w-full">
@ -777,6 +778,9 @@ export function DocumentsTableShell({
)}
</div>
{/* Mobile bulk delete bar */}
<div className="md:hidden">{bulkDeleteBar}</div>
{/* Mobile Card View */}
{loading ? (
<div className="md:hidden divide-y divide-border/50 flex-1 overflow-auto">

View file

@ -40,6 +40,7 @@ import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation";
import {
CreateGoogleDriveFileToolUI,
DeleteGoogleDriveFileToolUI,
@ -148,6 +149,7 @@ function extractMentionedDocuments(content: unknown): MentionedDocumentInfo[] {
const TOOLS_WITH_UI = new Set([
"generate_podcast",
"generate_report",
"generate_video_presentation",
"link_preview",
"display_image",
"delete_notion_page",
@ -1662,6 +1664,7 @@ export default function NewChatPage() {
<AssistantRuntimeProvider runtime={runtime}>
<GeneratePodcastToolUI />
<GenerateReportToolUI />
<GenerateVideoPresentationToolUI />
<LinkPreviewToolUI />
<DisplayImageToolUI />
<ScrapeWebpageToolUI />

View file

@ -6,6 +6,7 @@ import { ReportPanel } from "@/components/report-panel/report-panel";
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation";
import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview";
import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage";
import { Spinner } from "@/components/ui/spinner";
@ -45,6 +46,7 @@ export function PublicChatView({ shareToken }: PublicChatViewProps) {
{/* Tool UIs for rendering tool results */}
<GeneratePodcastToolUI />
<GenerateReportToolUI />
<GenerateVideoPresentationToolUI />
<LinkPreviewToolUI />
<DisplayImageToolUI />
<ScrapeWebpageToolUI />

View file

@ -32,6 +32,7 @@ export {
} from "./display-image";
export { GeneratePodcastToolUI } from "./generate-podcast";
export { GenerateReportToolUI } from "./generate-report";
export { GenerateVideoPresentationToolUI } from "./video-presentation";
export { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI } from "./google-drive";
export {
Image,

View file

@ -0,0 +1,74 @@
"use client";
import React, { useMemo } from "react";
import { Player } from "@remotion/player";
import { Sequence, AbsoluteFill } from "remotion";
import { Audio } from "@remotion/media";
import { FPS } from "@/lib/remotion/constants";
export interface CompiledSlide {
component: React.ComponentType;
title: string;
code: string;
durationInFrames: number;
audioUrl?: string;
}
function CombinedComposition({ scenes }: { scenes: CompiledSlide[] }) {
let offset = 0;
return (
<AbsoluteFill>
{scenes.map((scene, i) => {
const from = offset;
offset += scene.durationInFrames;
return (
<Sequence key={i} from={from} durationInFrames={scene.durationInFrames}>
<scene.component />
{scene.audioUrl && <Audio src={scene.audioUrl} />}
</Sequence>
);
})}
</AbsoluteFill>
);
}
export function buildCompositionComponent(slides: CompiledSlide[]): React.FC {
const scenesSnapshot = [...slides];
const Comp: React.FC = () => <CombinedComposition scenes={scenesSnapshot} />;
return Comp;
}
interface CombinedPlayerProps {
slides: CompiledSlide[];
}
export function CombinedPlayer({ slides }: CombinedPlayerProps) {
const CompositionWithScenes = useMemo(() => {
const scenesSnapshot = [...slides];
const Comp: React.FC = () => <CombinedComposition scenes={scenesSnapshot} />;
return Comp;
}, [slides]);
const totalFrames = useMemo(
() => slides.reduce((sum, s) => sum + s.durationInFrames, 0),
[slides],
);
return (
<div className="overflow-hidden rounded-xl border shadow-2xl shadow-purple-500/5">
<Player
component={CompositionWithScenes}
durationInFrames={totalFrames}
fps={FPS}
compositionWidth={1920}
compositionHeight={1080}
style={{ width: "100%", aspectRatio: "16/9" }}
controls
autoPlay
loop
acknowledgeRemotionLicense
/>
</div>
);
}

View file

@ -0,0 +1,682 @@
"use client";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
AlertCircleIcon,
Download,
Film,
Loader2,
Presentation,
X,
} from "lucide-react";
import { z } from "zod";
import { Spinner } from "@/components/ui/spinner";
import { baseApiService } from "@/lib/apis/base-api.service";
import { authenticatedFetch } from "@/lib/auth-utils";
import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check";
import { FPS } from "@/lib/remotion/constants";
import {
CombinedPlayer,
buildCompositionComponent,
type CompiledSlide,
} from "./combined-player";
const GenerateVideoPresentationArgsSchema = z.object({
source_content: z.string(),
video_title: z.string().nullish(),
user_prompt: z.string().nullish(),
});
const GenerateVideoPresentationResultSchema = z.object({
status: z.enum(["pending", "generating", "ready", "failed"]),
video_presentation_id: z.number().nullish(),
title: z.string().nullish(),
message: z.string().nullish(),
error: z.string().nullish(),
});
const VideoPresentationStatusResponseSchema = z.object({
status: z.enum(["pending", "generating", "ready", "failed"]),
id: z.number(),
title: z.string(),
slides: z
.array(
z.object({
slide_number: z.number(),
title: z.string(),
subtitle: z.string().nullish(),
content_in_markdown: z.string().nullish(),
speaker_transcripts: z.array(z.string()).nullish(),
background_explanation: z.string().nullish(),
audio_url: z.string().nullish(),
duration_seconds: z.number().nullish(),
duration_in_frames: z.number().nullish(),
}),
)
.nullish(),
scene_codes: z
.array(
z.object({
slide_number: z.number(),
code: z.string(),
title: z.string().nullish(),
}),
)
.nullish(),
slide_count: z.number().nullish(),
});
type GenerateVideoPresentationArgs = z.infer<typeof GenerateVideoPresentationArgsSchema>;
type GenerateVideoPresentationResult = z.infer<typeof GenerateVideoPresentationResultSchema>;
type VideoPresentationStatusResponse = z.infer<typeof VideoPresentationStatusResponseSchema>;
function parseStatusResponse(data: unknown): VideoPresentationStatusResponse | null {
const result = VideoPresentationStatusResponseSchema.safeParse(data);
if (!result.success) {
console.warn("Invalid video presentation status:", result.error.issues);
return null;
}
return result.data;
}
function GeneratingState({ title }: { title: string }) {
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="flex items-center gap-3 sm:gap-4">
<div className="relative shrink-0">
<div className="flex size-12 sm:size-16 items-center justify-center rounded-full bg-primary/20">
<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>
);
}
function ErrorState({ title, error }: { title: string; error: string }) {
return (
<div className="my-4 overflow-hidden rounded-xl border border-destructive/20 bg-destructive/5 p-4 sm:p-6">
<div className="flex items-center gap-3 sm:gap-4">
<div className="flex size-12 sm:size-16 shrink-0 items-center justify-center rounded-full bg-destructive/10">
<AlertCircleIcon className="size-6 sm:size-8 text-destructive" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground text-sm sm:text-base leading-tight">
{title}
</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>
);
}
function CompilationLoadingState({ title }: { title: string }) {
return (
<div className="my-4 overflow-hidden rounded-xl border bg-muted/30 p-4 sm:p-6">
<div className="flex items-center gap-3 sm:gap-4">
<div className="flex size-12 sm:size-16 shrink-0 items-center justify-center rounded-full bg-primary/10">
<Film className="size-6 sm:size-8 text-primary/50" />
</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>
);
}
function VideoPresentationPlayer({
presentationId,
title,
}: {
presentationId: number;
title: string;
}) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [compiledSlides, setCompiledSlides] = useState<CompiledSlide[]>([]);
const [isRendering, setIsRendering] = useState(false);
const [renderProgress, setRenderProgress] = useState<number | null>(null);
const [renderError, setRenderError] = useState<string | null>(null);
const [renderFormat, setRenderFormat] = useState<string | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const [isPptxExporting, setIsPptxExporting] = useState(false);
const [pptxProgress, setPptxProgress] = useState<string | null>(null);
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "";
const audioBlobUrlsRef = useRef<string[]>([]);
const loadPresentation = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const raw = await baseApiService.get<unknown>(
`/api/v1/video-presentations/${presentationId}`,
);
const data = parseStatusResponse(raw);
if (!data) throw new Error("Invalid response");
if (data.status !== "ready") throw new Error(`Unexpected status: ${data.status}`);
if (!data.slides?.length || !data.scene_codes?.length) {
throw new Error("No slides or scene codes in response");
}
const sceneMap = new Map(data.scene_codes.map((sc) => [sc.slide_number, sc]));
const compiled: CompiledSlide[] = [];
for (const slide of data.slides) {
const scene = sceneMap.get(slide.slide_number);
if (!scene) continue;
const durationInFrames = slide.duration_in_frames ?? 300;
const check = compileCheck(scene.code);
if (!check.success) {
console.warn(
`Slide ${slide.slide_number} failed to compile: ${check.error}`,
);
continue;
}
const component = compileToComponent(scene.code, durationInFrames);
compiled.push({
component,
title: scene.title ?? slide.title,
code: scene.code,
durationInFrames,
audioUrl: slide.audio_url
? `${backendUrl}${slide.audio_url}`
: undefined,
});
}
if (compiled.length === 0) {
throw new Error("No slides compiled successfully");
}
// Pre-fetch all audio files with auth headers and convert to blob URLs.
// Remotion's <Audio> uses a plain <audio> element which can't send auth
// headers, so we fetch the audio ourselves and hand it a blob: URL.
const withBlobs = await Promise.all(
compiled.map(async (slide) => {
if (!slide.audioUrl) return slide;
try {
const resp = await authenticatedFetch(slide.audioUrl, {
method: "GET",
});
if (!resp.ok) {
console.warn(
`Audio fetch ${resp.status} for slide "${slide.title}"`,
);
return { ...slide, audioUrl: undefined };
}
const blob = await resp.blob();
const blobUrl = URL.createObjectURL(blob);
audioBlobUrlsRef.current.push(blobUrl);
return { ...slide, audioUrl: blobUrl };
} catch (err) {
console.warn(`Failed to fetch audio for "${slide.title}":`, err);
return { ...slide, audioUrl: undefined };
}
}),
);
setCompiledSlides(withBlobs);
} catch (err) {
console.error("Error loading video presentation:", err);
setError(err instanceof Error ? err.message : "Failed to load presentation");
} finally {
setIsLoading(false);
}
}, [presentationId, backendUrl]);
useEffect(() => {
loadPresentation();
return () => {
for (const url of audioBlobUrlsRef.current) {
URL.revokeObjectURL(url);
}
audioBlobUrlsRef.current = [];
};
}, [loadPresentation]);
const totalDuration = useMemo(
() => compiledSlides.reduce((sum, s) => sum + s.durationInFrames / FPS, 0),
[compiledSlides],
);
const handleDownload = async () => {
if (isRendering || compiledSlides.length === 0) return;
setIsRendering(true);
setRenderProgress(0);
setRenderError(null);
setRenderFormat(null);
const controller = new AbortController();
abortControllerRef.current = controller;
try {
const { canRenderMediaOnWeb, renderMediaOnWeb } = await import(
"@remotion/web-renderer"
);
const formats = [
{ container: "mp4" as const, videoCodec: "h264" as const, ext: "mp4" },
{ container: "mp4" as const, videoCodec: "h265" as const, ext: "mp4" },
{ container: "webm" as const, videoCodec: "vp8" as const, ext: "webm" },
{ container: "webm" as const, videoCodec: "vp9" as const, ext: "webm" },
];
let chosen: (typeof formats)[number] | null = null;
for (const fmt of formats) {
const { canRender } = await canRenderMediaOnWeb({
width: 1920,
height: 1080,
container: fmt.container,
videoCodec: fmt.videoCodec,
});
if (canRender) {
chosen = fmt;
break;
}
}
if (!chosen) {
throw new Error(
"Your browser does not support video rendering (WebCodecs). Please use Chrome, Edge, or Firefox 130+.",
);
}
setRenderFormat(chosen.ext.toUpperCase());
const totalFrames = compiledSlides.reduce((sum, s) => sum + s.durationInFrames, 0);
const CompositionComponent = buildCompositionComponent(compiledSlides);
const { getBlob } = await renderMediaOnWeb({
composition: {
component: CompositionComponent,
durationInFrames: totalFrames,
fps: FPS,
width: 1920,
height: 1080,
id: "combined-video",
},
container: chosen.container,
videoCodec: chosen.videoCodec,
videoBitrate: "high",
onProgress: ({ progress }) => {
setRenderProgress(progress);
},
signal: controller.signal,
});
const blob = await getBlob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `video.${chosen.ext}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
if ((err as Error).name === "AbortError") {
// User cancelled
} else {
setRenderError(err instanceof Error ? err.message : "Failed to render video");
}
} finally {
setIsRendering(false);
setRenderProgress(null);
abortControllerRef.current = null;
}
};
const handleCancelRender = () => {
abortControllerRef.current?.abort();
};
const handleDownloadPPTX = async () => {
if (isPptxExporting || compiledSlides.length === 0) return;
setIsPptxExporting(true);
setPptxProgress("Preparing...");
setRenderError(null);
try {
const { exportToPptx } = await import("dom-to-pptx");
const { Thumbnail } = await import("@remotion/player");
const { createRoot } = await import("react-dom/client");
const { flushSync } = await import("react-dom");
const offscreen = document.createElement("div");
offscreen.style.cssText =
"position:fixed;left:-99999px;top:0;overflow:hidden;pointer-events:none;";
document.body.appendChild(offscreen);
const slideElements: HTMLElement[] = [];
const roots: ReturnType<typeof createRoot>[] = [];
for (let i = 0; i < compiledSlides.length; i++) {
const slide = compiledSlides[i];
setPptxProgress(`Rendering slide ${i + 1}/${compiledSlides.length}...`);
const wrapper = document.createElement("div");
wrapper.style.cssText = "width:1920px;height:1080px;overflow:hidden;";
offscreen.appendChild(wrapper);
const holdFrame = Math.floor(slide.durationInFrames * 0.3);
const root = createRoot(wrapper);
flushSync(() => {
root.render(
React.createElement(Thumbnail, {
component: slide.component,
compositionWidth: 1920,
compositionHeight: 1080,
frameToDisplay: holdFrame,
durationInFrames: slide.durationInFrames,
fps: FPS,
style: { width: 1920, height: 1080 },
}),
);
});
await new Promise((r) => setTimeout(r, 500));
slideElements.push(wrapper);
roots.push(root);
}
setPptxProgress("Converting to editable PPTX...");
await exportToPptx(slideElements, {
fileName: "presentation.pptx",
});
roots.forEach((r) => r.unmount());
document.body.removeChild(offscreen);
} catch (err) {
setRenderError(err instanceof Error ? err.message : "Failed to export PPTX");
} finally {
setIsPptxExporting(false);
setPptxProgress(null);
}
};
if (isLoading) {
return <CompilationLoadingState title={title} />;
}
if (error || compiledSlides.length === 0) {
return <ErrorState title={title} error={error || "Failed to compile scenes"} />;
}
return (
<div className="my-4 space-y-3">
{/* Title bar with actions */}
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-3 min-w-0">
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<Film className="size-4 text-primary" />
</div>
<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 &middot; {totalDuration.toFixed(1)}s &middot;{" "}
{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>
{/* Render error */}
{renderError && (
<div className="flex items-start gap-3 rounded-xl border border-destructive/20 bg-destructive/5 p-3">
<AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />
<div>
<p className="text-sm font-medium text-destructive">Download Failed</p>
<p className="mt-1 text-xs text-destructive/70 whitespace-pre-wrap">
{renderError}
</p>
</div>
</div>
)}
{/* Combined Remotion Player */}
<CombinedPlayer slides={compiledSlides} />
</div>
);
}
function StatusPoller({
presentationId,
title,
}: {
presentationId: number;
title: string;
}) {
const [status, setStatus] = useState<VideoPresentationStatusResponse | null>(null);
const pollingRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const poll = async () => {
try {
const raw = await baseApiService.get<unknown>(
`/api/v1/video-presentations/${presentationId}`,
);
const response = parseStatusResponse(raw);
if (response) {
setStatus(response);
if (response.status === "ready" || response.status === "failed") {
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
}
}
} catch (err) {
console.error("Error polling video presentation status:", err);
}
};
poll();
pollingRef.current = setInterval(poll, 5000);
return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
};
}, [presentationId]);
if (!status || status.status === "pending" || status.status === "generating") {
return <GeneratingState title={title} />;
}
if (status.status === "failed") {
return <ErrorState title={title} error="Generation failed" />;
}
if (status.status === "ready") {
return (
<VideoPresentationPlayer
presentationId={status.id}
title={status.title || title}
/>
);
}
return <ErrorState title={title} error="Unexpected state" />;
}
export const GenerateVideoPresentationToolUI = makeAssistantToolUI<
GenerateVideoPresentationArgs,
GenerateVideoPresentationResult
>({
toolName: "generate_video_presentation",
render: function GenerateVideoPresentationUI({ args, result, status }) {
const title = args.video_title || "SurfSense Presentation";
if (status.type === "running" || status.type === "requires-action") {
return <GeneratingState title={title} />;
}
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return (
<div className="my-4 rounded-xl border border-muted p-3 sm:p-4 text-muted-foreground">
<p className="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm">
<Film className="size-3.5 sm:size-4" />
<span className="line-through">Presentation generation cancelled</span>
</p>
</div>
);
}
if (status.reason === "error") {
return (
<ErrorState
title={title}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
}
if (!result) {
return <GeneratingState title={title} />;
}
if (result.status === "failed") {
return <ErrorState title={title} error={result.error || "Generation failed"} />;
}
if (result.status === "generating") {
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="flex items-center gap-2.5 sm:gap-3">
<div className="flex size-8 sm:size-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
<Film className="size-4 sm:size-5 text-amber-500" />
</div>
<div className="min-w-0">
<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>
);
}
if (result.status === "pending" && result.video_presentation_id) {
return (
<StatusPoller
presentationId={result.video_presentation_id}
title={result.title || title}
/>
);
}
if (result.status === "ready" && result.video_presentation_id) {
return (
<VideoPresentationPlayer
presentationId={result.video_presentation_id}
title={result.title || title}
/>
);
}
return <ErrorState title={title} error="Missing presentation ID" />;
},
});

View file

@ -0,0 +1 @@
export { GenerateVideoPresentationToolUI } from "./generate-video-presentation";

View file

@ -3,6 +3,7 @@ import {
Brain,
Database,
FileText,
Film,
Globe,
ImageIcon,
Link2,
@ -16,6 +17,7 @@ import {
const TOOL_ICONS: Record<string, LucideIcon> = {
search_knowledge_base: Database,
generate_podcast: Podcast,
generate_video_presentation: Film,
generate_report: FileText,
link_preview: Link2,
display_image: ImageIcon,

View file

@ -0,0 +1,154 @@
import * as Babel from "@babel/standalone";
import React from "react";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
Sequence,
Easing,
} from "remotion";
import { DURATION_IN_FRAMES } from "./constants";
export interface CompileResult {
success: boolean;
error: string | null;
}
function createStagger(totalFrames: number) {
return function stagger(
frame: number,
fps: number,
index: number,
total: number,
): { opacity: number; transform: string } {
const enterPhase = Math.floor(totalFrames * 0.2);
const exitStart = Math.floor(totalFrames * 0.8);
const gap = Math.max(6, Math.floor(enterPhase / Math.max(total, 1)));
const delay = index * gap;
const s = spring({
frame: Math.max(0, frame - delay),
fps,
config: { damping: 15, stiffness: 120, mass: 0.8 },
});
const exit = interpolate(frame, [exitStart, totalFrames], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const ambient = s > 0.99 ? Math.sin(frame * 0.05) * 2 : 0;
const opacity = s * (1 - exit);
const translateY =
interpolate(s, [0, 1], [40, 0]) +
interpolate(exit, [0, 1], [0, -30]) +
ambient;
const scale = interpolate(s, [0, 1], [0.97, 1]);
return {
opacity,
transform: `translateY(${translateY}px) scale(${scale})`,
};
};
}
const defaultStagger = createStagger(DURATION_IN_FRAMES);
const INJECTED_NAMES = [
"React",
"AbsoluteFill",
"useCurrentFrame",
"useVideoConfig",
"spring",
"interpolate",
"Sequence",
"Easing",
"stagger",
] as const;
const linear = (t: number) => t;
const SafeEasing = new Proxy(Easing, {
get(target, prop) {
const val = target[prop as keyof typeof Easing];
if (typeof val === "function") return val;
return linear;
},
});
function buildInjectedValues(staggerFn: ReturnType<typeof createStagger>) {
return [
React,
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
Sequence,
SafeEasing,
staggerFn,
];
}
export function prepareSource(code: string): string {
const codeWithoutImports = code.replace(/^import\s+.*$/gm, "").trim();
const match = codeWithoutImports.match(
/export\s+(?:const|function)\s+(\w+)\s*(?::\s*React\.FC\s*)?=?\s*\(\s*\)\s*=>\s*\{([\s\S]*)\};?\s*$/,
);
if (match) {
return `const DynamicComponent = () => {\n${match[2].trim()}\n};`;
}
const cleanedCode = codeWithoutImports
.replace(/export\s+default\s+/, "")
.replace(/export\s+/, "");
return cleanedCode.replace(/const\s+(\w+)/, "const DynamicComponent");
}
function transpile(code: string): string {
const wrappedSource = prepareSource(code);
const transpiled = Babel.transform(wrappedSource, {
presets: ["react", "typescript"],
filename: "dynamic.tsx",
});
if (!transpiled.code) throw new Error("Transpilation produced no output");
return transpiled.code;
}
export function compileCheck(code: string): CompileResult {
if (!code?.trim()) {
return { success: false, error: "Empty code" };
}
try {
const jsCode = transpile(code);
new Function(...INJECTED_NAMES, `${jsCode}\nreturn DynamicComponent;`);
return { success: true, error: null };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Unknown compilation error",
};
}
}
export function compileToComponent(
code: string,
durationInFrames?: number,
): React.ComponentType {
const staggerFn = durationInFrames
? createStagger(durationInFrames)
: defaultStagger;
const jsCode = transpile(code);
const factory = new Function(
...INJECTED_NAMES,
`${jsCode}\nreturn DynamicComponent;`,
);
return factory(...buildInjectedValues(staggerFn)) as React.ComponentType;
}

View file

@ -0,0 +1,2 @@
export const FPS = 30;
export const DURATION_IN_FRAMES = 300;

View file

@ -0,0 +1,18 @@
declare module "dom-to-pptx" {
interface ExportOptions {
fileName?: string;
autoEmbedFonts?: boolean;
fonts?: Array<{ name: string; url: string }>;
skipDownload?: boolean;
svgAsVector?: boolean;
listConfig?: {
color?: string;
spacing?: { before?: number; after?: number };
};
}
export function exportToPptx(
elementOrSelector: string | HTMLElement | Array<string | HTMLElement>,
options?: ExportOptions,
): Promise<Blob>;
}

View file

@ -26,6 +26,7 @@
"@assistant-ui/react": "^0.11.53",
"@assistant-ui/react-ai-sdk": "^1.1.20",
"@assistant-ui/react-markdown": "^0.11.9",
"@babel/standalone": "^7.29.2",
"@electric-sql/client": "^1.4.0",
"@electric-sql/pglite": "^0.3.14",
"@electric-sql/pglite-sync": "^0.4.0",
@ -72,6 +73,9 @@
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-toolbar": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.7",
"@remotion/media": "^4.0.438",
"@remotion/player": "^4.0.438",
"@remotion/web-renderer": "^4.0.438",
"@streamdown/code": "^1.0.2",
"@streamdown/math": "^1.0.2",
"@tabler/icons-react": "^3.34.1",
@ -88,6 +92,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dom-to-pptx": "^1.1.5",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.5",
"emblor": "^1.4.8",
@ -126,6 +131,7 @@
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"remotion": "^4.0.438",
"server-only": "^0.0.1",
"sonner": "^2.0.6",
"streamdown": "^2.2.0",
@ -143,6 +149,7 @@
"@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"@types/babel__standalone": "^7.1.9",
"@types/canvas-confetti": "^1.9.0",
"@types/gapi": "^0.0.47",
"@types/google.picker": "^0.0.52",

View file

@ -23,6 +23,9 @@ importers:
'@assistant-ui/react-markdown':
specifier: ^0.11.9
version: 0.11.10(@assistant-ui/react@0.11.58(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(immer@10.2.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@babel/standalone':
specifier: ^7.29.2
version: 7.29.2
'@electric-sql/client':
specifier: ^1.4.0
version: 1.5.7
@ -161,6 +164,15 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.2.7
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@remotion/media':
specifier: ^4.0.438
version: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@remotion/player':
specifier: ^4.0.438
version: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@remotion/web-renderer':
specifier: ^4.0.438
version: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@streamdown/code':
specifier: ^1.0.2
version: 1.0.3(react@19.2.4)
@ -209,6 +221,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
dom-to-pptx:
specifier: ^1.1.5
version: 1.1.5
dotenv:
specifier: ^17.2.3
version: 17.3.1
@ -323,6 +338,9 @@ importers:
remark-math:
specifier: ^6.0.0
version: 6.0.0
remotion:
specifier: ^4.0.438
version: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
server-only:
specifier: ^0.0.1
version: 0.0.1
@ -369,6 +387,9 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.19(tailwindcss@4.2.1)
'@types/babel__standalone':
specifier: ^7.1.9
version: 7.1.9
'@types/canvas-confetti':
specifier: ^1.9.0
version: 1.9.0
@ -1074,6 +1095,10 @@ packages:
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
'@babel/standalone@7.29.2':
resolution: {integrity: sha512-VSuvywmVRS8efooKrvJzs6BlVSxRvAdLeGrAKUrWoBx1fFBSeE/oBpUZCQ5BcprLyXy04W8skzz7JT8GqlNRJg==}
engines: {node: '>=6.9.0'}
'@babel/template@7.28.6':
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
@ -1892,6 +1917,21 @@ packages:
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
'@mediabunny/aac-encoder@1.39.2':
resolution: {integrity: sha512-KD6KADVzAnW7tqhRFGBOX4uaiHbd0Yxvg0lfthj3wJLAEEgEBAvi43w+ZXWeEn54X/jpabrLe4bW/eYFFvlbUA==}
peerDependencies:
mediabunny: ^1.0.0
'@mediabunny/flac-encoder@1.39.2':
resolution: {integrity: sha512-VwBr3AzZTPEEPvt4aladZiXwOf3W293eq213zDupGQi/taS8WWNqDd3eBdf8FfvlbXATfbRiycXDKyQ0HlOZaQ==}
peerDependencies:
mediabunny: ^1.0.0
'@mediabunny/mp3-encoder@1.39.2':
resolution: {integrity: sha512-3rrodrGnUpUP8F2d1aRUl8IvjqK3jegkupbOzvOokooSAO5rXk2Lr5jZe7TnPeiVGiXfmnoJ7s9uyUOHlCd8qw==}
peerDependencies:
mediabunny: ^1.0.0
'@microsoft/fetch-event-source@2.0.1':
resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==}
@ -3329,6 +3369,27 @@ packages:
'@react-dnd/shallowequal@4.0.2':
resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
'@remotion/licensing@4.0.438':
resolution: {integrity: sha512-JYg8ebbVkDJDnKt0BZV7OEptAysKkIWTdRPVGdwPeNw6CMcCooPPDuR7gGHjPtm0Odn/A3EKUopCX3kTvyWTPQ==}
'@remotion/media@4.0.438':
resolution: {integrity: sha512-l47r+8Gyrv8KqqumLC9HU0Gq2CWNm8cGaRKkwC1VbdnqZ4bPQUf9SnYJV1U4IsWR8eAhk0i1QGa1Hc4VKYqKVQ==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@remotion/player@4.0.438':
resolution: {integrity: sha512-2OT8r0arsjxUEVEa5dNgain7ChbDG0THZglxh1Bo4U8nTSMnuX+1yyF0MVOlmT56yaESfoiGbZvnnpB6e7Ltew==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@remotion/web-renderer@4.0.438':
resolution: {integrity: sha512-cOGtgqRiD+q77Vj65gy/fo+GK1wA3G5ft2xYLPJZzddmrhhrhibyFglljY2Vu7UaXYIUWZTkGoXwdKchC09UZA==}
peerDependencies:
react: '>=18.0.0'
react-dom: '>=18.0.0'
'@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm]
@ -3798,6 +3859,21 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
'@types/babel__generator@7.27.0':
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
'@types/babel__standalone@7.1.9':
resolution: {integrity: sha512-IcCNPLqpevUD7UpV8QB0uwQPOyoOKACFf0YtYWRHcmxcakaje4Q7dbG2+jMqxw/I8Zk0NHvEps66WwS7z/UaaA==}
'@types/babel__template@7.4.4':
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/canvas-confetti@1.9.0':
resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==}
@ -3807,6 +3883,12 @@ packages:
'@types/diff-match-patch@1.0.36':
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
'@types/dom-mediacapture-transform@0.1.11':
resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==}
'@types/dom-webcodecs@0.1.13':
resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==}
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@ -3843,6 +3925,9 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@20.19.33':
resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==}
@ -4053,6 +4138,10 @@ packages:
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
engines: {node: '>= 20'}
'@xmldom/xmldom@0.8.11':
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
engines: {node: '>=10.0.0'}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -4190,6 +4279,10 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
baseline-browser-mapping@2.10.0:
resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
engines: {node: '>=6.0.0'}
@ -4346,6 +4439,9 @@ packages:
core-js@3.48.0:
resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
cosmiconfig@8.3.6:
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
engines: {node: '>=14'}
@ -4364,6 +4460,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
css-select@5.2.2:
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
@ -4481,6 +4580,9 @@ packages:
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dom-to-pptx@1.1.5:
resolution: {integrity: sha512-3uGRnPJH2oOIicClhoslkTUHxLx8GYqAK1kFvFYy2Z2BQ9RFjgenduZsKuYdiDRs2RRVtXiTKtUeZmPyMgn/Ug==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
@ -4906,6 +5008,9 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
fonteditor-core@2.6.3:
resolution: {integrity: sha512-YUryIKjkenjZ41E7JvM3V+02Ak4mTHDDTwBWgs9KBzypzHqLZHuua1UDRevZNTKawmnq1dbBAa70Jddl2+F4FQ==}
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@ -5208,6 +5313,13 @@ packages:
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
https@1.0.0:
resolution: {integrity: sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==}
icu-minify@4.8.3:
resolution: {integrity: sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==}
@ -5219,11 +5331,19 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
image-size@1.2.1:
resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
engines: {node: '>=16.x'}
hasBin: true
image-size@2.0.2:
resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==}
engines: {node: '>=16.x'}
hasBin: true
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@10.2.0:
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
@ -5235,6 +5355,9 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
inline-style-parser@0.2.7:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
@ -5386,6 +5509,9 @@ packages:
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
engines: {node: '>= 0.4'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@ -5506,6 +5632,9 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
katex@0.16.22:
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
hasBin: true
@ -5542,6 +5671,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lightningcss-android-arm64@1.31.1:
resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
engines: {node: '>= 12.0.0'}
@ -5744,6 +5876,9 @@ packages:
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
mediabunny@1.39.2:
resolution: {integrity: sha512-VcrisGRt+OI7tTPrziucJoCIPYIS/DEWY37TqzQVLWSUUHiyvsiRizEypQ3FOlhfIZ4ytAG/Mw4zxfetCTyKUg==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@ -6020,6 +6155,11 @@ packages:
oniguruma-to-es@4.3.4:
resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
opentype.js@1.3.4:
resolution: {integrity: sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==}
engines: {node: '>= 8.0.0'}
hasBin: true
optics-ts@2.4.1:
resolution: {integrity: sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==}
@ -6039,6 +6179,12 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@ -6178,6 +6324,9 @@ packages:
resolution: {integrity: sha512-mdb8TKt+YCRbGQdYar3AKNUPCyEiqcprScF4unYpGALF6HlBaEuO6wPuIqXXpCWkw4VclJYCKbb6lq6pH6bJeA==}
engines: {node: ^20.20.0 || >=22.22.0}
pptxgenjs@3.12.0:
resolution: {integrity: sha512-ZozkYKWb1MoPR4ucw3/aFYlHkVIJxo9czikEclcUVnS4Iw/M+r+TEwdlB3fyAWO9JY1USxJDt0Y0/r15IR/RUA==}
preact@10.28.4:
resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==}
@ -6193,6 +6342,9 @@ packages:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@ -6219,6 +6371,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
queue@6.0.2:
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
radix-ui@1.4.3:
resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==}
peerDependencies:
@ -6392,6 +6547,9 @@ packages:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readdirp@5.0.0:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'}
@ -6490,6 +6648,12 @@ packages:
remend@1.2.1:
resolution: {integrity: sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w==}
remotion@4.0.438:
resolution: {integrity: sha512-keG2GxHh81UFV0zFrwRh+yngXHn6C3z/Nohc+ru49M1TlIcaThG/hD2amJNH9otYzpYJxVjNF1t/i1hqcI8OGw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@ -6523,6 +6687,9 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-push-apply@1.0.0:
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
engines: {node: '>= 0.4'}
@ -6567,6 +6734,9 @@ packages:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
engines: {node: '>= 0.4'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -6666,6 +6836,9 @@ packages:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
string.prototype.codepointat@0.2.1:
resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==}
string.prototype.includes@2.0.1:
resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==}
engines: {node: '>= 0.4'}
@ -6689,6 +6862,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
@ -6763,10 +6939,16 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
throttleit@2.1.0:
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
engines: {node: '>=18'}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
@ -6845,6 +7027,9 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@ -6986,6 +7171,9 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
vaul@1.1.2:
resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
peerDependencies:
@ -7960,6 +8148,8 @@ snapshots:
'@babel/runtime@7.28.6': {}
'@babel/standalone@7.29.2': {}
'@babel/template@7.28.6':
dependencies:
'@babel/code-frame': 7.29.0
@ -8565,6 +8755,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@mediabunny/aac-encoder@1.39.2(mediabunny@1.39.2)':
dependencies:
mediabunny: 1.39.2
'@mediabunny/flac-encoder@1.39.2(mediabunny@1.39.2)':
dependencies:
mediabunny: 1.39.2
'@mediabunny/mp3-encoder@1.39.2(mediabunny@1.39.2)':
dependencies:
mediabunny: 1.39.2
'@microsoft/fetch-event-source@2.0.1': {}
'@napi-rs/wasm-runtime@0.2.12':
@ -10078,6 +10280,33 @@ snapshots:
'@react-dnd/shallowequal@4.0.2': {}
'@remotion/licensing@4.0.438': {}
'@remotion/media@4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
mediabunny: 1.39.2
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
remotion: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
zod: 4.3.6
'@remotion/player@4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
remotion: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@remotion/web-renderer@4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@mediabunny/aac-encoder': 1.39.2(mediabunny@1.39.2)
'@mediabunny/flac-encoder': 1.39.2(mediabunny@1.39.2)
'@mediabunny/mp3-encoder': 1.39.2(mediabunny@1.39.2)
'@remotion/licensing': 4.0.438
mediabunny: 1.39.2
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
remotion: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@rollup/rollup-android-arm-eabi@4.59.0':
optional: true
@ -10484,6 +10713,36 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.28.0
'@types/babel__generator@7.27.0':
dependencies:
'@babel/types': 7.29.0
'@types/babel__standalone@7.1.9':
dependencies:
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@types/babel__core': 7.20.5
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.28.0
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@types/babel__traverse@7.28.0':
dependencies:
'@babel/types': 7.29.0
'@types/canvas-confetti@1.9.0': {}
'@types/debug@4.1.12':
@ -10492,6 +10751,12 @@ snapshots:
'@types/diff-match-patch@1.0.36': {}
'@types/dom-mediacapture-transform@0.1.11':
dependencies:
'@types/dom-webcodecs': 0.1.13
'@types/dom-webcodecs@0.1.13': {}
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.8
@ -10524,6 +10789,10 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/node@18.19.130':
dependencies:
undici-types: 5.26.5
'@types/node@20.19.33':
dependencies:
undici-types: 6.21.0
@ -10737,6 +11006,8 @@ snapshots:
'@vercel/oidc@3.1.0': {}
'@xmldom/xmldom@0.8.11': {}
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
@ -10911,6 +11182,8 @@ snapshots:
balanced-match@4.0.4: {}
base64-arraybuffer@1.0.2: {}
baseline-browser-mapping@2.10.0: {}
boolbase@1.0.0: {}
@ -11052,6 +11325,8 @@ snapshots:
core-js@3.48.0: {}
core-util-is@1.0.3: {}
cosmiconfig@8.3.6(typescript@5.9.3):
dependencies:
import-fresh: 3.3.1
@ -11071,6 +11346,10 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
css-line-break@2.1.0:
dependencies:
utrie: 1.0.2
css-select@5.2.2:
dependencies:
boolbase: 1.0.0
@ -11183,6 +11462,15 @@ snapshots:
domhandler: 5.0.3
entities: 4.5.0
dom-to-pptx@1.1.5:
dependencies:
fonteditor-core: 2.6.3
html2canvas: 1.4.1
jszip: 3.10.1
opentype.js: 1.3.4
pako: 2.1.0
pptxgenjs: 3.12.0
domelementtype@2.3.0: {}
domhandler@5.0.3:
@ -11766,6 +12054,10 @@ snapshots:
flatted@3.3.3: {}
fonteditor-core@2.6.3:
dependencies:
'@xmldom/xmldom': 0.8.11
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
@ -12158,6 +12450,13 @@ snapshots:
html-void-elements@3.0.0: {}
html2canvas@1.4.1:
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
https@1.0.0: {}
icu-minify@4.8.3:
dependencies:
'@formatjs/icu-messageformat-parser': 3.5.1
@ -12166,8 +12465,14 @@ snapshots:
ignore@7.0.5: {}
image-size@1.2.1:
dependencies:
queue: 6.0.2
image-size@2.0.2: {}
immediate@3.0.6: {}
immer@10.2.0: {}
import-fresh@3.3.1:
@ -12177,6 +12482,8 @@ snapshots:
imurmurhash@0.1.4: {}
inherits@2.0.4: {}
inline-style-parser@0.2.7: {}
internal-slot@1.1.0:
@ -12334,6 +12641,8 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
isarray@1.0.0: {}
isarray@2.0.5: {}
isexe@2.0.0: {}
@ -12418,6 +12727,13 @@ snapshots:
object.assign: 4.1.7
object.values: 1.2.1
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
katex@0.16.22:
dependencies:
commander: 8.3.0
@ -12445,6 +12761,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lie@3.3.0:
dependencies:
immediate: 3.0.6
lightningcss-android-arm64@1.31.1:
optional: true
@ -12736,6 +13056,11 @@ snapshots:
mdn-data@2.0.30: {}
mediabunny@1.39.2:
dependencies:
'@types/dom-mediacapture-transform': 0.1.11
'@types/dom-webcodecs': 0.1.13
merge2@1.4.1: {}
micromark-core-commonmark@2.0.3:
@ -13180,6 +13505,11 @@ snapshots:
regex: 6.1.0
regex-recursion: 6.0.2
opentype.js@1.3.4:
dependencies:
string.prototype.codepointat: 0.2.1
tiny-inflate: 1.0.3
optics-ts@2.4.1: {}
optionator@0.9.4:
@ -13205,6 +13535,10 @@ snapshots:
dependencies:
p-limit: 3.1.0
pako@1.0.11: {}
pako@2.1.0: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@ -13368,6 +13702,13 @@ snapshots:
dependencies:
'@posthog/core': 1.23.1
pptxgenjs@3.12.0:
dependencies:
'@types/node': 18.19.130
https: 1.0.0
image-size: 1.2.1
jszip: 3.10.1
preact@10.28.4: {}
prelude-ls@1.2.1: {}
@ -13376,6 +13717,8 @@ snapshots:
prismjs@1.30.0: {}
process-nextick-args@2.0.1: {}
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
@ -13411,6 +13754,10 @@ snapshots:
queue-microtask@1.2.3: {}
queue@6.0.2:
dependencies:
inherits: 2.0.4
radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@radix-ui/primitive': 1.1.3
@ -13639,6 +13986,16 @@ snapshots:
react@19.2.4: {}
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readdirp@5.0.0: {}
recma-build-jsx@1.0.0:
@ -13825,6 +14182,11 @@ snapshots:
remend@1.2.1: {}
remotion@4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@ -13889,6 +14251,8 @@ snapshots:
has-symbols: 1.1.0
isarray: 2.0.5
safe-buffer@5.1.2: {}
safe-push-apply@1.0.0:
dependencies:
es-errors: 1.3.0
@ -13938,6 +14302,8 @@ snapshots:
es-errors: 1.3.0
es-object-atoms: 1.1.1
setimmediate@1.0.5: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.0.0
@ -14102,6 +14468,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
string.prototype.codepointat@0.2.1: {}
string.prototype.includes@2.0.1:
dependencies:
call-bind: 1.0.8
@ -14152,6 +14520,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
stringify-entities@4.0.4:
dependencies:
character-entities-html4: 2.1.0
@ -14216,8 +14588,14 @@ snapshots:
tapable@2.3.0: {}
text-segmentation@1.0.3:
dependencies:
utrie: 1.0.2
throttleit@2.1.0: {}
tiny-inflate@1.0.3: {}
tiny-invariant@1.3.1: {}
tinyexec@1.0.2: {}
@ -14307,6 +14685,8 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
undici-types@5.26.5: {}
undici-types@6.21.0: {}
unicode-canonical-property-names-ecmascript@2.0.1: {}
@ -14465,6 +14845,10 @@ snapshots:
util-deprecate@1.0.2: {}
utrie@1.0.2:
dependencies:
base64-arraybuffer: 1.0.2
vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)