/** * @file AiToolIcon.tsx * @description An AI tool's icon: the mark shipped in `client/public/ai-tools/` * when there is one, else * the server's cached favicon (GET /api/ai-tools/:id/icon), else the label's * first letter. Every tool icon in the app renders through here. * @author Nguyễn Ngọc Trí Vĩ */ import { useState } from "react"; import { api, type AiTool } from "../lib/api"; /** Shipped marks by tool id. */ const BUNDLED: Record = { claude: "/ai-tools/claude.svg", codex: "/ai-tools/codex.svg", gemini: "/ai-tools/gemini.svg", openclaw: "/ai-tools/openclaw.svg", hermes: "/ai-tools/hermes.svg", }; /** * @param props.tool - The catalog entry. * @param props.className - Size classes (default `h-8 w-8`). */ export function AiToolIcon({ tool, className = "h-8 w-8", }: { tool: Pick; className?: string; }) { // Keyed by id: one instance showing another tool must retry its icon. const [failedId, setFailedId] = useState(null); const box = `${className} shrink-0 rounded-md bg-surface-4 object-contain p-1`; const bundled = BUNDLED[tool.id]; if (bundled) return ; if (failedId !== tool.id) { return ( setFailedId(tool.id)} className={box} /> ); } return ( {tool.label.charAt(0).toUpperCase()} ); }