import { useEffect, useMemo, useState } from "react";
import type { RetainedArtifactMetadata, RetainedArtifactReference } from "@opengeni/sdk";
import { isRetainedImageContentType, useRetainedImageObjectUrl } from "../../../artifacts";
import { createWorkspaceRetainedArtifactLoader, useLightboxOptional } from "../../../timeline";
import { useAppContext } from "../../app-context";
import { Button } from "../../../ui/button";
import { Link } from "../../lib/navigation";
import { DeferredChatMedia } from "../../../artifacts";
import {
cachedRetainedArtifactLoader,
cachedRetainedMetadata,
retainedImageCache,
retainedImageScope,
} from "./retained-image-cache";
const CHAT_IMAGE_HEIGHT = 360;
type InlineChatImageProps = {
workspaceId: string;
artifactId: string;
alt: string;
/** Non-interactive image for a surrounding catalog link; no nested controls. */
thumbnail?: boolean | undefined;
/** Full artifact viewer uses the page width instead of the fixed chat slot. */
viewer?: boolean | undefined;
showArtifactLink?: boolean | undefined;
fromSession?: string | undefined;
};
export function InlineChatImage(props: InlineChatImageProps) {
if (props.thumbnail || props.viewer) return ;
return (
{/* Metadata must not change the height reserved before viewport entry. */}
);
}
function InlineChatImageBody({
workspaceId,
artifactId,
alt,
thumbnail = false,
viewer = false,
showArtifactLink = false,
fromSession,
}: InlineChatImageProps) {
const { client, accessKeyVersion } = useAppContext();
const cache = retainedImageCache(client);
const scope = retainedImageScope(accessKeyVersion, workspaceId, artifactId);
// A remount (tab switch, search, scroll) starts from what this page already has.
const [loaded, setLoaded] = useState<{
client: typeof client;
workspaceId: string;
accessKeyVersion: number;
artifact: RetainedArtifactReference;
} | null>(() => {
const artifact = cache.peek(`${scope}meta`);
return artifact && usableImage(artifact, artifactId)
? { client, workspaceId, accessKeyVersion, artifact }
: null;
});
const [error, setError] = useState(false);
const [retry, setRetry] = useState(0);
useEffect(() => {
let active = true;
setError(false);
void cachedRetainedMetadata(retainedImageCache(client), scope, () =>
client.getRetainedArtifact(workspaceId, artifactId),
)
.then((artifact) => {
if (!active) return;
if (!usableImage(artifact, artifactId)) {
setError(true);
return;
}
setLoaded((current) =>
current?.artifact === artifact &&
current.client === client &&
current.workspaceId === workspaceId &&
current.accessKeyVersion === accessKeyVersion
? current
: { client, workspaceId, accessKeyVersion, artifact },
);
})
.catch(() => {
if (active) setError(true);
});
return () => {
active = false;
};
}, [client, workspaceId, accessKeyVersion, artifactId, scope, retry]);
const load = useMemo(
() =>
cachedRetainedArtifactLoader(
retainedImageCache(client),
accessKeyVersion,
workspaceId,
createWorkspaceRetainedArtifactLoader(client, workspaceId),
),
[client, workspaceId, accessKeyVersion],
);
// Retry means "fetch again": never answer it from the cache.
const retryFresh = () => {
cache.forget(scope);
setLoaded(null);
setRetry((v) => v + 1);
};
if (error)
return (
{alt || "Image"} unavailable.{" "}
{!thumbnail ? (
) : null}
);
if (
!loaded ||
loaded.client !== client ||
loaded.workspaceId !== workspaceId ||
loaded.accessKeyVersion !== accessKeyVersion ||
loaded.artifact.artifactId !== artifactId
)
return (
Loading image…
);
const image = (
);
return (
<>
{thumbnail ? (
image
) : (
{image}
)}
{showArtifactLink && !thumbnail ? (
Open in Artifacts
) : null}
>
);
}
function usableImage(
artifact: RetainedArtifactMetadata,
artifactId: string,
): artifact is RetainedArtifactReference {
return (
artifact.available &&
artifact.artifactId === artifactId &&
isRetainedImageContentType(artifact.contentType)
);
}
function LoadedImage({
artifact,
load,
alt,
onRetry,
thumbnail,
viewer,
}: {
artifact: RetainedArtifactReference;
load: ReturnType;
alt: string;
onRetry: () => void;
thumbnail: boolean;
viewer: boolean;
}) {
const state = useRetainedImageObjectUrl(artifact, load);
const lightbox = useLightboxOptional();
const [failed, setFailed] = useState(false);
if (state.kind === "loading")
return (
Loading image…
);
if (state.kind !== "ready" || failed)
return (
{alt || "Image"} unavailable.{" "}
{!thumbnail ? (
) : null}
);
const image = (
setFailed(true)}
/>
);
return lightbox && !thumbnail ? (
) : (
image
);
}