Files
2026-06-18 18:36:48 +01:00

89 lines
2.6 KiB
TypeScript

"use client";
import { useEffect, useState, useSyncExternalStore } from "react";
import { useTheme } from "@/components/theme/app-theme";
import { getBackendConfig } from "@/lib/config";
interface AgentLogoProps {
agentName: string;
size?: number;
className?: string;
}
const emptySubscribe = () => () => {};
// Module-level cache: logo URL → object URL (blob). Survives re-renders and remounts.
const logoCache = new Map<string, string>();
function fetchLogo(url: string): Promise<string> {
const cached = logoCache.get(url);
if (cached) return Promise.resolve(cached);
return fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`Logo fetch failed: ${res.status}`);
return res.blob();
})
.then((blob) => {
const objectUrl = URL.createObjectURL(blob);
logoCache.set(url, objectUrl);
return objectUrl;
});
}
/** Wrapper that re-mounts the inner image when agentName changes, resetting error state. */
export function AgentLogo({ agentName, size = 16, className }: AgentLogoProps) {
return <AgentLogoImage key={agentName} agentName={agentName} size={size} className={className} />;
}
function AgentLogoImage({ agentName, size = 16, className }: AgentLogoProps) {
const { resolvedTheme } = useTheme();
const mounted = useSyncExternalStore(
emptySubscribe,
() => true,
() => false,
);
const [fetchedSrc, setFetchedSrc] = useState<string | null>(null);
const [error, setError] = useState(false);
const variant = resolvedTheme === "dark" ? "dark" : "light";
const logoUrl = mounted
? `${getBackendConfig().apiBaseUrl}/api/v1/agents/${encodeURIComponent(agentName)}/logo?variant=${variant}`
: null;
// Read from cache during render to avoid setState in effect
const cachedSrc = logoUrl ? (logoCache.get(logoUrl) ?? null) : null;
useEffect(() => {
if (!logoUrl || logoCache.has(logoUrl)) return;
fetchLogo(logoUrl)
.then(setFetchedSrc)
.catch(() => setError(true));
}, [logoUrl]);
const src = cachedSrc ?? fetchedSrc;
if (!mounted || error || !src) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
style={{ display: "inline-block", opacity: 0.5 }}
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
);
}
return <img src={src} alt="" width={size} height={size} className={className} />;
}