Skip to content
Source
Sign inSign up

ProviderInstanceIcon.tsx

194 lines7.5 KB

1import { type CSSProperties, memo } from "react";
2​
3import {
4 normalizeProviderAccentColor,
5 providerAccentForegroundColor,
6 providerInstanceInitials,
7} from "@t3tools/client-runtime/state/provider-instance-display";
8​
9import { ProviderDriverKind } from "@t3tools/contracts";
10import {
11 AnthropicIcon,
12 AntigravityIcon,
13 ClaudeAI,
14 Icon,
15 OllamaIcon,
16 OpenAI,
17 OpenRouterIcon,
18} from "../Icons";
19​
20import { cn } from "~/lib/utils";
21import { providerClients } from "../settings/providerDriverMeta";
22import { ProviderPackageIcon } from "./ProviderPackageIcon";
23import {
24 AcpRegistryAgentIcon,
25 officialAcpRegistryIconUrlForAgentId,
26 resolveOfficialAcpRegistryIconUrl,
27} from "../settings/AcpRegistryIcon";
28​
29const PROVIDER_ICON_BY_PROVIDER: Partial<Record<ProviderDriverKind, Icon>> = {
30 [ProviderDriverKind.make("codex")]: OpenAI,
31 [ProviderDriverKind.make("claudeAgent")]: ClaudeAI,
32 [ProviderDriverKind.make("antigravity")]: AntigravityIcon,
33 // Text generators, by the service they reach (see textGeneratorBrand).
34 [ProviderDriverKind.make("openai")]: OpenAI,
35 [ProviderDriverKind.make("anthropic")]: AnthropicIcon,
36 [ProviderDriverKind.make("openrouter")]: OpenRouterIcon,
37 [ProviderDriverKind.make("ollama")]: OllamaIcon,
38};
39​
40const PROVIDER_TEXT_COLOR_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {
41 [ProviderDriverKind.make("codex")]: "text-black dark:text-white",
42 [ProviderDriverKind.make("claudeAgent")]: "text-[#d97757]",
43 [ProviderDriverKind.make("antigravity")]: "text-[#5b87bf]",
44 [ProviderDriverKind.make("openai")]: "text-black dark:text-white",
45 [ProviderDriverKind.make("anthropic")]: "text-black dark:text-white",
46 [ProviderDriverKind.make("openrouter")]: "text-black dark:text-white",
47 [ProviderDriverKind.make("ollama")]: "text-black dark:text-white",
48};
49​
50const BUNDLED_LOGO_COLOR_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {
51 [ProviderDriverKind.make("claudeAgent")]: "#d97757",
52 [ProviderDriverKind.make("antigravity")]: "#5b87bf",
53};
54​
55/** The logo's own color, or null for logos that swap between black and white per theme. */
56export function providerLogoColor(driverKind: ProviderDriverKind): string | null {
57 const fill = providerClients.get(driverKind)?.icon?.fill;
58 if (fill) return fill.light === fill.dark ? fill.light : null;
59 return BUNDLED_LOGO_COLOR_BY_PROVIDER[driverKind] ?? null;
61​
62export function providerTextColor(driverKind: ProviderDriverKind): {
63 readonly className?: string;
64 readonly style?: CSSProperties;
65} {
66 const icon = providerClients.get(driverKind)?.icon;
67 if (icon) {
68 return {
69 className: "text-(--icon-light) dark:text-(--icon-dark)",
70 style: { "--icon-light": icon.fill.light, "--icon-dark": icon.fill.dark } as CSSProperties,
71 };
72 }
73 const className = PROVIDER_TEXT_COLOR_BY_PROVIDER[driverKind];
74 return className ? { className } : {};
76​
77export function resolveProviderInstanceAcpRegistryIconUrl(input: {
78 readonly driverKind: ProviderDriverKind;
79 readonly agentId?: string | undefined;
80 readonly iconUrl?: string | undefined;
81}): string | null {
82 if (input.driverKind !== "acpRegistry") return null;
83 return (
84 resolveOfficialAcpRegistryIconUrl(input.iconUrl ?? null) ??
85 officialAcpRegistryIconUrlForAgentId(input.agentId?.trim() || null)
86 );
88​
89export const ProviderInstanceIcon = memo(function ProviderInstanceIcon(props: {
90 driverKind: ProviderDriverKind;
91 displayName: string;
92 accentColor?: string | undefined;
93 acpRegistryAgentId?: string | undefined;
94 acpRegistryIconUrl?: string | undefined;
95 showBadge?: boolean;
96 badgeContent?: "initials" | "none";
97 size?: "default" | "compact";
98 className?: string;
99 iconClassName?: string;
100 badgeClassName?: string;
101 indicatorBackground?: string;
102}) {
103 const Icon = PROVIDER_ICON_BY_PROVIDER[props.driverKind] ?? null;
104 const packageIcon = providerClients.get(props.driverKind)?.icon;
105 const indicatorBackground = props.indicatorBackground ?? "var(--card)";
106 const accentColor = normalizeProviderAccentColor(props.accentColor);
107 const accentStyle = accentColor
108 ? ({
109 "--provider-accent": accentColor,
110 "--provider-accent-foreground": providerAccentForegroundColor(accentColor),
111 } as CSSProperties)
112 : undefined;
113 const badgeContent = props.badgeContent ?? "initials";
114 const iconClassName = cn(props.size === "compact" && "size-3.5", props.iconClassName);
115 const isAcpRegistry = props.driverKind === "acpRegistry";
116 // A custom OpenAI-compatible endpoint has no brand: show its name, marked with the protocol.
117 const isCustomEndpoint = props.driverKind === "openaiCompatible";
118 const acpRegistryIconUrl = resolveProviderInstanceAcpRegistryIconUrl({
119 driverKind: props.driverKind,
120 agentId: props.acpRegistryAgentId,
121 iconUrl: props.acpRegistryIconUrl,
122 });
123​
124 return (
125 <span
126 className={cn(
127 "relative isolate z-30 inline-flex shrink-0 items-center justify-center overflow-visible",
128 props.className,
129 )}
130 style={accentStyle}
131 data-provider-accent-color={accentColor}
132 >
133 {isAcpRegistry ? (
134 <AcpRegistryAgentIcon
135 className={cn("size-5 rounded-none bg-transparent", iconClassName)}
136 fallbackClassName="size-full"
137 icon={acpRegistryIconUrl}
138 />
139 ) : isCustomEndpoint ? (
140 // The tile is a size container: below ~18px it keeps one letter and drops the mark.
141 <span
142 className={cn(
143 "@container relative flex size-5 shrink-0 items-center justify-center rounded-md border bg-card text-4xs font-semibold leading-none text-foreground",
144 iconClassName,
145 )}
146 aria-hidden
147 >
148 {Array.from(providerInstanceInitials(props.displayName))[0]}
149 <span className="@max-[1.1rem]:hidden">
150 {Array.from(providerInstanceInitials(props.displayName)).slice(1).join("")}
151 </span>
152 <span
153 className={cn(
154 "pointer-events-none absolute right-[-0.3125rem] bottom-[-0.3125rem] z-10 flex size-3 items-center justify-center rounded-full border bg-card text-foreground shadow-sm @max-[1.1rem]:hidden",
155 props.badgeClassName,
156 )}
157 style={{ borderColor: indicatorBackground }}
158 >
159 <OpenAI className="size-2" />
160 </span>
161 </span>
162 ) : packageIcon ? (
163 <ProviderPackageIcon
164 icon={packageIcon}
165 className={cn("size-5 shrink-0", iconClassName)}
166 aria-hidden
167 />
168 ) : Icon ? (
169 <Icon className={cn("size-5 shrink-0", iconClassName)} aria-hidden />
170 ) : (
171 <span className={cn("text-3xs font-semibold leading-none", iconClassName)}>
172 {providerInstanceInitials(props.displayName)}
173 </span>
174 )}
175 {isCustomEndpoint ? null : props.showBadge ? (
176 <span
177 className={cn(
178 "pointer-events-none absolute right-0 bottom-0 z-10 flex h-3.5 min-w-3.5 items-center justify-center rounded-full border px-0.5 text-4xs font-semibold leading-none shadow-sm",
179 accentColor
180 ? "bg-(--provider-accent) text-(--provider-accent-foreground)"
181 : "bg-card text-muted-foreground",
182 props.size === "compact" &&
183 "right-[-0.1875rem] bottom-[-0.1875rem] h-3 min-w-3 px-0.5 text-5xs",
184 props.badgeClassName,
185 )}
186 style={{ borderColor: indicatorBackground }}
187 aria-hidden
188 >
189 {badgeContent === "initials" ? providerInstanceInitials(props.displayName) : null}
190 </span>
191 ) : null}
192 </span>
193 );
194});