FN-8500: add Xiaomi provider icons
Show Xiaomi branding for direct provider names and MiMo model labels. - Add an accessible, tokenized Xiaomi provider mark - Infer boundary-safe MiMo labels through the shared provider icon key - Reuse shared inference in usage cards and cover Xiaomi mappings - Add a patch changeset for Xiaomi provider branding Files changed: .changeset/fn-8500-xiaomi-provider-icon.md | 7 +++ packages/dashboard/app/components/ProviderIcon.tsx | 24 +++++++++ .../dashboard/app/components/UsageIndicator.tsx | 58 +++------------------- .../app/components/__tests__/ProviderIcon.test.tsx | 48 ++++++++++++++++++ .../components/__tests__/UsageIndicator.test.tsx | 17 +++++++ packages/dashboard/app/styles.css | 1 + .../app/utils/__tests__/providerIconKey.test.ts | 9 ++++ packages/dashboard/app/utils/providerIconKey.ts | 8 +++ 8 files changed, 120 insertions(+), 52 deletions(-) Fusion-Task-Id: FN-8500 Fusion-Task-Lineage: 1784a180-cf41-4666-a59e-10aa24f47b72 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8500-xiaomi-provider-icon.md
Normal file
7
.changeset/fn-8500-xiaomi-provider-icon.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Show Xiaomi branding for Xiaomi and MiMo provider labels.
|
||||||
|
category: feature
|
||||||
|
dev: Adds a tokenized shared ProviderIcon mark and boundary-safe MiMo inference.
|
||||||
@@ -258,6 +258,29 @@ function OpencodeIcon({ size, color, label = "Opencode" }: { size: number; color
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ProviderIcons 2026-07-22-17:04:
|
||||||
|
FN-8500 requires Hermes-supported Xiaomi and confirmed MiMo labels to share one accessible brand mark across dashboard surfaces. This inline path is Xiaomi's Simple Icons mark (CC0 1.0); keep its color supplied by the provider token rather than hardcoding presentation in TSX.
|
||||||
|
*/
|
||||||
|
function XiaomiIcon({ size, color, label = "Xiaomi" }: { size: number; color: string; label?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
data-testid="xiaomi-icon"
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 0C8.016 0 4.756.255 2.493 2.516.23 4.776 0 8.033 0 12.012c0 3.98.23 7.235 2.494 9.497C4.757 23.77 8.017 24 12 24c3.983 0 7.243-.23 9.506-2.491C23.77 19.247 24 15.99 24 12.012c0-3.984-.233-7.243-2.502-9.504C19.234.252 15.978 0 12 0zM4.906 7.405h5.624c1.47 0 3.007.068 3.764.827.746.746.827 2.233.83 3.676v4.54a.15.15 0 0 1-.152.147h-1.947a.15.15 0 0 1-.152-.148V11.83c-.002-.806-.048-1.634-.464-2.051-.358-.36-1.026-.441-1.72-.458H7.158a.15.15 0 0 0-.151.147v6.98a.15.15 0 0 1-.152.148H4.906a.15.15 0 0 1-.15-.148V7.554a.15.15 0 0 1 .15-.149zm12.131 0h1.949a.15.15 0 0 1 .15.15v8.892a.15.15 0 0 1-.15.148h-1.949a.15.15 0 0 1-.151-.148V7.554a.15.15 0 0 1 .151-.149zM8.92 10.948h2.046c.083 0 .15.066.15.147v5.352a.15.15 0 0 1-.15.148H8.92a.15.15 0 0 1-.152-.148v-5.352a.15.15 0 0 1 .152-.147Z"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DeepSeekIcon({ size, color, label = "DeepSeek" }: { size: number; color: string; label?: string }) {
|
function DeepSeekIcon({ size, color, label = "DeepSeek" }: { size: number; color: string; label?: string }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -841,6 +864,7 @@ const providerConfig: Record<
|
|||||||
minimax: { component: MiniMaxIcon, color: "var(--provider-minimax)" },
|
minimax: { component: MiniMaxIcon, color: "var(--provider-minimax)" },
|
||||||
"minimax-cn": { component: MiniMaxIcon, color: "var(--provider-minimax)", label: "MiniMax (CN)" },
|
"minimax-cn": { component: MiniMaxIcon, color: "var(--provider-minimax)", label: "MiniMax (CN)" },
|
||||||
zai: { component: ZaiIcon, color: "var(--provider-zai)" },
|
zai: { component: ZaiIcon, color: "var(--provider-zai)" },
|
||||||
|
xiaomi: { component: XiaomiIcon, color: "var(--provider-xiaomi)" },
|
||||||
|
|
||||||
kimi: { component: KimiIcon, color: "var(--provider-kimi)" },
|
kimi: { component: KimiIcon, color: "var(--provider-kimi)" },
|
||||||
moonshot: { component: KimiIcon, color: "var(--provider-kimi)" }, // Moonshot alias
|
moonshot: { component: KimiIcon, color: "var(--provider-kimi)" }, // Moonshot alias
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { X, RefreshCw, Activity, TrendingUp, CheckCircle, AlertTriangle, Eye, Gr
|
|||||||
import type { ProviderUsage, UsageWindow } from "../api";
|
import type { ProviderUsage, UsageWindow } from "../api";
|
||||||
import { useUsageData } from "../hooks/useUsageData";
|
import { useUsageData } from "../hooks/useUsageData";
|
||||||
import { ProviderIcon } from "./ProviderIcon";
|
import { ProviderIcon } from "./ProviderIcon";
|
||||||
|
import { inferProviderIconKey } from "../utils/providerIconKey";
|
||||||
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||||
import "./UsageIndicator.css";
|
import "./UsageIndicator.css";
|
||||||
|
|
||||||
@@ -408,57 +409,10 @@ interface ProviderCardProps {
|
|||||||
onMoveDown: () => void;
|
onMoveDown: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Map provider names to ProviderIcon provider keys
|
FNXC:UsageIndicator 2026-07-22-17:04:
|
||||||
*/
|
FN-8500 makes usage cards consume the canonical provider/model inference seam rather than duplicate it. This keeps Xiaomi MiMo labels and every existing shared mapping aligned with model, task, and analytics surfaces.
|
||||||
function getProviderIconKey(providerName: string): string {
|
*/
|
||||||
const normalized = providerName.toLowerCase();
|
|
||||||
|
|
||||||
// Map common provider 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';
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
FNXC:UsageIndicator 2026-07-10-00:00:
|
|
||||||
Cursor usage cards are rendered by the generic ProviderCard path, so provider-name mapping is the only frontend-specific requirement: route "Cursor" to the existing cursor-cli icon token and SVG.
|
|
||||||
*/
|
|
||||||
if (normalized.includes('cursor')) {
|
|
||||||
return 'cursor-cli';
|
|
||||||
}
|
|
||||||
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 providerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider card showing status and usage windows
|
* Provider card showing status and usage windows
|
||||||
@@ -522,7 +476,7 @@ function ProviderCard({
|
|||||||
>
|
>
|
||||||
<div className="usage-provider-header">
|
<div className="usage-provider-header">
|
||||||
<div className="usage-provider-info">
|
<div className="usage-provider-info">
|
||||||
<ProviderIcon provider={getProviderIconKey(provider.name)} size="md" />
|
<ProviderIcon provider={inferProviderIconKey(provider.name)} size="md" />
|
||||||
<span className="usage-provider-name">{provider.name}</span>
|
<span className="usage-provider-name">{provider.name}</span>
|
||||||
{hiddenCount > 0 && (
|
{hiddenCount > 0 && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const SYNTHETIC_FIRST_CLASS_PROVIDER_IDS = [
|
|||||||
"paperclip-ai",
|
"paperclip-ai",
|
||||||
"omp",
|
"omp",
|
||||||
"oh-my-pi",
|
"oh-my-pi",
|
||||||
|
"xiaomi",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const FIRST_CLASS_PROVIDER_IDS = [...new Set([
|
const FIRST_CLASS_PROVIDER_IDS = [...new Set([
|
||||||
@@ -423,6 +424,37 @@ describe("ProviderIcon", () => {
|
|||||||
expect(wrapper).toHaveAttribute("data-provider", "minimax");
|
expect(wrapper).toHaveAttribute("data-provider", "minimax");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["xiaomi", "xiaomi"],
|
||||||
|
["XIAOMI", "xiaomi"],
|
||||||
|
])("renders an accessible tokenized Xiaomi mark for %s", (provider, expectedDataProvider) => {
|
||||||
|
render(<ProviderIcon provider={provider} size="md" />);
|
||||||
|
const svg = screen.getByTestId("xiaomi-icon");
|
||||||
|
const wrapper = svg.parentElement;
|
||||||
|
|
||||||
|
expect(svg).toHaveAccessibleName("Xiaomi");
|
||||||
|
expect(isNonCpuMark(svg)).toBe(true);
|
||||||
|
expect(wrapper).toHaveAttribute("data-provider", expectedDataProvider);
|
||||||
|
expect(wrapper).toHaveStyle({ color: "var(--provider-xiaomi)" });
|
||||||
|
expect(svg.querySelector("path")).toHaveAttribute("fill", "var(--provider-xiaomi)");
|
||||||
|
expect(svg).toHaveAttribute("width", "20");
|
||||||
|
expect(svg).toHaveAttribute("height", "20");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders independent Xiaomi SVGs for duplicate provider entries", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<>
|
||||||
|
<ProviderIcon provider="xiaomi" size="sm" />
|
||||||
|
<ProviderIcon provider="xiaomi" size="lg" />
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
const icons = container.querySelectorAll("[data-testid='xiaomi-icon']");
|
||||||
|
|
||||||
|
expect(icons).toHaveLength(2);
|
||||||
|
expect(Array.from(icons).map((icon) => icon.getAttribute("width"))).toEqual(["16", "24"]);
|
||||||
|
expect(Array.from(icons).every((icon) => isNonCpuMark(icon as SVGElement))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders Z.ai brand icon for zai provider", () => {
|
it("renders Z.ai brand icon for zai provider", () => {
|
||||||
render(<ProviderIcon provider="zai" />);
|
render(<ProviderIcon provider="zai" />);
|
||||||
expect(screen.getByTestId("zai-icon")).toBeInTheDocument();
|
expect(screen.getByTestId("zai-icon")).toBeInTheDocument();
|
||||||
@@ -561,6 +593,22 @@ describe("ProviderIcon", () => {
|
|||||||
expect(icon).toHaveAttribute("data-provider", expectedDataProvider);
|
expect(icon).toHaveAttribute("data-provider", expectedDataProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(["xiaomi/MiMo-V2-Flash", "MiMo-V2-Flash"])('infers Xiaomi icon from the boundary-safe MiMo model label %s', (provider) => {
|
||||||
|
render(<ProviderIcon provider={provider} />);
|
||||||
|
const svg = screen.getByTestId("xiaomi-icon");
|
||||||
|
|
||||||
|
expect(svg).toHaveAccessibleName("Xiaomi");
|
||||||
|
expect(isNonCpuMark(svg)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the CPU fallback for Xiaomi near-match custom names", () => {
|
||||||
|
const { container } = render(<ProviderIcon provider="mimosa-v1" />);
|
||||||
|
const svg = container.querySelector<SVGElement>('[data-provider="mimosa-v1"] svg');
|
||||||
|
|
||||||
|
expect(svg).toBeTruthy();
|
||||||
|
expect(hasLucideCpuSignature(svg as SVGElement)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("infers non-Grok provider icons from model-shaped ids", () => {
|
it("infers non-Grok provider icons from model-shaped ids", () => {
|
||||||
render(<ProviderIcon provider="gpt-5.5" />);
|
render(<ProviderIcon provider="gpt-5.5" />);
|
||||||
const icon = screen.getByTestId("openai-icon").parentElement;
|
const icon = screen.getByTestId("openai-icon").parentElement;
|
||||||
|
|||||||
@@ -1536,6 +1536,23 @@ describe("UsageIndicator", () => {
|
|||||||
expect(screen.getByText("Moonshot")).toBeInTheDocument();
|
expect(screen.getByText("Moonshot")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps Xiaomi MiMo usage labels through the shared provider inference", () => {
|
||||||
|
mockUseUsageData.mockReturnValue(createUsageDataState({
|
||||||
|
providers: [
|
||||||
|
{ name: "xiaomi/MiMo-V2-Flash", icon: "🟠", status: "ok", windows: [] },
|
||||||
|
],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
refresh: mockRefresh,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UsageIndicator isOpen={true} onClose={mockOnClose} projectId={TEST_PROJECT_ID} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("xiaomi-icon")).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('[data-provider="xiaomi"]')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("maps Cursor provider to the cursor-cli icon and renders usage windows", () => {
|
it("maps Cursor provider to the cursor-cli icon and renders usage windows", () => {
|
||||||
mockUseUsageData.mockReturnValue(createUsageDataState({
|
mockUseUsageData.mockReturnValue(createUsageDataState({
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -380,6 +380,7 @@ svg.spinning {
|
|||||||
--provider-tavily: #4f46e5;
|
--provider-tavily: #4f46e5;
|
||||||
--provider-minimax: #e73562;
|
--provider-minimax: #e73562;
|
||||||
--provider-zai: #1a6dff;
|
--provider-zai: #1a6dff;
|
||||||
|
--provider-xiaomi: #ff6900;
|
||||||
--provider-kimi: #6c5ce7;
|
--provider-kimi: #6c5ce7;
|
||||||
--provider-bedrock: #ff9900;
|
--provider-bedrock: #ff9900;
|
||||||
--provider-opencode: #38bdf8;
|
--provider-opencode: #38bdf8;
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ describe("inferProviderIconKey", () => {
|
|||||||
["omp-cli/MiniMax-M2.5", "omp-cli"],
|
["omp-cli/MiniMax-M2.5", "omp-cli"],
|
||||||
["omp/claude-sonnet-4", "omp-cli"],
|
["omp/claude-sonnet-4", "omp-cli"],
|
||||||
["Oh My Pi", "omp-cli"],
|
["Oh My Pi", "omp-cli"],
|
||||||
|
["xiaomi", "xiaomi"],
|
||||||
|
["Xiaomi", "xiaomi"],
|
||||||
|
["xiaomi/MiMo-V2-Flash", "xiaomi"],
|
||||||
|
["MiMo-V2-Flash", "xiaomi"],
|
||||||
["ollama/llama3", "ollama"],
|
["ollama/llama3", "ollama"],
|
||||||
["minimax-text-01", "minimax"],
|
["minimax-text-01", "minimax"],
|
||||||
["zhipu-glm-4", "zai"],
|
["zhipu-glm-4", "zai"],
|
||||||
@@ -40,6 +44,11 @@ describe("inferProviderIconKey", () => {
|
|||||||
expect(inferProviderIconKey("claude-sonnet-4")).not.toBe("omp-cli");
|
expect(inferProviderIconKey("claude-sonnet-4")).not.toBe("omp-cli");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not infer Xiaomi from near-match custom names", () => {
|
||||||
|
expect(inferProviderIconKey("mimosa-v1")).toBe("mimosa-v1");
|
||||||
|
expect(inferProviderIconKey("custom-mimoized-model")).toBe("custom-mimoized-model");
|
||||||
|
});
|
||||||
|
|
||||||
it("returns unknown ids unchanged so ProviderIcon can render its fallback", () => {
|
it("returns unknown ids unchanged so ProviderIcon can render its fallback", () => {
|
||||||
expect(inferProviderIconKey("custom-model-v1")).toBe("custom-model-v1");
|
expect(inferProviderIconKey("custom-model-v1")).toBe("custom-model-v1");
|
||||||
expect(inferProviderIconKey("not-a-glmish-model")).toBe("not-a-glmish-model");
|
expect(inferProviderIconKey("not-a-glmish-model")).toBe("not-a-glmish-model");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Standalone GLM model ids such as glm-5.1, glm-4.5-air, and glm-5v-turbo are Z.ai
|
|||||||
const GLM_MODEL_SEGMENT_PATTERN = /(?:^|[/:_-])glm(?:$|[\d._:-]|-[\da-z])/;
|
const GLM_MODEL_SEGMENT_PATTERN = /(?:^|[/:_-])glm(?:$|[\d._:-]|-[\da-z])/;
|
||||||
const OMP_PROVIDER_SEGMENT_PATTERN = /(?:^|[/:_\s-])omp(?:$|[/:_\s-])/;
|
const OMP_PROVIDER_SEGMENT_PATTERN = /(?:^|[/:_\s-])omp(?:$|[/:_\s-])/;
|
||||||
const OMP_PROVIDER_NAME_PATTERN = /oh[-\s]my[-\s]pi/;
|
const OMP_PROVIDER_NAME_PATTERN = /oh[-\s]my[-\s]pi/;
|
||||||
|
const XIAOMI_MIMO_MODEL_SEGMENT_PATTERN = /(?:^|[/:_-])mimo(?:$|[/:_.-])/;
|
||||||
|
|
||||||
export function inferProviderIconKey(modelOrProviderName: string): string {
|
export function inferProviderIconKey(modelOrProviderName: string): string {
|
||||||
const normalized = modelOrProviderName.toLowerCase();
|
const normalized = modelOrProviderName.toLowerCase();
|
||||||
@@ -28,6 +29,13 @@ export function inferProviderIconKey(modelOrProviderName: string): string {
|
|||||||
if (OMP_PROVIDER_SEGMENT_PATTERN.test(normalized) || OMP_PROVIDER_NAME_PATTERN.test(normalized)) {
|
if (OMP_PROVIDER_SEGMENT_PATTERN.test(normalized) || OMP_PROVIDER_NAME_PATTERN.test(normalized)) {
|
||||||
return "omp-cli";
|
return "omp-cli";
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
FNXC:ProviderIcons 2026-07-22-17:04:
|
||||||
|
FN-8500 requires the Hermes Xiaomi provider and Xiaomi MiMo model IDs to resolve through the shared icon path. Restrict MiMo matching to model-id segment boundaries so unrelated custom names such as "mimosa" retain the fallback.
|
||||||
|
*/
|
||||||
|
if (normalized === "xiaomi" || XIAOMI_MIMO_MODEL_SEGMENT_PATTERN.test(normalized)) {
|
||||||
|
return "xiaomi";
|
||||||
|
}
|
||||||
if (normalized.includes("claude") || normalized.includes("anthropic")) {
|
if (normalized.includes("claude") || normalized.includes("anthropic")) {
|
||||||
return "anthropic";
|
return "anthropic";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user