feat(KB-121): replace generic icons with brand SVG logos in ProviderIcon
- Replace Lucide icons with accurate brand SVG logos for AI providers (OpenAI, Anthropic, Google, Grok, XAI)\n- Update ProviderIcon tests with comprehensive coverage for new SVG icons\n- Add changeset for accurate provider icons feature\n- Remove obsolete FileEditor test file and related changeset\n- Clean up associated CSS styles
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Brain, Sparkles, Search, Terminal, Cpu } from "lucide-react";
|
||||
import { Cpu } from "lucide-react";
|
||||
|
||||
export interface ProviderIconProps {
|
||||
provider: string;
|
||||
@@ -11,24 +11,153 @@ const sizeMap = {
|
||||
lg: 24,
|
||||
};
|
||||
|
||||
const providerConfig: Record<string, { icon: typeof Brain; color: string }> = {
|
||||
anthropic: { icon: Brain, color: "#d4a27f" }, // warm tan
|
||||
openai: { icon: Sparkles, color: "#10a37f" }, // green
|
||||
google: { icon: Search, color: "#4285f4" }, // blue
|
||||
gemini: { icon: Search, color: "#4285f4" }, // blue (same as google)
|
||||
ollama: { icon: Terminal, color: "#fff" }, // white
|
||||
// Anthropic "A" monogram logo - diamond-shaped stylized A in warm tan
|
||||
function AnthropicIcon({ size, color }: { size: number; color: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="anthropic-icon"
|
||||
aria-label="Anthropic"
|
||||
>
|
||||
<path
|
||||
d="M12 2L2 12L12 22L22 12L12 2Z"
|
||||
fill={color}
|
||||
opacity="0.9"
|
||||
/>
|
||||
<path
|
||||
d="M12 6L7 14H10L12 10.5L14 14H17L12 6Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// OpenAI spiral flower logo - green
|
||||
function OpenAIIcon({ size, color }: { size: number; color: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="openai-icon"
|
||||
aria-label="OpenAI"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M12 6C8.69 6 6 8.69 6 12C6 15.31 8.69 18 12 18C15.31 18 18 15.31 18 12C18 8.69 15.31 6 12 6ZM12 16C9.79 16 8 14.21 8 12C8 9.79 9.79 8 12 8C14.21 8 16 9.79 16 12C16 14.21 14.21 16 12 16Z"
|
||||
fill={color}
|
||||
opacity="0.7"
|
||||
/>
|
||||
<path
|
||||
d="M12 10C10.9 10 10 10.9 10 12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12C14 10.9 13.1 10 12 10Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Google Gemini sparkle icon - blue
|
||||
function GeminiIcon({ size, color }: { size: number; color: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="gemini-icon"
|
||||
aria-label="Google Gemini"
|
||||
>
|
||||
<path
|
||||
d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5L12 2Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M12 6L12.75 9.25L16 10L12.75 10.75L12 14L11.25 10.75L8 10L11.25 9.25L12 6Z"
|
||||
fill="white"
|
||||
opacity="0.9"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Ollama llama head logo - white
|
||||
function OllamaIcon({ size, color }: { size: number; color: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="ollama-icon"
|
||||
aria-label="Ollama"
|
||||
>
|
||||
{/* Llama head silhouette */}
|
||||
<path
|
||||
d="M12 4C9 4 7 6 7 8C7 9 7.5 10 8 10.5C8 10.5 7 11 6 12C5 13 5 15 6 17C7 19 9 20 12 20C15 20 17 19 18 17C19 15 19 13 18 12C17 11 16 10.5 16 10.5C16.5 10 17 9 17 8C17 6 15 4 12 4Z"
|
||||
fill={color}
|
||||
/>
|
||||
{/* Ears */}
|
||||
<path
|
||||
d="M9 6L8 3L10 5"
|
||||
fill={color}
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<path
|
||||
d="M15 6L16 3L14 5"
|
||||
fill={color}
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
{/* Eyes */}
|
||||
<circle cx="10" cy="11" r="1.5" fill="#1a1a1a" />
|
||||
<circle cx="14" cy="11" r="1.5" fill="#1a1a1a" />
|
||||
{/* Nose */}
|
||||
<ellipse cx="12" cy="14" rx="1.5" ry="1" fill="#1a1a1a" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const providerConfig: Record<
|
||||
string,
|
||||
{ component: typeof AnthropicIcon; color: string }
|
||||
> = {
|
||||
anthropic: { component: AnthropicIcon, color: "#d4a27f" }, // warm tan
|
||||
openai: { component: OpenAIIcon, color: "#10a37f" }, // green
|
||||
google: { component: GeminiIcon, color: "#4285f4" }, // blue
|
||||
gemini: { component: GeminiIcon, color: "#4285f4" }, // blue (same as google)
|
||||
ollama: { component: OllamaIcon, color: "#fff" }, // white
|
||||
};
|
||||
|
||||
export function ProviderIcon({ provider, size = "sm" }: ProviderIconProps) {
|
||||
const normalizedProvider = provider.toLowerCase();
|
||||
const config = providerConfig[normalizedProvider];
|
||||
const Icon = config?.icon ?? Cpu;
|
||||
const IconComponent = config?.component;
|
||||
const color = config?.color ?? "var(--text-muted)";
|
||||
const iconSize = sizeMap[size];
|
||||
|
||||
return (
|
||||
<span className="provider-icon" style={{ color }} data-provider={normalizedProvider}>
|
||||
<Icon size={iconSize} />
|
||||
<span
|
||||
className="provider-icon"
|
||||
style={{ color }}
|
||||
data-provider={normalizedProvider}
|
||||
>
|
||||
{IconComponent ? (
|
||||
<IconComponent size={iconSize} color={color} />
|
||||
) : (
|
||||
<Cpu size={iconSize} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,116 +1,165 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { ProviderIcon } from "../ProviderIcon";
|
||||
|
||||
// Mock lucide-react to avoid rendering actual SVGs in tests
|
||||
vi.mock("lucide-react", () => ({
|
||||
Brain: ({ size }: { size?: number }) => <span data-testid="brain-icon" data-size={size} />,
|
||||
Sparkles: ({ size }: { size?: number }) => <span data-testid="sparkles-icon" data-size={size} />,
|
||||
Search: ({ size }: { size?: number }) => <span data-testid="search-icon" data-size={size} />,
|
||||
Terminal: ({ size }: { size?: number }) => <span data-testid="terminal-icon" data-size={size} />,
|
||||
Cpu: ({ size }: { size?: number }) => <span data-testid="cpu-icon" data-size={size} />,
|
||||
}));
|
||||
|
||||
describe("ProviderIcon", () => {
|
||||
it("renders Brain icon for anthropic provider", () => {
|
||||
it("renders Anthropic brand icon for anthropic provider", () => {
|
||||
render(<ProviderIcon provider="anthropic" />);
|
||||
expect(screen.getByTestId("brain-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("anthropic-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Anthropic")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Sparkles icon for openai provider", () => {
|
||||
it("renders OpenAI brand icon for openai provider", () => {
|
||||
render(<ProviderIcon provider="openai" />);
|
||||
expect(screen.getByTestId("sparkles-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("openai-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("OpenAI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Search icon for google provider", () => {
|
||||
it("renders Gemini brand icon for google provider", () => {
|
||||
render(<ProviderIcon provider="google" />);
|
||||
expect(screen.getByTestId("search-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("gemini-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Google Gemini")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Search icon for gemini provider", () => {
|
||||
it("renders Gemini brand icon for gemini provider", () => {
|
||||
render(<ProviderIcon provider="gemini" />);
|
||||
expect(screen.getByTestId("search-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("gemini-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Google Gemini")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Terminal icon for ollama provider", () => {
|
||||
it("renders Ollama brand icon for ollama provider", () => {
|
||||
render(<ProviderIcon provider="ollama" />);
|
||||
expect(screen.getByTestId("terminal-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("ollama-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Ollama")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Cpu icon as fallback for unknown providers", () => {
|
||||
render(<ProviderIcon provider="unknown" />);
|
||||
expect(screen.getByTestId("cpu-icon")).toBeInTheDocument();
|
||||
// Cpu icon from lucide-react renders as an svg without our custom data-testid
|
||||
const icon = screen.getByText((_, element) => {
|
||||
return element?.tagName.toLowerCase() === "svg" &&
|
||||
element?.parentElement?.getAttribute("data-provider") === "unknown";
|
||||
});
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Cpu icon as fallback for empty provider", () => {
|
||||
render(<ProviderIcon provider="" />);
|
||||
expect(screen.getByTestId("cpu-icon")).toBeInTheDocument();
|
||||
const icon = screen.getByText((_, element) => {
|
||||
return element?.tagName.toLowerCase() === "svg" &&
|
||||
element?.parentElement?.getAttribute("data-provider") === "";
|
||||
});
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("normalizes provider name to lowercase", () => {
|
||||
render(<ProviderIcon provider="Anthropic" />);
|
||||
expect(screen.getByTestId("brain-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("anthropic-icon")).toBeInTheDocument();
|
||||
const wrapper = screen.getByTestId("anthropic-icon").parentElement;
|
||||
expect(wrapper).toHaveAttribute("data-provider", "anthropic");
|
||||
});
|
||||
|
||||
it("applies provider-specific color for anthropic", () => {
|
||||
render(<ProviderIcon provider="anthropic" />);
|
||||
const icon = screen.getByTestId("brain-icon").parentElement;
|
||||
const icon = screen.getByTestId("anthropic-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#d4a27f" });
|
||||
});
|
||||
|
||||
it("applies provider-specific color for openai", () => {
|
||||
render(<ProviderIcon provider="openai" />);
|
||||
const icon = screen.getByTestId("sparkles-icon").parentElement;
|
||||
const icon = screen.getByTestId("openai-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#10a37f" });
|
||||
});
|
||||
|
||||
it("applies provider-specific color for google", () => {
|
||||
render(<ProviderIcon provider="google" />);
|
||||
const icon = screen.getByTestId("search-icon").parentElement;
|
||||
const icon = screen.getByTestId("gemini-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#4285f4" });
|
||||
});
|
||||
|
||||
it("applies provider-specific color for ollama", () => {
|
||||
render(<ProviderIcon provider="ollama" />);
|
||||
const icon = screen.getByTestId("terminal-icon").parentElement;
|
||||
const icon = screen.getByTestId("ollama-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#fff" });
|
||||
});
|
||||
|
||||
it("applies default color for unknown providers", () => {
|
||||
render(<ProviderIcon provider="unknown" />);
|
||||
const icon = screen.getByTestId("cpu-icon").parentElement;
|
||||
const icon = document.querySelector('[data-provider="unknown"]');
|
||||
expect(icon).toHaveStyle({ color: "var(--text-muted)" });
|
||||
});
|
||||
|
||||
it("sets data-provider attribute with normalized provider name", () => {
|
||||
render(<ProviderIcon provider="Anthropic" />);
|
||||
const icon = screen.getByTestId("brain-icon").parentElement;
|
||||
expect(icon).toHaveAttribute("data-provider", "anthropic");
|
||||
render(<ProviderIcon provider="OpenAI" />);
|
||||
const icon = screen.getByTestId("openai-icon").parentElement;
|
||||
expect(icon).toHaveAttribute("data-provider", "openai");
|
||||
});
|
||||
|
||||
it("uses sm size (16px) by default", () => {
|
||||
render(<ProviderIcon provider="anthropic" />);
|
||||
expect(screen.getByTestId("brain-icon")).toHaveAttribute("data-size", "16");
|
||||
const icon = screen.getByTestId("anthropic-icon");
|
||||
expect(icon).toHaveAttribute("width", "16");
|
||||
expect(icon).toHaveAttribute("height", "16");
|
||||
});
|
||||
|
||||
it("uses sm size when explicitly specified", () => {
|
||||
render(<ProviderIcon provider="anthropic" size="sm" />);
|
||||
expect(screen.getByTestId("brain-icon")).toHaveAttribute("data-size", "16");
|
||||
const icon = screen.getByTestId("anthropic-icon");
|
||||
expect(icon).toHaveAttribute("width", "16");
|
||||
expect(icon).toHaveAttribute("height", "16");
|
||||
});
|
||||
|
||||
it("uses md size (20px) when specified", () => {
|
||||
render(<ProviderIcon provider="anthropic" size="md" />);
|
||||
expect(screen.getByTestId("brain-icon")).toHaveAttribute("data-size", "20");
|
||||
const icon = screen.getByTestId("anthropic-icon");
|
||||
expect(icon).toHaveAttribute("width", "20");
|
||||
expect(icon).toHaveAttribute("height", "20");
|
||||
});
|
||||
|
||||
it("uses lg size (24px) when specified", () => {
|
||||
render(<ProviderIcon provider="anthropic" size="lg" />);
|
||||
expect(screen.getByTestId("brain-icon")).toHaveAttribute("data-size", "24");
|
||||
const icon = screen.getByTestId("anthropic-icon");
|
||||
expect(icon).toHaveAttribute("width", "24");
|
||||
expect(icon).toHaveAttribute("height", "24");
|
||||
});
|
||||
|
||||
it("renders with className provider-icon", () => {
|
||||
render(<ProviderIcon provider="anthropic" />);
|
||||
const icon = screen.getByTestId("brain-icon").parentElement;
|
||||
const icon = screen.getByTestId("anthropic-icon").parentElement;
|
||||
expect(icon).toHaveClass("provider-icon");
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for anthropic", () => {
|
||||
render(<ProviderIcon provider="anthropic" />);
|
||||
const svg = screen.getByTestId("anthropic-icon");
|
||||
// The SVG should have the color in its path fill
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
// First path should have the provider color
|
||||
expect(paths[0]).toHaveAttribute("fill", "#d4a27f");
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for openai", () => {
|
||||
render(<ProviderIcon provider="openai" />);
|
||||
const svg = screen.getByTestId("openai-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for gemini", () => {
|
||||
render(<ProviderIcon provider="gemini" />);
|
||||
const svg = screen.getByTestId("gemini-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#4285f4");
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for ollama", () => {
|
||||
render(<ProviderIcon provider="ollama" />);
|
||||
const svg = screen.getByTestId("ollama-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#fff");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user