(null);
const downloads = useRef({ generation: 0 }).current;
useEffect(() => {
let current = true;
setState(null);
setDownloading(false);
setDownloadError(null);
void Promise.all([
client.getRetainedArtifact(workspaceId, artifactId),
client.getFile(workspaceId, artifactId).catch(() => null),
])
.then(([artifact, file]) => {
if (!current) return;
if (!artifact.available || artifact.artifactId !== artifactId)
throw new Error("This artifact is no longer available.");
const own = file?.id === artifactId && file.workspaceId === workspaceId ? file : undefined;
setState({
key,
client,
artifact,
filename: own?.filename,
...(own ? { file: own } : {}),
});
})
.catch((error: unknown) => {
if (current)
setState({
key,
client,
error: error instanceof Error ? error : new Error("Artifact could not be loaded."),
});
});
return () => {
current = false;
downloads.generation++;
};
}, [key, client, workspaceId, artifactId, retry, downloads]);
const loaded = state?.key === key && state.client === client ? state : null;
const artifact = loaded?.artifact;
const filename =
loaded?.filename || (artifact?.contentType.startsWith("image/") ? "Image" : "Artifact");
// Only a file with something to look at has a full view; the rest open on
// their page, where Download is.
const expandable =
!embedded &&
artifact !== undefined &&
retainedPreviewKind(artifact.contentType, loaded?.filename) !== null;
const workspace = context.workspaces.find((candidate) => candidate.id === workspaceId) ?? null;
// A Personal workspace has nobody to share with, so there is no audience.
// Session screenshots and saved tool output stay with their chat.
const file = loaded?.file;
const showAudience =
file !== undefined &&
file.scope !== undefined &&
artifact?.kind !== "computer_screenshot" &&
artifact?.kind !== "browser_screenshot" &&
artifact?.kind !== "tool_result" &&
!isPersonalWorkspace(workspace, context.managedSelfContext);
const audience = useArtifactAudience({
client,
workspaceId,
workspaceName: workspace?.name ?? "this workspace",
title: filename,
visibility: file?.scope === "personal" ? "private" : "workspace",
ownedByCurrentUser: Boolean(
showAudience &&
file?.ownerSubjectId &&
file.ownerSubjectId === context.accessContext.subjectId &&
hasWorkspacePermission(context.accessContext, workspaceId, "files:upload"),
),
copy: FILE_AUDIENCE_COPY,
update: async (visibility) => {
const response = await client.updateFileVisibility(workspaceId, artifactId, { visibility });
setState((current) => (current?.key === key ? { ...current, file: response.file } : current));
invalidateArtifactCatalog(client, workspaceId);
},
});
const patchLoader = useMemo(
() => createWorkspaceRetainedArtifactLoader(client, workspaceId),
[client, workspaceId],
);
const download = async () => {
if (!artifact || downloading) return;
setDownloading(true);
setDownloadError(null);
const generation = ++downloads.generation;
try {
if (artifact.kind === "generated_video") {
const source = await client.createVideoArtifactPlaybackSource(workspaceId, artifactId);
if (generation === downloads.generation) {
const link = document.createElement("a");
link.href = source.url;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.click();
}
return;
}
const result = await client.downloadRetainedArtifact(workspaceId, artifact);
if (generation === downloads.generation)
saveRetainedArtifact(artifact, result.bytes, filename);
} catch (error) {
if (generation === downloads.generation) setDownloadError(userErrorText(error, "Try again."));
} finally {
if (generation === downloads.generation) setDownloading(false);
}
};
const downloadButton = artifact ? (
void download()} disabled={downloading}>
{downloading ? "Opening…" : artifact.kind === "generated_video" ? "Open video" : "Download"}
) : null;
const downloadNotice =
artifact && downloadError ? (
{downloadError}
) : null;
const preview = artifact ? (
) : null;
const header = artifact ? (
}
title={filename}
chips={showAudience ? audience.chip : null}
meta={[
fileTypeLabel(artifact.contentType, loaded?.filename),
formatBytes(artifact.originalBytes),
]}
actions={
<>
{showAudience ? audience.shareButton : null}
{expandable ? (
setFullView(true)}>
Full screen
) : null}
{downloadButton}
>
}
/>
) : null;
if (expandable && fullView)
return (
setFullView(false)} actions={downloadButton}>
{downloadNotice ? {downloadNotice}
: null}
{preview}
);
return (
{!loaded ? (
<>
Loading artifact…
>
) : null}
{loaded?.error ? (
setRetry((value) => value + 1)}
/>
) : null}
{artifact ? (
<>
{header}
{downloadNotice}
{artifact.kind === "file" && loaded?.filename && isPatchFilename(loaded.filename) ? (
) : null}
{preview}
>
) : null}
{showAudience ? audience.dialog : null}
);
}
/** "PNG", "PDF", "CSV": the file's type as a person names it, never a MIME string. */
function fileTypeLabel(contentType: string, filename?: string): string {
const extension = filename?.match(/\.([A-Za-z0-9]{1,5})$/)?.[1];
const subtype = contentType.split(";")[0]?.split("/")[1] ?? "";
const word = extension ?? (/^[a-z0-9]{1,5}$/i.test(subtype) ? subtype : "");
const kind = contentType.startsWith("image/")
? "image"
: contentType.startsWith("video/")
? "video"
: "file";
return word
? `${word.toUpperCase()} ${kind}`
: kind === "file"
? "File"
: kind === "image"
? "Image"
: "Video";
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function RetainedArtifactLoadError({ error, onRetry }: { error: Error; onRetry: () => void }) {
const presentation = retainedArtifactLoadErrorPresentation(error);
// Someone else's Only me file is not a failure: it is private, and its
// owner can share it. Nothing about the file itself is shown.
const privateFile = isPrivateFileLoadError(error);
return (
} : {})}
title={presentation.title}
actionLayout="responsive"
action={
presentation.retryable ? (
) : undefined
}
>
{presentation.description}
{presentation.supportReference ? (
) : null}
);
}