mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Show provider icons in the provider list
This commit is contained in:
parent
088ec97e7b
commit
9163e306c0
3 changed files with 90 additions and 18 deletions
|
|
@ -12,7 +12,7 @@ import {
|
|||
providerAccountConnectorsTextWithNewApiUserBalanceTemplate, providerAccountSnapshotCredentialLabel, providerAccountSnapshotLabel, ProviderAccountTestPath,
|
||||
ProviderAccountTestResult, providerBaseUrl, providerCapabilitiesSummary, ProviderCredentialDraft, ProviderDeepLinkPayload, ProviderDeepLinkRequest, providerDraftSafetyIssue, providerCredentialDraftPatchFromJson, providerHttpJsonConnectorFromDraft,
|
||||
ProviderConnectivityCheckReport, providerDeepLinkDisplayIcon, providerListItemKey, providerMatchesQuery, ProviderPreset, providerPresetIconUrls, providerProbeHasSupportedProtocol,
|
||||
providerModelDisplayName, providerModelDisplayTitle, providerSelectableProtocolsFromProbe, providerUsageFieldPatch, ProviderUsageFieldTarget, providerUsageMethodOptions, Search, SelectControl,
|
||||
providerDisplayIcon, providerModelDisplayName, providerModelDisplayTitle, providerSelectableProtocolsFromProbe, providerUsageFieldPatch, ProviderUsageFieldTarget, providerUsageMethodOptions, Search, SelectControl,
|
||||
resolveProviderDeepLinkPreset, ShieldCheck, splitLines, splitModelTagInput, Switch, Textarea, translatedProviderProtocolLabel, translateOptions,
|
||||
translateProbeProtocolMessage, Trash2, uniqueProviderName, uniqueProviderProtocols, useAppErrorText, useAppText, useEffect, useMemo,
|
||||
useRef, useState, X, isPlainRecord
|
||||
|
|
@ -111,24 +111,25 @@ export function ProvidersView({ accountSnapshots, addProvider, editProvider, not
|
|||
</div>
|
||||
<div className="divide-y divide-border/60">
|
||||
<AnimatePresence initial={false}>
|
||||
{visibleProviders.map(({ provider, index }) => {
|
||||
const itemKey = providerListItemKey(provider, index);
|
||||
const expanded = expandedProviders.has(itemKey);
|
||||
{visibleProviders.map(({ provider, index }) => {
|
||||
const itemKey = providerListItemKey(provider, index);
|
||||
const expanded = expandedProviders.has(itemKey);
|
||||
const providerAccountSnapshots = accountSnapshotsByProvider.get(provider.name) ?? [];
|
||||
return (
|
||||
<AnimatedListItem key={itemKey}>
|
||||
<div
|
||||
className="grid min-h-[58px] cursor-pointer grid-cols-[minmax(160px,0.8fr)_minmax(220px,1fr)_minmax(160px,0.7fr)_minmax(150px,0.65fr)_80px_84px] items-center gap-3 px-4 py-2.5 transition-colors hover:bg-muted/35"
|
||||
onClick={() => toggleProvider(provider, index)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
toggleProvider(provider, index);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
const providerIconUrl = providerDisplayIcon(provider);
|
||||
return (
|
||||
<AnimatedListItem key={itemKey}>
|
||||
<div
|
||||
className="grid min-h-[58px] cursor-pointer grid-cols-[minmax(160px,0.8fr)_minmax(220px,1fr)_minmax(160px,0.7fr)_minmax(150px,0.65fr)_80px_84px] items-center gap-3 px-4 py-2.5 transition-colors hover:bg-muted/35"
|
||||
onClick={() => toggleProvider(provider, index)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
toggleProvider(provider, index);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<button
|
||||
aria-expanded={expanded}
|
||||
|
|
@ -143,6 +144,7 @@ export function ProvidersView({ accountSnapshots, addProvider, editProvider, not
|
|||
>
|
||||
{expanded ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
<ProviderPresetIcon className="h-8 w-8 rounded-md" iconUrl={providerIconUrl} />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[12px] font-semibold">{provider.name || t("Unnamed")}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -772,6 +772,16 @@ export function providerDeepLinkDisplayIcon(payload: ProviderDeepLinkPayload): s
|
|||
return presetIcon || payload.icon?.trim() || "";
|
||||
}
|
||||
|
||||
export function providerDisplayIcon(provider: GatewayProviderConfig): string {
|
||||
const icon = provider.icon?.trim();
|
||||
if (icon) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
const preset = findProviderPresetByBaseUrl(providerBaseUrl(provider));
|
||||
return preset ? providerPresetIconUrls[preset.id] ?? "" : "";
|
||||
}
|
||||
|
||||
export type ProviderDeepLinkCatalogModelsResolution = {
|
||||
modelDisplayNames?: Record<string, string>;
|
||||
models: string[];
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import * as React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { newApiKeyUsageAccountConfig } from "../../packages/core/src/providers/new-api.ts";
|
||||
import { geminiProviderPreset } from "../../packages/core/src/providers/presets/gemini/index.ts";
|
||||
import { ProvidersView } from "../../packages/ui/src/pages/home/components/providers.tsx";
|
||||
import {
|
||||
applyProviderProbeResult,
|
||||
createProviderDraft,
|
||||
providerDisplayIcon,
|
||||
providerAccountConnectorsTextWithNewApiUserBalanceTemplate,
|
||||
providerPresetIconUrls,
|
||||
providerProtocolOptions,
|
||||
providerProbeCandidates,
|
||||
setProviderPresets
|
||||
} from "../../packages/ui/src/pages/home/shared/index.tsx";
|
||||
import { installBrowserGlobals } from "./fixtures.ts";
|
||||
|
||||
installBrowserGlobals();
|
||||
|
||||
test("Gemini preset keeps full protocol probing candidates", () => {
|
||||
setProviderPresets([geminiProviderPreset]);
|
||||
|
|
@ -112,6 +120,58 @@ test("provider probe result applies detected New API key quota account connector
|
|||
assert.equal(connectors[0].mapping.meters[0].remaining, "$.data.total_available");
|
||||
});
|
||||
|
||||
test("provider display icon prefers custom icons and falls back to preset icons", () => {
|
||||
setProviderPresets([geminiProviderPreset]);
|
||||
|
||||
assert.equal(
|
||||
providerDisplayIcon({
|
||||
api_base_url: "https://custom.example/v1",
|
||||
icon: "https://custom.example/icon.png",
|
||||
models: [],
|
||||
name: "Custom Provider",
|
||||
type: "openai_chat_completions"
|
||||
}),
|
||||
"https://custom.example/icon.png"
|
||||
);
|
||||
assert.equal(
|
||||
providerDisplayIcon({
|
||||
api_base_url: "https://generativelanguage.googleapis.com",
|
||||
models: [],
|
||||
name: "Google Gemini",
|
||||
type: "gemini_generate_content"
|
||||
}),
|
||||
providerPresetIconUrls.gemini
|
||||
);
|
||||
});
|
||||
|
||||
test("ProvidersView renders configured provider icons in the list", () => {
|
||||
const iconUrl = "https://custom.example/icon.png";
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(ProvidersView, {
|
||||
accountSnapshots: [],
|
||||
addProvider: () => undefined,
|
||||
editProvider: () => undefined,
|
||||
notify: () => undefined,
|
||||
providers: [
|
||||
{
|
||||
index: 0,
|
||||
provider: {
|
||||
api_base_url: "https://custom.example/v1",
|
||||
icon: iconUrl,
|
||||
models: ["custom-model"],
|
||||
name: "Custom Provider",
|
||||
type: "openai_chat_completions"
|
||||
}
|
||||
}
|
||||
],
|
||||
removeProvider: () => undefined
|
||||
})
|
||||
);
|
||||
|
||||
assert.match(html, /Custom Provider/);
|
||||
assert.match(html, /src="https:\/\/custom\.example\/icon\.png"/);
|
||||
});
|
||||
|
||||
test("New API user balance template adds configurable user self connector", () => {
|
||||
const account = newApiKeyUsageAccountConfig("https://gateway.example/v1");
|
||||
const text = providerAccountConnectorsTextWithNewApiUserBalanceTemplate(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue