import { Button } from "@tutti-os/ui-system";
import {
AlertCircle,
LoaderCircle,
RotateCcw,
type LucideIcon
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState, type JSX } from "react";
import {
useOptionalAgentGUIRuntime,
type AgentGUIRuntime
} from "../../../agentActivityRuntime";
import { ZoomableImage } from "../../../app/renderer/components/ZoomableImage";
import { useTranslation } from "../../../i18n/index";
import styles from "../../../agent-gui/agentGuiNode/AgentGUIConversation.styles";
import type {
AgentMessageContentVM,
AgentMessageImageVM
} from "../contracts/agentMessageRowVM";
type ImageFailureKind = "load" | "read" | "unavailable";
type ImageFailureUnavailableReason = "source_vm";
type ImageFailureSource =
| {
kind: "resolved";
src: string;
}
| {
agentSessionId: string;
attachmentId: string;
kind: "locator";
path: string;
readerAvailable: boolean;
unavailableReason?: ImageFailureUnavailableReason;
workspaceId: string;
};
type ImageFailureReporter = (
image: AgentMessageImageVM,
kind: ImageFailureKind,
attempt: number
) => void;
export function AgentUserImageGrid({
message
}: {
message: AgentMessageContentVM;
}): JSX.Element {
"use memo";
const images = message.images ?? [];
const runtime = useOptionalAgentGUIRuntime();
const reportFailure = useCallback(
(image: AgentMessageImageVM, kind: ImageFailureKind, attempt: number) => {
reportImageLoadFailure(runtime, image, kind, attempt);
},
[runtime]
);
const { failedSources, loadingIds, retryCounts, sources, markFailed, retry } =
useAgentMessageImageSources(images, reportFailure);
const { t } = useTranslation();
const columnCount = Math.min(Math.max(images.length, 1), 4);
const thumbnailWidth = images.length === 1 ? "160px" : "80px";
return (
{images.map((image) => {
const src = sources.get(image.id) ?? imageSourceUrl(image);
const failureSource = imageFailureSource(image, src, runtime);
const failedSource = failedSources.get(image.id);
const displayedFailureSource = failedSource ?? failureSource;
const failed = isMatchingImageFailure(failedSource, failureSource);
const canRetry = canRetryImageFailure(displayedFailureSource);
const attempt = retryCounts.get(image.id) ?? 0;
return (
{
markFailed(image.id, failureSource);
reportFailure(image, "load", attempt);
}}
onRetry={() => retry(image.id)}
/>
);
})}
);
}
function AgentUserImageTile({
attempt,
canRetry,
failed,
failureLabel,
image,
onFailed,
onRetry,
retryLabel,
sourceLoading,
src
}: {
attempt: number;
canRetry: boolean;
failed: boolean;
failureLabel: string;
image: AgentMessageImageVM;
onFailed: () => void;
onRetry: () => void;
retryLabel: string;
sourceLoading: boolean;
src: string | null;
}): JSX.Element {
const [loadedImage, setLoadedImage] = useState<{
attempt: number;
src: string;
} | null>(null);
const browserLoaded = Boolean(
src && loadedImage?.attempt === attempt && loadedImage.src === src
);
const showingFailure = failed || (!src && !sourceLoading);
const loading = !failed && (src ? !browserLoaded : sourceLoading);
const state = showingFailure ? "failed" : loading ? "loading" : "loaded";
return (
{showingFailure ? (
) : src ? (
<>
{
setLoadedImage((current) =>
current?.attempt === attempt && current.src === src
? current
: { attempt, src }
);
}}
onError={onFailed}
/>
{loading ? : null}
>
) : (
)}
);
}
function useAgentMessageImageSources(
images: readonly AgentMessageImageVM[],
reportFailure: ImageFailureReporter
): {
failedSources: ReadonlyMap;
loadingIds: ReadonlySet;
retryCounts: ReadonlyMap;
sources: ReadonlyMap;
markFailed: (imageId: string, source: ImageFailureSource) => void;
retry: (imageId: string) => void;
} {
const runtime = useOptionalAgentGUIRuntime();
const [failedSources, setFailedSources] = useState<
Map
>(() => new Map());
const [sources, setSources] = useState