A settings panel that lets users connect multiple AI provider API keys. The first connected provider is used as the active model; subsequent providers are automatic fallbacks. Keys are stored encrypted in the database.
Origin: ~/clarity — src/components/settings/ai-providers-panel.tsx
Add this panel to any JB Cloud Next.js app that needs AI provider management. Copy the component files directly from Clarity and adapt the PROVIDERS array for your app's supported models.
| File | Purpose |
|---|---|
src/components/settings/ai-providers-panel.tsx |
Main UI panel — full component |
src/app/api/integrations/[provider]/route.ts |
API route — POST (save) / DELETE (remove) |
src/lib/crypto.ts |
encryptToken() helper |
The ai-connect-form.tsx file is an older, simpler version. Prefer ai-providers-panel.tsx for new work.
Logos are stored in the claude-codex assets directory (~/.claude/assets/logos/), synced to all machines via /codex-sync. Copy them to the target project:
mkdir -p public/logos
cp ~/.claude/assets/logos/claude-logo.svg public/logos/
cp ~/.claude/assets/logos/google-logo.svg public/logos/
cp ~/.claude/assets/logos/groq-logo.svg public/logos/
DeepSeek has a logo (DeepSeek_idPu03Khfd_1.svg) but it is a wide wordmark (195×41 viewBox) — not suitable for an 8×8 avatar. Use the teal "D" initial instead.
| Provider | Logo file | Notes |
|---|---|---|
| Claude (Anthropic) | claude-logo.svg |
Coral swirl mark (#D97757), transparent bg |
| Gemini (Google) | google-logo.svg |
Multicolor Google G, transparent bg |
| DeepSeek | — | Teal initial "D" |
| Groq | groq-logo.svg |
Red square bg (#F54F35) baked into SVG |
// src/components/settings/ai-providers-panel.tsx
type ProviderId = "anthropic" | "gemini" | "deepseek" | "groq"
interface ProviderConfig {
id: ProviderId
label: string
model: string // shown as monospace label
description: string // shown as muted subtitle
placeholder: string // API key format hint
docsUrl: string // link to provider's API key page
avatarColor: string // tailwind bg class — used when no logoSrc
initial: string // 1-2 chars for avatar fallback
logoSrc?: string // path to SVG in public/logos/; absent = colored initial
}
const PROVIDERS: ProviderConfig[] = [
{ id: "anthropic", label: "Claude", model: "claude-sonnet-4-6",
avatarColor: "bg-violet-500", initial: "A", logoSrc: "/logos/claude-logo.svg" },
{ id: "gemini", label: "Gemini", model: "gemini-2.0-flash",
avatarColor: "bg-blue-500", initial: "G", logoSrc: "/logos/google-logo.svg" },
{ id: "deepseek", label: "DeepSeek", model: "deepseek-chat",
avatarColor: "bg-teal-500", initial: "D" /* no logo */ },
{ id: "groq", label: "Groq", model: "llama-3.3-70b-versatile",
avatarColor: "bg-orange-500", initial: "Gr", logoSrc: "/logos/groq-logo.svg" },
]
// Avatar render (replaces the colored-initial div):
// {provider.logoSrc ? (
// <img src={provider.logoSrc} alt={provider.label}
// className="w-8 h-8 rounded-lg object-contain shrink-0" />
// ) : (
// <div className={cn("w-8 h-8 rounded-lg flex items-center justify-center",
// "text-white text-xs font-semibold shrink-0",
// provider.avatarColor)}>
// {provider.initial}
// </div>
// )}
interface Props {
connected: Record<ProviderId, boolean>
}
<Card> with divide-y rows — one row per provider