import { useEffect, useMemo, useRef, useState, type ComponentType, type SVGProps } from "react"; import { Button, Input, Modal } from "antd"; import { Cpu, Search, X } from "lucide-react"; import { toc } from "@lobehub/icons/es/toc"; import { cn } from "@/lib/utils"; type LobeIconComponent = ComponentType & { size?: number | string }>; // 只允许按需加载 Mono 组件。这里不能使用 eager glob:模型 Logo 目录有数百个 provider 模块, // eager 会让每次进入工作区都发起数百个开发模块请求,即使用户从未打开 Logo 选择器。 const iconModules = "Bun" in globalThis ? {} : import.meta.glob("../../node_modules/@lobehub/icons/es/*/components/Mono.js", { import: "default" }); const iconLoaders = Object.fromEntries( Object.entries(iconModules) .map(([path, loader]) => [path.match(/\/([^/]+)\/components\/Mono\.js$/)?.[1], loader]) .filter((entry): entry is [string, () => Promise] => Boolean(entry[0] && entry[1])), ) as Record Promise>; const iconRegistry = new Map(); const iconLoadPromises = new Map>(); const iconOptions = toc .filter((item) => item.group === "model" || item.group === "provider" || item.group === "application") .map((item) => ({ id: item.id, title: item.fullTitle || item.title })) .filter((item) => Boolean(iconLoaders[item.id])); function loadIcon(icon?: string) { if (!icon) return Promise.resolve(undefined); const cached = iconRegistry.get(icon); if (cached) return Promise.resolve(cached); const existing = iconLoadPromises.get(icon); if (existing) return existing; const loader = iconLoaders[icon]; if (!loader) return Promise.resolve(undefined); const promise = loader() .then((module) => { const loaded = (module as unknown as { default?: LobeIconComponent }).default || module; iconRegistry.set(icon, loaded); return loaded; }) .catch(() => undefined); iconLoadPromises.set(icon, promise); return promise; } export function ModelLogo({ icon, size = 18, className }: { icon?: string; size?: number; className?: string }) { const [Icon, setIcon] = useState(() => (icon ? iconRegistry.get(icon) : undefined)); useEffect(() => { let cancelled = false; setIcon(icon ? iconRegistry.get(icon) : undefined); if (!icon || !iconLoaders[icon]) return () => { cancelled = true; }; void loadIcon(icon).then((loaded) => { if (!cancelled) setIcon(loaded); }); return () => { cancelled = true; }; }, [icon]); if (!Icon) return ; return ; } export function ModelIconPicker({ value, onChange }: { value?: string; onChange?: (value: string) => void }) { const [open, setOpen] = useState(false); const [keyword, setKeyword] = useState(""); const searchInputRef = useRef(null); useEffect(() => { if (open) { const timer = setTimeout(() => searchInputRef.current?.focus(), 100); return () => clearTimeout(timer); } else { setKeyword(""); } }, [open]); const filteredIcons = useMemo(() => { const query = keyword.trim().toLowerCase(); return query ? iconOptions.filter((item) => `${item.id} ${item.title}`.toLowerCase().includes(query)) : iconOptions; }, [keyword]); const selectedOption = useMemo(() => (value ? iconOptions.find((item) => item.id === value) : undefined), [value]); return ( <> setOpen(false)} width={540} centered destroyOnClose footer={
共 {filteredIcons.length} 个可用 Logo
{value ? ( ) : null}
} >
} value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="搜索品牌或模型名称(如 OpenAI, Claude, Google, Flux...)" allowClear />
{filteredIcons.length ? ( filteredIcons.map((item) => { const selected = value === item.id; return ( ); }) ) : (
未找到与 “{keyword}” 相关的 Logo
)}
); }