Files
kandev/apps/web/components/agent-logo.tsx
T
2026-02-16 16:26:26 +00:00

78 lines
2.3 KiB
TypeScript

'use client';
import { useEffect, useState, useSyncExternalStore } from 'react';
import { useTheme } from 'next-themes';
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 <span style={{ display: 'inline-block', width: size, height: size }} className={className} />;
}
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
alt=""
width={size}
height={size}
className={className}
/>
);
}