import { PdfPreviewSurface } from "../../lib/app-surfaces";
import { Link } from "../../lib/navigation";
import {
Component,
lazy,
Suspense,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
type RefObject,
} from "react";
import type { RetainedArtifactReference } from "@opengeni/sdk";
import {
createWorkspaceRetainedArtifactLoader,
useLightboxGalleryItem,
type LightboxGalleryEntry,
} from "../../../timeline";
import { isRetainedImageContentType } from "../../../artifacts";
import { useAppContext } from "../../app-context";
import { Button } from "../../../ui/button";
import { cn } from "../../lib/utils";
import {
PRIVATE_FILE_PRESENTATION,
isPrivateFileLoadError,
} from "../../lib/retained-artifact-load-error";
import { InlineChatImage } from "./inline-chat-image";
import { DeferredChatMedia } from "../../../artifacts";
import { isRetainedTextPreview } from "./retained-text-preview-policy";
import {
cachedRetainedArtifactLoader,
cachedRetainedMetadata,
retainedImageCache,
retainedImageScope,
} from "./retained-image-cache";
const RetainedTextPreview = lazy(() => import("./retained-text-preview"));
export function retainedPreviewKind(contentType: string, filename?: string) {
// Older sandbox publications predate media MIME classification. Use only the
// saved filename, never chat labels, and leave the integrity receipt unchanged.
if (contentType === "application/octet-stream" && filename) {
const extension = filename.split(".").pop()?.toLowerCase();
if (["mp4", "webm", "ogv"].includes(extension ?? "")) return "video";
if (["mp3", "m4a", "ogg", "wav", "flac"].includes(extension ?? "")) return "audio";
}
if (isRetainedImageContentType(contentType)) return "image";
if (["video/mp4", "video/webm", "video/ogg"].includes(contentType)) return "video";
if (
["audio/mpeg", "audio/mp4", "audio/ogg", "audio/wav", "audio/webm", "audio/flac"].includes(
contentType,
)
)
return "audio";
if (contentType === "application/pdf") return "pdf";
return null;
}
/** The same authorized preview is used by the artifact page and chat. */
type PreviewProps = {
workspaceId: string;
artifact: RetainedArtifactReference;
title: string;
filename?: string | undefined;
/** Explicit opt-in for the workbench; chat and other viewers stay unchanged. */
workbenchTextPreview?: boolean | undefined;
/** Let artifact pages size images and video to the window instead of the chat slot. */
fullSize?: boolean | undefined;
};
export function RetainedFilePreview(props: PreviewProps) {
const { accessKeyVersion } = useAppContext();
if (!props.artifact.available) return
Artifact unavailable.
;
if (
props.workbenchTextPreview &&
props.artifact.kind === "file" &&
isRetainedTextPreview(props.artifact.contentType, props.filename)
)
return (
Loading preview…}>
);
return (
);
}
function RetainedFilePreviewBody({
workspaceId,
artifact: initialArtifact,
title,
filename,
fullSize,
}: PreviewProps) {
// The parent remounts on receipt changes, not object allocation on a rerender.
const [artifact] = useState(initialArtifact);
const { client, accessKeyVersion } = useAppContext();
const kind = retainedPreviewKind(artifact.contentType, filename);
const [retry, setRetry] = useState(0);
const [failed, setFailed] = useState(false);
const [source, setSource] = useState<{
key: string;
client: typeof client;
url: string;
} | null>(null);
const key = `${workspaceId}:${artifact.artifactId}:${artifact.sha256}:${accessKeyVersion}:${retry}`;
useEffect(() => {
setSource(null);
setFailed(false);
if (!kind || kind === "image") return;
const controller = new AbortController();
let objectUrl: string | undefined;
void (async () => {
// PDFs need an inline Blob rather than a storage URL with download disposition.
const url =
kind === "pdf"
? URL.createObjectURL(
new Blob(
[
(
await client.downloadRetainedArtifact(workspaceId, artifact, {
signal: controller.signal,
})
).bytes as BlobPart,
],
{ type: "application/pdf" },
),
)
: artifact.kind === "generated_video"
? (
await client.createVideoArtifactPlaybackSource(workspaceId, artifact.artifactId, {
signal: controller.signal,
})
).url
: (
await client.createRetainedArtifactDownloadUrl(workspaceId, artifact, {
signal: controller.signal,
})
).url;
if (kind === "pdf") objectUrl = url;
if (controller.signal.aborted) {
if (objectUrl) URL.revokeObjectURL(objectUrl);
return;
}
setSource({ key, client, url });
})().catch(() => {
if (!controller.signal.aborted) setFailed(true);
});
return () => {
controller.abort();
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [client, key, kind, workspaceId, artifact]);
if (kind === "image")
return (
);
if (!kind)
return (
Preview is not available for this file. Download it to open it.
);
if (failed)
return (
Preview could not be loaded. Use the artifact action to open the file.{" "}
);
if (!source || source.key !== key || source.client !== client)
return (
Loading preview…
);
if (kind === "video")
return (