feat: add library media viewers

This commit is contained in:
CREDO23 2026-06-23 15:18:08 +02:00
parent b63e95e987
commit 19698bcc0b
2 changed files with 130 additions and 0 deletions

View file

@ -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"
/>
);
}

View file

@ -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>
);
}