mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
feat: add library media viewers
This commit is contained in:
parent
b63e95e987
commit
19698bcc0b
2 changed files with 130 additions and 0 deletions
|
|
@ -0,0 +1,45 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image, ImageLoading } from "@/components/tool-ui/image";
|
||||
import { imageGenerationsApiService } from "@/lib/apis/image-generations-api.service";
|
||||
|
||||
function extractImageSrc(responseData: Record<string, unknown> | null | undefined): string | null {
|
||||
const data = (responseData as { data?: unknown } | null | undefined)?.data;
|
||||
if (!Array.isArray(data) || data.length === 0) return null;
|
||||
const first = data[0] as { url?: string; b64_json?: string };
|
||||
if (first?.url) return first.url;
|
||||
if (first?.b64_json) return `data:image/png;base64,${first.b64_json}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function LibraryImageViewer({ imageId, prompt }: { imageId: number; prompt: string }) {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["image-generation-detail", imageId],
|
||||
queryFn: () => imageGenerationsApiService.getDetail(imageId),
|
||||
});
|
||||
|
||||
if (isLoading) return <ImageLoading title="Loading image" maxWidth="640px" />;
|
||||
|
||||
const src = extractImageSrc(data?.response_data);
|
||||
if (error || !src) {
|
||||
return (
|
||||
<p className="px-6 py-10 text-center text-sm text-muted-foreground">
|
||||
{data?.error_message || "Image not available"}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
id={`library-image-${imageId}`}
|
||||
assetId={String(imageId)}
|
||||
src={src}
|
||||
alt={prompt}
|
||||
title={prompt}
|
||||
domain="ai-generated"
|
||||
ratio="auto"
|
||||
maxWidth="640px"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { LibraryArtifact, LibraryArtifactKind } from "../model/artifact";
|
||||
import { LibraryImageViewer } from "./library-image-viewer";
|
||||
|
||||
const ViewerFallback = () => (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
const PodcastPlayer = dynamic(
|
||||
() => import("@/components/tool-ui/podcast/player").then((m) => m.PodcastPlayer),
|
||||
{ ssr: false, loading: ViewerFallback }
|
||||
);
|
||||
|
||||
const VideoPresentationViewer = dynamic(
|
||||
() => import("@/components/tool-ui/video-presentation").then((m) => m.VideoPresentationViewer),
|
||||
{ ssr: false, loading: ViewerFallback }
|
||||
);
|
||||
|
||||
// `stretch` overrides the players' inline-chat max-w/margins so they fill the dialog.
|
||||
function dialogLayout(kind: LibraryArtifactKind): { width: string; stretch: boolean } {
|
||||
if (kind === "video") return { width: "max-w-4xl", stretch: true };
|
||||
if (kind === "podcast") return { width: "max-w-2xl", stretch: true };
|
||||
return { width: "max-w-2xl", stretch: false };
|
||||
}
|
||||
|
||||
function MediaViewerBody({ artifact }: { artifact: LibraryArtifact }) {
|
||||
if (artifact.kind === "podcast") {
|
||||
return <PodcastPlayer podcastId={artifact.entityId} title={artifact.title} />;
|
||||
}
|
||||
if (artifact.kind === "video") {
|
||||
return <VideoPresentationViewer presentationId={artifact.entityId} title={artifact.title} />;
|
||||
}
|
||||
return <LibraryImageViewer imageId={artifact.entityId} prompt={artifact.title} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal viewer for inline-media artifacts (podcast, video, image). Reports and
|
||||
* resumes use the shared report panel instead and never reach this dialog.
|
||||
*/
|
||||
export function MediaViewerDialog({
|
||||
artifact,
|
||||
onClose,
|
||||
}: {
|
||||
artifact: LibraryArtifact | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const layout = artifact ? dialogLayout(artifact.kind) : null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={artifact !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose();
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
// pt-12 keeps content clear of the absolute top-right close button.
|
||||
"flex max-h-[88vh] w-[95vw] flex-col overflow-y-auto pt-12",
|
||||
layout?.width ?? "max-w-2xl"
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="sr-only">{artifact?.title ?? "Artifact"}</DialogTitle>
|
||||
{artifact ? (
|
||||
<div
|
||||
className={cn(
|
||||
layout?.stretch
|
||||
? "w-full [&>div]:!my-0 [&>div]:!max-w-none [&>div>*]:!max-w-none"
|
||||
: "flex justify-center"
|
||||
)}
|
||||
>
|
||||
<MediaViewerBody artifact={artifact} />
|
||||
</div>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue