import {
AlertCircle,
Check,
Eye,
EyeOff,
KeyRound,
Loader2,
Lock,
Plus,
ShieldCheck,
Star,
Trash2,
X,
} from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { api } from '@/lib/api-client'
import { cn } from '@/lib/utils'
import type { ProviderInfo } from '@/types/engine'
import { ProviderSelect } from './provider-select'
// ─── Category accent colors ──────────────────────────────────
const CATEGORY_ACCENT: Record<string, string> = {
major: 'border-l-blue-500',
open: 'border-l-emerald-500',
regional: 'border-l-amber-500',
local: 'border-l-violet-500',
}
const CATEGORY_DOT: Record<string, string> = {
major: 'bg-blue-500',
open: 'bg-emerald-500',
regional: 'bg-amber-500',
local: 'bg-violet-500',
}
// ─── ProviderCard ────────────────────────────────────────────
interface ProviderCardProps {
provider: ProviderInfo
isDefault: boolean
onSetDefault: () => void
onChangeKey: (apiKey: string) => void
onRemove: () => void
isPending?: boolean
}
export function ProviderCard({
provider,
isDefault,
onSetDefault,
onChangeKey,
onRemove,
isPending,
}: ProviderCardProps) {
const { t } = useTranslation()
const [showKeyInput, setShowKeyInput] = useState(false)
const [keyValue, setKeyValue] = useState('')
const [keyVisible, setKeyVisible] = useState(false)
const [validateState, setValidateState] = useState<'idle' | 'validating' | 'valid' | 'invalid'>(
'idle',
)
const [validateMsg, setValidateMsg] = useState('')
const isEnvKey = provider.keySource === 'env'
const handleKeySubmit = () => {
if (keyValue.trim()) {
onChangeKey(keyValue.trim())
setKeyValue('')
setShowKeyInput(false)
}
}
const handleValidate = async () => {
setValidateState('validating')
setValidateMsg('')
try {
const res = await api.post<{ valid: boolean; message?: string }>('/api/engine/validate-key', {
provider: provider.id,
})
setValidateState(res.valid ? 'valid' : 'invalid')
setValidateMsg(res.message ?? '')
} catch {
setValidateState('invalid')
setValidateMsg(t('common.error'))
}
}
return (
<div
className={cn(
'flex flex-col rounded-lg border border-l-[3px] bg-card p-4 transition-all',
CATEGORY_ACCENT[provider.category] ?? 'border-l-gray-400',
isDefault
? 'border-primary/40 ring-1 ring-primary/20'
: 'hover:border-primary/30 hover:shadow-sm',
)}
>
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<span
className={cn(
'h-2.5 w-2.5 rounded-full shrink-0',
CATEGORY_DOT[provider.category] ?? 'bg-gray-400',
)}
/>
<span className="font-medium text-sm truncate">{provider.name}</span>
</div>
{isDefault && (
<Badge variant="secondary" className="shrink-0 gap-1">
<Star className="h-3 w-3 fill-current" />
{t('engine.default')}
</Badge>
)}
</div>
{/* Description */}
{provider.description && (
<p className="text-xs text-muted-foreground mt-1.5 line-clamp-2">{provider.description}</p>
)}
{/* Status */}
<div className="flex items-center gap-1.5 mt-2 text-xs">
<Check
className={cn('h-3.5 w-3.5 shrink-0', isEnvKey ? 'text-amber-500' : 'text-emerald-500')}
/>
<span className="text-muted-foreground">
{isEnvKey ? t('engine.envKey') : t('engine.connected')}
{' · '}
{provider.modelCount} {t('engine.models')}
</span>
</div>
{/* Validation result */}
{validateState !== 'idle' && (
<div
className={cn(
'flex items-center gap-1.5 mt-1.5 rounded px-1.5 py-1 text-xs',
validateState === 'validating' && 'bg-muted/50 text-muted-foreground',
validateState === 'valid' && 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
validateState === 'invalid' && 'bg-red-500/10 text-red-500 dark:text-red-400',
)}
>
{validateState === 'validating' && <Loader2 className="h-3 w-3 shrink-0 animate-spin" />}
{validateState === 'valid' && <Check className="h-3 w-3 shrink-0" />}
{validateState === 'invalid' && <AlertCircle className="h-3 w-3 shrink-0" />}
<span className="truncate">
{validateState === 'validating' && t('engine.verifying')}
{validateState === 'valid' && t('engine.valid')}
{validateState === 'invalid' && (validateMsg || t('engine.invalid'))}
</span>
</div>
)}
{/* Key change (inline) */}
{showKeyInput ? (
<div className="mt-3 space-y-2">
<div className="relative">
<Input
type={keyVisible ? 'text' : 'password'}
value={keyValue}
onChange={(e) => setKeyValue(e.target.value)}
placeholder={t('engine.enterApiKey')}
onKeyDown={(e) => {
if (e.key === 'Enter') handleKeySubmit()
if (e.key === 'Escape') {
setShowKeyInput(false)
setKeyValue('')
}
}}
className="h-8 pr-9 text-sm"
autoFocus
/>
<button
type="button"
onClick={() => setKeyVisible((v) => !v)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{keyVisible ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
</button>
</div>
<div className="flex gap-2">
<Button
size="sm"
className="h-7"
onClick={handleKeySubmit}
disabled={isPending || !keyValue.trim()}
>
{t('common.save')}
</Button>
<Button
size="sm"
variant="ghost"
className="h-7"
onClick={() => {
setShowKeyInput(false)
setKeyValue('')
}}
>
{t('common.cancel')}
</Button>
</div>
</div>
) : (
/* Spacer + Actions — pushed to bottom for equal card heights */
<div className="flex-1" />
)}
{/* Actions */}
{!showKeyInput && (
<div className="flex items-center gap-1 mt-3 -mr-1">
<Button
size="icon"
variant="ghost"
className={cn('h-8 w-8', validateState === 'valid' && 'text-emerald-500')}
onClick={handleValidate}
disabled={validateState === 'validating' || isPending}
title={t('engine.verify')}
>
{validateState === 'validating' ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ShieldCheck className="h-4 w-4" />
)}
</Button>
{!isDefault && (
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={onSetDefault}
disabled={isPending}
title={t('engine.setAsDefault')}
>
<Star className="h-4 w-4" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => setShowKeyInput(true)}
disabled={isPending}
title={t('engine.changeKey')}
>
<KeyRound className="h-4 w-4" />
</Button>
{isEnvKey ? (
<span
className="ml-auto flex items-center gap-1 pr-1 text-xs text-muted-foreground"
title={t('engine.envProtected')}
>
<Lock className="h-3 w-3" />
</span>
) : (
<Button
size="icon"
variant="ghost"
className="ml-auto h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={onRemove}
disabled={isPending}
title={t('common.delete')}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
)}
</div>
)
}
// ─── AddProviderCard ─────────────────────────────────────────
interface AddProviderCardProps {
availableProviders: ProviderInfo[]
onAdd: (provider: string, apiKey: string) => void
isPending?: boolean
}
export function AddProviderCard({ availableProviders, onAdd, isPending }: AddProviderCardProps) {
const { t } = useTranslation()
const [expanded, setExpanded] = useState(false)
const [selectedProvider, setSelectedProvider] = useState<string | null>(null)
const [apiKey, setApiKey] = useState('')
const [keyVisible, setKeyVisible] = useState(false)
const handleConnect = () => {
if (selectedProvider && apiKey.trim()) {
onAdd(selectedProvider, apiKey.trim())
setApiKey('')
setSelectedProvider(null)
setExpanded(false)
setKeyVisible(false)
}
}
const handleCancel = () => {
setApiKey('')
setSelectedProvider(null)
setExpanded(false)
setKeyVisible(false)
}
if (!expanded) {
return (
<button
type="button"
onClick={() => setExpanded(true)}
disabled={availableProviders.length === 0}
className={cn(
'flex flex-col items-center justify-center gap-2 rounded-lg border bg-card p-4 min-h-[140px] transition-all',
availableProviders.length === 0
? 'border-dashed border-border/50 opacity-40 cursor-not-allowed'
: 'border-dashed border-border hover:border-primary/40 hover:bg-primary/5 cursor-pointer',
)}
>
<div className="flex h-9 w-9 items-center justify-center rounded-full border border-dashed border-border">
<Plus className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-xs text-muted-foreground">
{availableProviders.length === 0 ? t('engine.allConnected') : t('engine.addProvider')}
</span>
</button>
)
}
return (
<div className="flex flex-col rounded-lg border border-primary/40 bg-primary/5 p-4 ring-1 ring-primary/10">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium">{t('engine.addProvider')}</span>
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={handleCancel}>
<X className="h-4 w-4" />
</Button>
</div>
<div className="space-y-2.5">
<ProviderSelect
providers={availableProviders}
value={selectedProvider}
onValueChange={setSelectedProvider}
/>
<div className="relative">
<Input
type={keyVisible ? 'text' : 'password'}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={t('engine.enterApiKey')}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConnect()
if (e.key === 'Escape') handleCancel()
}}
className="h-8 pr-9 text-sm"
autoFocus
/>
<button
type="button"
onClick={() => setKeyVisible((v) => !v)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{keyVisible ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
</button>
</div>
<Button
className="w-full"
size="sm"
onClick={handleConnect}
disabled={isPending || !selectedProvider || !apiKey.trim()}
>
{t('engine.connect')}
</Button>
</div>
</div>
)
}