From 9a2709dde94eb52345a741331d87f2ec16198bd1 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 25 Jun 2026 21:27:19 -0700 Subject: [PATCH] FN-6969: show provider icons beside Command Center models Show inferred provider icons beside model names across Command Center bar and table surfaces. - Add dashboard-side provider-icon inference for model/provider strings. - Render provider icons on Command Center model bar rows and token table rows while keeping pie chart labels text-only. - Cover overview, token, and ecosystem model-icon surfaces with tests and add a changeset. Files changed: .changeset/fn-6969-provider-icons.md | 7 ++ .../components/command-center/CommandCenter.tsx | 15 ++- .../__tests__/CommandCenter.test.tsx | 33 ++++++- .../command-center/areas/EcosystemArea.tsx | 15 ++- .../components/command-center/areas/TokensArea.tsx | 37 +++++++- .../areas/__tests__/TokensArea.test.tsx | 105 +++++++++++++++++++++ .../command-center/areas/__tests__/areas.test.tsx | 42 ++++++++- .../app/components/command-center/charts/Bar.tsx | 25 ++++- .../components/command-center/charts/charts.css | 42 +++++++++ .../app/utils/__tests__/providerIconKey.test.ts | 32 +++++++ packages/dashboard/app/utils/providerIconKey.ts | 45 +++++++++ 11 files changed, 377 insertions(+), 21 deletions(-) Fusion-Task-Id: FN-6969 Fusion-Task-Lineage: 6caf8864-1ecc-46e3-b7fa-bfa8e9a70aaa --- .changeset/fn-6969-provider-icons.md | 7 ++ .../command-center/CommandCenter.tsx | 15 ++- .../__tests__/CommandCenter.test.tsx | 33 +++++- .../command-center/areas/EcosystemArea.tsx | 15 ++- .../command-center/areas/TokensArea.tsx | 37 +++++- .../areas/__tests__/TokensArea.test.tsx | 105 ++++++++++++++++++ .../areas/__tests__/areas.test.tsx | 42 ++++++- .../components/command-center/charts/Bar.tsx | 25 ++++- .../command-center/charts/charts.css | 42 +++++++ .../utils/__tests__/providerIconKey.test.ts | 32 ++++++ .../dashboard/app/utils/providerIconKey.ts | 45 ++++++++ 11 files changed, 377 insertions(+), 21 deletions(-) create mode 100644 .changeset/fn-6969-provider-icons.md create mode 100644 packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx create mode 100644 packages/dashboard/app/utils/__tests__/providerIconKey.test.ts create mode 100644 packages/dashboard/app/utils/providerIconKey.ts diff --git a/.changeset/fn-6969-provider-icons.md b/.changeset/fn-6969-provider-icons.md new file mode 100644 index 0000000000..6f53dd3a4f --- /dev/null +++ b/.changeset/fn-6969-provider-icons.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Show provider icons next to Command Center model names. +category: feature +dev: Infers provider icons from model ids for Command Center model tables and bar charts; pie charts remain text-only. diff --git a/packages/dashboard/app/components/command-center/CommandCenter.tsx b/packages/dashboard/app/components/command-center/CommandCenter.tsx index 17b8a1423f..4833420e75 100644 --- a/packages/dashboard/app/components/command-center/CommandCenter.tsx +++ b/packages/dashboard/app/components/command-center/CommandCenter.tsx @@ -21,6 +21,7 @@ import { NodesView } from "../NodesView"; import type { ToastType } from "../../hooks/useToast"; import type { TaskView } from "../../hooks/useViewState"; import { SdlcFunnel } from "./SdlcFunnel"; +import { inferProviderIconKey } from "../../utils/providerIconKey"; import { Bar, type BarDatum } from "./charts/Bar"; import { Sparkline } from "./charts/Sparkline"; import { LineChart as RechartsLineChart, PieChart } from "./charts/recharts"; @@ -182,11 +183,15 @@ function OverviewTab({ [...(tokens.data?.groups ?? [])] .sort((a, b) => b.totalTokens - a.totalTokens || (a.key ?? "").localeCompare(b.key ?? "")) .slice(0, 8) - .map((g) => ({ - label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"), - value: g.totalTokens, - valueLabel: formatCount(g.totalTokens), - })), + .map((g) => { + const label = g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"); + return { + label, + value: g.totalTokens, + valueLabel: formatCount(g.totalTokens), + iconProvider: inferProviderIconKey(g.key ?? ""), + }; + }), [tokens.data?.groups, t], ); const toolCategoryData = useMemo( diff --git a/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx b/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx index 520862e3f0..eed82fbb00 100644 --- a/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx +++ b/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx @@ -44,6 +44,10 @@ vi.mock("../../NodesView", () => ({ ), })); +function providerIconIn(element: HTMLElement, provider: string): Element | null { + return element.querySelector(`.provider-icon[data-provider="${provider}"]`); +} + function tokenFixture(totalTokens = 1_500) { return { from: "2026-06-08", @@ -514,9 +518,15 @@ describe("CommandCenter shell", () => { const charts = screen.getByTestId("command-center-overview-charts"); expect(within(charts).getByText("Tokens by model")).toBeTruthy(); - expect(within(screen.getByTestId("command-center-overview-chart-tokens")).getByText("gpt-4o")).toBeTruthy(); + const overviewTokenChart = screen.getByTestId("command-center-overview-chart-tokens"); + expect(within(overviewTokenChart).getByText("gpt-4o")).toBeTruthy(); + expect(providerIconIn(overviewTokenChart, "openai")).toBeTruthy(); + expect(providerIconIn(overviewTokenChart, "anthropic")).toBeTruthy(); + expect(screen.getByRole("img", { name: "openai gpt-4o: 900" })).toBeTruthy(); expect(within(screen.getByTestId("command-center-overview-chart-tools")).getByText("read")).toBeTruthy(); expect(screen.getByTestId("cc-overview-pie")).toBeTruthy(); + expect(providerIconIn(screen.getByTestId("cc-overview-pie"), "openai")).toBeNull(); + expect(providerIconIn(screen.getByTestId("cc-overview-pie"), "anthropic")).toBeNull(); expect(screen.getByTestId("cc-overview-line")).toBeTruthy(); expectDailyActivityLineBeforeTrend(); expect(screen.getByRole("img", { name: "Token share by model" })).toBeTruthy(); @@ -525,6 +535,27 @@ describe("CommandCenter shell", () => { expectThroughputLastAfter("command-center-stat-tokens", "command-center-live-strip", "command-center-overview-charts"); }); + it("renders Overview providerless and unknown model icons without touching pie labels", async () => { + mockOverviewApi({ + tokens: { + ...tokenFixture(), + groups: [ + { ...tokenFixture().groups[0], key: "legacy-model", totalTokens: 50, nTasks: 1 }, + { ...tokenFixture().groups[1], key: null, totalTokens: 25, nTasks: 1 }, + ], + }, + }); + render(); + + const overviewTokenChart = await screen.findByTestId("command-center-overview-chart-tokens"); + expect(providerIconIn(overviewTokenChart, "legacy-model")).toBeTruthy(); + expect(providerIconIn(overviewTokenChart, "")).toBeTruthy(); + expect(screen.getByRole("img", { name: "legacy-model: 50" })).toBeTruthy(); + expect(within(overviewTokenChart).getByText("legacy-model").closest(".cc-bar-label")).toBeTruthy(); + expect(within(overviewTokenChart).getByText("(unknown)").closest(".cc-bar-label")).toBeTruthy(); + expect(providerIconIn(screen.getByTestId("cc-overview-pie"), "legacy-model")).toBeNull(); + }); + it("renders large comma-grouped token totals unchanged in Overview stat surfaces", async () => { mockOverviewApi({ tokens: tokenFixture(1_234_567_890) }); render(); diff --git a/packages/dashboard/app/components/command-center/areas/EcosystemArea.tsx b/packages/dashboard/app/components/command-center/areas/EcosystemArea.tsx index 59158b6e34..5d87acf9c5 100644 --- a/packages/dashboard/app/components/command-center/areas/EcosystemArea.tsx +++ b/packages/dashboard/app/components/command-center/areas/EcosystemArea.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { PluginActivationAnalytics, TokenAnalytics } from "@fusion/core"; import type { DateRange } from "../DateRangePicker"; +import { inferProviderIconKey } from "../../../utils/providerIconKey"; import { Bar } from "../charts/Bar"; import { LineChart, PieChart } from "../charts/recharts"; import { AreaShell } from "./AreaShell"; @@ -43,11 +44,15 @@ export function EcosystemArea({ range }: { range: DateRange }) { [...models] .sort((a, b) => b.nTasks - a.nTasks || (a.key ?? "").localeCompare(b.key ?? "")) .slice(0, 12) - .map((g) => ({ - label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"), - value: g.nTasks, - valueLabel: formatCount(g.nTasks), - })), + .map((g) => { + const label = g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"); + return { + label, + value: g.nTasks, + valueLabel: formatCount(g.nTasks), + iconProvider: inferProviderIconKey(g.key ?? ""), + }; + }), [models, t], ); /* diff --git a/packages/dashboard/app/components/command-center/areas/TokensArea.tsx b/packages/dashboard/app/components/command-center/areas/TokensArea.tsx index cfe723d521..ef2fbd3dab 100644 --- a/packages/dashboard/app/components/command-center/areas/TokensArea.tsx +++ b/packages/dashboard/app/components/command-center/areas/TokensArea.tsx @@ -11,6 +11,8 @@ import type { TokenTimeGranularity, } from "@fusion/core"; import type { DateRange } from "../DateRangePicker"; +import { ProviderIcon } from "../../ProviderIcon"; +import { inferProviderIconKey } from "../../../utils/providerIconKey"; import { Bar } from "../charts/Bar"; import { TokenSeriesChart } from "../charts/TokenSeriesChart"; import { LineChart as RechartsLineChart, PieChart } from "../charts/recharts"; @@ -36,6 +38,32 @@ function costSortValue(cost: CostResult): number { return cost.unavailable || cost.usd === null ? -1 : cost.usd; } +function modelGroupIdentity(group: TokenGroupSummary): string { + return group.key ?? "unknown"; +} + +function modelGroupDisplayLabel(group: TokenGroupSummary, unknownLabel: string): string { + return group.key ?? unknownLabel; +} + +function modelGroupIconProvider(group: TokenGroupSummary): string { + return inferProviderIconKey(group.key ?? ""); +} + +/* +FNXC:CommandCenterProviderIcons 2026-06-25-00:00: +Every textual Command Center model-name surface needs an adjacent ProviderIcon inferred from the model id, including null/unknown ids where ProviderIcon's CPU fallback is the intended safe state. +*/ +function ModelNameWithProviderIcon({ group, unknownLabel }: { group: TokenGroupSummary; unknownLabel: string }) { + const label = group.key ?? unknownLabel; + return ( + + + {label} + + ); +} + function sortGroups(groups: TokenGroupSummary[], key: SortKey, dir: 1 | -1): TokenGroupSummary[] { const sorted = [...groups]; sorted.sort((a, b) => { @@ -99,9 +127,10 @@ export function TokensArea({ range }: { range: DateRange }) { .sort((a, b) => b.totalTokens - a.totalTokens) .slice(0, 12) .map((g) => ({ - label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"), + label: modelGroupDisplayLabel(g, t("commandCenter.tokens.unknownModel", "(unknown)")), value: g.totalTokens, valueLabel: formatCount(g.totalTokens), + iconProvider: modelGroupIconProvider(g), })), [groups, t], ); @@ -112,7 +141,7 @@ export function TokensArea({ range }: { range: DateRange }) { .sort((a, b) => b.totalTokens - a.totalTokens) .slice(0, 12) .map((g) => ({ - label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"), + label: modelGroupDisplayLabel(g, t("commandCenter.tokens.unknownModel", "(unknown)")), value: g.totalTokens, })), [groups, t], @@ -244,8 +273,8 @@ export function TokensArea({ range }: { range: DateRange }) { {sortedGroups.map((g) => ( - - {g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)")} + + {formatCount(g.inputTokens)} {formatCount(g.outputTokens)} {formatCount(g.cachedTokens)} diff --git a/packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx b/packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx new file mode 100644 index 0000000000..737595200e --- /dev/null +++ b/packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx @@ -0,0 +1,105 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { render, screen, within } from "@testing-library/react"; +import { TokensArea } from "../TokensArea"; +import type { DateRange } from "../../DateRangePicker"; + +const apiMock = vi.fn(); + +vi.mock("../../../../api/legacy", () => ({ + api: (path: string, opts?: RequestInit) => apiMock(path, opts), +})); + +vi.mock("../../../ProviderIcon", () => ({ + ProviderIcon: ({ provider, size }: { provider: string; size?: "sm" | "md" | "lg" }) => ( + + ), +})); + +const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" }; + +function tokenFixture() { + return { + from: "2026-06-08", + to: null, + groupBy: "model", + totals: { + inputTokens: 1_350, + outputTokens: 675, + cachedTokens: 225, + cacheWriteTokens: 0, + totalTokens: 2_250, + nTasks: 5, + }, + cost: { usd: 12.5, unavailable: false, stale: false }, + series: [], + groups: [ + { + key: "claude-sonnet-4-5", + inputTokens: 600, + outputTokens: 300, + cachedTokens: 100, + cacheWriteTokens: 0, + totalTokens: 1_000, + nTasks: 2, + cost: { usd: 3.5, unavailable: false, stale: false }, + }, + { + key: "gpt-4o-mini", + inputTokens: 500, + outputTokens: 250, + cachedTokens: 100, + cacheWriteTokens: 0, + totalTokens: 850, + nTasks: 2, + cost: { usd: 7, unavailable: false, stale: false }, + }, + { + key: null, + inputTokens: 250, + outputTokens: 125, + cachedTokens: 25, + cacheWriteTokens: 0, + totalTokens: 400, + nTasks: 1, + cost: { usd: null, unavailable: true, stale: false }, + }, + ], + }; +} + +beforeEach(() => { + apiMock.mockReset(); + apiMock.mockResolvedValue(tokenFixture()); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("TokensArea provider model icons", () => { + it("renders inferred provider icons in the per-model table and Tokens by model bars", async () => { + render(); + + const table = await screen.findByTestId("cc-tokens-table"); + const claudeRow = screen.getByTestId("cc-tokens-row-claude-sonnet-4-5"); + const gptRow = screen.getByTestId("cc-tokens-row-gpt-4o-mini"); + const unknownRow = screen.getByTestId("cc-tokens-row-unknown"); + + expect(within(claudeRow).getByText("claude-sonnet-4-5")).toBeTruthy(); + expect(within(claudeRow).getByTestId("provider-icon-anthropic")).toBeTruthy(); + expect(within(gptRow).getByText("gpt-4o-mini")).toBeTruthy(); + expect(within(gptRow).getByTestId("provider-icon-openai")).toBeTruthy(); + expect(within(unknownRow).getByText("(unknown)")).toBeTruthy(); + expect(within(unknownRow).getByTestId("provider-icon-")).toBeTruthy(); + + const byModelChart = screen.getByRole("list", { name: "Tokens by model" }); + const claudeBarLabel = within(byModelChart).getByText("claude-sonnet-4-5").closest(".cc-bar-label"); + const gptBarLabel = within(byModelChart).getByText("gpt-4o-mini").closest(".cc-bar-label"); + const unknownBarLabel = within(byModelChart).getByText("(unknown)").closest(".cc-bar-label"); + + expect(claudeBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-anthropic"); + expect(gptBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-openai"); + expect(unknownBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-"); + expect(table.querySelectorAll(".provider-icon").length).toBeGreaterThanOrEqual(3); + }); +}); diff --git a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx index a4328fcf03..a5c8fa5490 100644 --- a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx +++ b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx @@ -63,6 +63,10 @@ import { defaultPresets, rangeFromPreset, type DateRange } from "../../DateRange const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" }; const customRange = (from: string, to: string): DateRange => ({ from, to, preset: "custom" }); +function providerIconIn(element: HTMLElement, provider: string): Element | null { + return element.querySelector(`.provider-icon[data-provider="${provider}"]`); +} + function tokenFixture(totalTokens = 1500) { return { from: "2026-06-08", @@ -760,8 +764,36 @@ describe("TokensArea", () => { expect(screen.getByRole("img", { name: "Tokens trend" })).toBeTruthy(); expect(screen.getByRole("img", { name: "Token share by model" })).toBeTruthy(); expect(screen.getByLabelText("2026-06-09: 900")).toBeTruthy(); - expect(screen.getByTestId("cc-tokens-row-gpt-4o")).toBeTruthy(); - expect(screen.getByTestId("cc-tokens-row-claude-sonnet")).toBeTruthy(); + const tokensBar = screen.getByRole("list", { name: "Tokens by model" }) as HTMLElement; + expect(providerIconIn(tokensBar, "openai")).toBeTruthy(); + expect(providerIconIn(tokensBar, "anthropic")).toBeTruthy(); + expect(screen.getByRole("img", { name: "openai gpt-4o: 900" })).toBeTruthy(); + expect(providerIconIn(screen.getByTestId("cc-tokens-pie"), "openai")).toBeNull(); + expect(providerIconIn(screen.getByTestId("cc-tokens-pie"), "anthropic")).toBeNull(); + expect(providerIconIn(screen.getByTestId("cc-tokens-row-gpt-4o"), "openai")).toBeTruthy(); + expect(providerIconIn(screen.getByTestId("cc-tokens-row-claude-sonnet"), "anthropic")).toBeTruthy(); + }); + + it("renders providerless and unknown model rows correctly", async () => { + apiMock.mockResolvedValue({ + ...tokenFixture(), + totals: { ...tokenFixture().totals, totalTokens: 75, nTasks: 2 }, + groups: [ + { ...tokenFixture().groups[0], key: "legacy-model", totalTokens: 50, nTasks: 1 }, + { ...tokenFixture().groups[1], key: null, totalTokens: 25, nTasks: 1 }, + ], + }); + render(); + + await screen.findByTestId("cc-area-tokens"); + expect(screen.getByRole("img", { name: "legacy-model: 50" })).toBeTruthy(); + + const legacyRow = screen.getByTestId("cc-tokens-row-legacy-model"); + const unknownRow = screen.getByTestId("cc-tokens-row-unknown"); + expect(legacyRow.textContent).toContain("legacy-model"); + expect(unknownRow.textContent).toContain("(unknown)"); + expect(providerIconIn(legacyRow, "legacy-model")).toBeTruthy(); + expect(providerIconIn(unknownRow, "")).toBeTruthy(); }); it("renders a large comma-grouped total unchanged in the total tokens card", async () => { @@ -1623,6 +1655,12 @@ describe("EcosystemArea", () => { expect(screen.getByTestId("cc-ecosystem-pie")).toBeTruthy(); expect(screen.getByTestId("cc-ecosystem-line")).toBeTruthy(); expect(screen.getByRole("img", { name: "Task share by model" })).toBeTruthy(); + const ecosystemBar = screen.getByRole("list", { name: "Tasks per model" }) as HTMLElement; + expect(providerIconIn(ecosystemBar, "openai")).toBeTruthy(); + expect(providerIconIn(ecosystemBar, "anthropic")).toBeTruthy(); + expect(providerIconIn(screen.getByTestId("cc-ecosystem-pie"), "openai")).toBeNull(); + expect(providerIconIn(screen.getByTestId("cc-ecosystem-pie"), "anthropic")).toBeNull(); + expect(screen.getByRole("img", { name: "openai gpt-4o: 3" })).toBeTruthy(); expect(screen.getByRole("img", { name: "Ecosystem trend" })).toBeTruthy(); expect(screen.getByTestId("cc-ecosystem-plugins-unavailable").textContent).toBe("—"); expect(screen.getByTestId("cc-area-ecosystem").textContent).not.toContain("NaN"); diff --git a/packages/dashboard/app/components/command-center/charts/Bar.tsx b/packages/dashboard/app/components/command-center/charts/Bar.tsx index b0f4df840d..6cb0d50a20 100644 --- a/packages/dashboard/app/components/command-center/charts/Bar.tsx +++ b/packages/dashboard/app/components/command-center/charts/Bar.tsx @@ -1,10 +1,21 @@ +import { ProviderIcon } from "../../ProviderIcon"; import "./charts.css"; +/* + * FNXC:CommandCenterModelLabels 2026-06-25-00:00: + * Command Center model-heavy bar analytics must render provider marks immediately before model names. Keep provider icons opt-in on BarDatum so non-model chart rows (tool categories, activity series, plugin labels) continue rendering as plain strings with no provider icon shell. + */ +function hasIconProvider(datum: BarDatum): boolean { + return Object.prototype.hasOwnProperty.call(datum, "iconProvider"); +} + export interface BarDatum { label: string; value: number; /** Optional display string for the value (defaults to the number). */ valueLabel?: string; + /** Optional ProviderIcon provider key for model-name rows; omitted for non-model charts. */ + iconProvider?: string; } export interface BarProps { @@ -35,18 +46,24 @@ export function Bar({ data, max, ariaLabel }: BarProps) { return (
    - {data.map((d) => { + {data.map((d, index) => { const width = safeWidthPercent(d.value, computedMax); const valueText = d.valueLabel ?? String(Number.isFinite(d.value) ? d.value : 0); + const labelText = d.label; + const iconLabelText = d.iconProvider && d.iconProvider !== labelText ? `${d.iconProvider} ${labelText}` : labelText; + const accessibleLabel = hasIconProvider(d) ? iconLabelText : labelText; return ( -
  • - {d.label} +
  • + + {hasIconProvider(d) ? : null} + {d.label} +
    {valueText} diff --git a/packages/dashboard/app/components/command-center/charts/charts.css b/packages/dashboard/app/components/command-center/charts/charts.css index 502221974c..ece2cece71 100644 --- a/packages/dashboard/app/components/command-center/charts/charts.css +++ b/packages/dashboard/app/components/command-center/charts/charts.css @@ -57,6 +57,9 @@ FN-6680 re-checked FN-6664 in a real Blink layout engine because jsdom does not .cc-bar-label { min-inline-size: 0; + display: inline-flex; + align-items: center; + gap: var(--space-xs); font-size: 0.8125rem; color: var(--text-muted); overflow: hidden; @@ -64,6 +67,37 @@ FN-6680 re-checked FN-6664 in a real Blink layout engine because jsdom does not white-space: nowrap; } +.cc-bar-label > .provider-icon { + flex: 0 0 auto; +} + +.cc-bar-label-text { + min-inline-size: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cc-model-label { + min-inline-size: 0; + max-inline-size: 100%; + display: inline-flex; + align-items: center; + gap: var(--space-xs); + vertical-align: middle; +} + +.cc-model-label .provider-icon { + flex: 0 0 auto; +} + +.cc-model-label-text { + min-inline-size: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .cc-bar-track { min-inline-size: 0; position: relative; @@ -473,6 +507,10 @@ On narrow mobile, Recharts' default legend measured to 0px wide and positioned i .cc-recharts-empty { block-size: calc(var(--space-2xl) * 8 + var(--space-md)); } + + .cc-model-label { + max-inline-size: 100%; + } } /* ---- RadialGauge ---- */ @@ -679,6 +717,10 @@ On narrow mobile, Recharts' default legend measured to 0px wide and positioned i align-items: start; } + .cc-bar-label { + max-inline-size: 100%; + } + .cc-bar-value { grid-column: 1 / -1; justify-self: end; diff --git a/packages/dashboard/app/utils/__tests__/providerIconKey.test.ts b/packages/dashboard/app/utils/__tests__/providerIconKey.test.ts new file mode 100644 index 0000000000..f39a357f33 --- /dev/null +++ b/packages/dashboard/app/utils/__tests__/providerIconKey.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { inferProviderIconKey } from "../providerIconKey"; + +describe("inferProviderIconKey", () => { + it.each([ + ["claude-sonnet-4-5", "anthropic"], + ["Anthropic Claude", "anthropic"], + ["gpt-4o", "openai"], + ["openai-codex", "openai"], + ["gemini-2.5-pro", "google"], + ["google-antigravity", "google"], + ["ollama/llama3", "ollama"], + ["minimax-text-01", "minimax"], + ["zhipu-glm-4", "zai"], + ["zai/glm-4.5", "zai"], + ["kimi-k2", "kimi"], + ["moonshot-v1", "kimi"], + ["amazon-bedrock-claude", "anthropic"], + ["bedrock-titan", "bedrock"], + ["xai-grok-4", "xai"], + ["opencode", "opencode"], + ["github copilot", "github-copilot"], + ["copilot-gpt-4", "openai"], + ])("maps %s to %s", (input, expected) => { + expect(inferProviderIconKey(input)).toBe(expected); + }); + + it("returns unknown ids unchanged so ProviderIcon can render its fallback", () => { + expect(inferProviderIconKey("custom-model-v1")).toBe("custom-model-v1"); + expect(inferProviderIconKey("")).toBe(""); + }); +}); diff --git a/packages/dashboard/app/utils/providerIconKey.ts b/packages/dashboard/app/utils/providerIconKey.ts new file mode 100644 index 0000000000..79d8d2a7ea --- /dev/null +++ b/packages/dashboard/app/utils/providerIconKey.ts @@ -0,0 +1,45 @@ +/* +FNXC:ProviderIcons 2026-06-25-00:00: +Command Center model analytics group rows by model id only, so dashboard model-name surfaces must infer the provider icon from the model/provider text before handing off to ProviderIcon's fallback-safe renderer. +*/ +export function inferProviderIconKey(modelOrProviderName: string): string { + const normalized = modelOrProviderName.toLowerCase(); + + // Map common provider/model names to their icon keys. + if (normalized.includes("claude") || normalized.includes("anthropic")) { + return "anthropic"; + } + if (normalized.includes("codex") || normalized.includes("openai") || normalized.includes("gpt")) { + return "openai"; + } + if (normalized.includes("gemini") || normalized.includes("google") || normalized.includes("antigravity")) { + return "google"; + } + if (normalized.includes("ollama")) { + return "ollama"; + } + if (normalized.includes("minimax")) { + return "minimax"; + } + if (normalized.includes("zai") || normalized.includes("zhipu")) { + return "zai"; + } + if (normalized.includes("kimi") || normalized.includes("moonshot")) { + return "kimi"; + } + if (normalized.includes("bedrock") || normalized.includes("amazon")) { + return "bedrock"; + } + if (normalized.includes("xai") || normalized.includes("grok")) { + return "xai"; + } + if (normalized.includes("opencode")) { + return "opencode"; + } + if (normalized.includes("copilot") || normalized === "github copilot") { + return "github-copilot"; + } + + // Return the original name as fallback (ProviderIcon will show a default icon). + return modelOrProviderName; +}