import { useState } from 'react'; import { Plus, Save, Trash2, RefreshCw, Loader2, X, Pencil } from 'lucide-react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@/components/ui/command'; import { useClient } from 'hooks/useClient'; import { useTranslation } from '@/lib/i18n'; type VersionInfo = { version: string | null; path: string | null; globalPath: string | null }; type StoredApiKeys = { keys: { provider: string; value: string }[] }; type LocalProviderEntry = { id: string; name: string; url: string; apiType: 'ollama' | 'openai-compatible' | 'lmstudio'; auth?: { type: 'api-key' | 'basic' }; }; type ProbeResult = { success: boolean; apiType?: LocalProviderEntry['apiType']; name?: string; needsAuth?: boolean; authType?: 'api-key' | 'basic' | 'unknown'; models?: string[]; error?: string; }; type ChatProvider = { key: string; providerId: string }; type ProbeState = { step: 'url' } | { step: 'probing' } | { step: 'auth'; probe: ProbeResult } | { step: 'saving' }; export const AIHarnessesSection = () => { const { t, tUi } = useTranslation(); const client = useClient(); const queryClient = useQueryClient(); const [installing, setInstalling] = useState(false); const { data: CHAT_PROVIDERS = [] as ChatProvider[] } = useQuery({ queryKey: ['CHAT_PROVIDERS_PROVIDERS'], queryFn: () => client.get('/server-settings/chat-providers/providers'), }); const { data: piMonoVersion, isLoading: piMonoLoading } = useQuery({ queryKey: ['CHAT_PROVIDERS_VERSION'], queryFn: () => client.get('/server-settings/chat-providers/version'), }); const { data: piMonoKeys } = useQuery({ queryKey: ['CHAT_PROVIDERS_API_KEYS'], queryFn: () => client.get('/server-settings/chat-providers/api-keys'), enabled: !!piMonoVersion?.version, }); const { data: localProviders = [] as LocalProviderEntry[] } = useQuery({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'], queryFn: () => client.get('/server-settings/chat-providers/local-providers'), enabled: !!piMonoVersion?.version, }); const { data: localHealth = {} as Record } = useQuery({ queryKey: ['CHAT_PROVIDERS_LOCAL_HEALTH'], queryFn: () => client.get>('/server-settings/chat-providers/local-providers/health'), enabled: localProviders.length > 0, refetchInterval: 15_000, }); const [keyInputs, setKeyInputs] = useState>({}); const [savingKey, setSavingKey] = useState(null); const [commandOpen, setCommandOpen] = useState(false); const [editingProvider, setEditingProvider] = useState(null); // Local provider connection flow const [addingLocal, setAddingLocal] = useState(false); const [localName, setLocalName] = useState(''); const [localUrl, setLocalUrl] = useState(''); const [probeState, setProbeState] = useState({ step: 'url' }); const [authApiKey, setAuthApiKey] = useState(''); const [authUsername, setAuthUsername] = useState(''); const [authPassword, setAuthPassword] = useState(''); const resetLocalForm = () => { setAddingLocal(false); setLocalName(''); setLocalUrl(''); setProbeState({ step: 'url' }); setAuthApiKey(''); setAuthUsername(''); setAuthPassword(''); }; const storedKeys: StoredApiKeys['keys'] = piMonoKeys?.keys ?? []; const storedProviderIds = new Set(storedKeys.map((k) => k.provider)); const connectedProviders = CHAT_PROVIDERS.filter((p) => storedProviderIds.has(p.providerId)); const unconnectedProviders = CHAT_PROVIDERS.filter((p) => !storedProviderIds.has(p.providerId)); const { data: remoteHealth = {} as Record } = useQuery({ queryKey: ['CHAT_PROVIDERS_REMOTE_HEALTH'], queryFn: () => client.get>('/server-settings/chat-providers/api-keys/health'), enabled: connectedProviders.length > 0, refetchInterval: 30_000, }); const getStoredMasked = (providerId: string) => storedKeys.find((k) => k.provider === providerId)?.value ?? ''; const saveApiKey = async (providerId: string) => { const value = keyInputs[providerId]; if (value === undefined) return; setSavingKey(providerId); try { await client.put('/server-settings/chat-providers/api-keys', { provider: providerId, value }); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_API_KEYS'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_REMOTE_HEALTH'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] }); setKeyInputs((prev) => { const next = { ...prev }; delete next[providerId]; return next; }); setEditingProvider(null); } finally { setSavingKey(null); } }; const disconnectProvider = async (provider: (typeof CHAT_PROVIDERS)[number]) => { await client.put('/server-settings/chat-providers/api-keys', { provider: provider.providerId, value: '' }); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_API_KEYS'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_REMOTE_HEALTH'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] }); if (editingProvider === provider.key) setEditingProvider(null); }; const installAgent = async () => { setInstalling(true); try { const result = await client.post('/server-settings/chat-providers/install'); queryClient.setQueryData(['CHAT_PROVIDERS_VERSION'], result); } finally { setInstalling(false); } }; const handleProbe = async ( auth?: { type: 'api-key'; apiKey: string } | { type: 'basic'; username: string; password: string }, ) => { setProbeState({ step: 'probing' }); try { const result = await client.post('/server-settings/chat-providers/local-providers/probe', { url: localUrl.trim(), auth, }); if (!result.success) { toast.error(result.error ?? 'Could not detect API type'); setProbeState({ step: 'url' }); return; } if (result.needsAuth) { setProbeState({ step: 'auth', probe: result }); return; } // No auth needed — save directly await saveLocalProvider(result, auth); } catch { toast.error(t('settings.server.harnesses.connectFailed')); setProbeState({ step: 'url' }); } }; const handleAuthSubmit = async (probe: ProbeResult) => { const auth = probe.authType === 'basic' ? { type: 'basic' as const, username: authUsername, password: authPassword } : { type: 'api-key' as const, apiKey: authApiKey }; // Re-probe with credentials to verify they work setProbeState({ step: 'probing' }); try { const result = await client.post('/server-settings/chat-providers/local-providers/probe', { url: localUrl.trim(), auth, }); if (!result.success) { toast.error(result.error ?? 'Could not connect with provided credentials'); setProbeState({ step: 'auth', probe }); return; } if (result.needsAuth) { toast.error(t('settings.server.harnesses.authFailed')); setProbeState({ step: 'auth', probe }); return; } await saveLocalProvider(result, auth); } catch { toast.error(t('settings.server.harnesses.connectFailed')); setProbeState({ step: 'auth', probe }); } }; const saveLocalProvider = async ( probe: ProbeResult, auth?: { type: 'api-key'; apiKey: string } | { type: 'basic'; username: string; password: string }, ) => { setProbeState({ step: 'saving' }); try { await client.post('/server-settings/chat-providers/local-providers', { url: localUrl.trim(), name: localName.trim() || probe.name, apiType: probe.apiType, auth, }); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_HEALTH'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] }); toast.success(t('settings.server.harnesses.connectedTo', { name: probe.name ?? '' })); resetLocalForm(); } catch { toast.error(t('settings.server.harnesses.saveFailed')); setProbeState({ step: 'url' }); } }; const submitLocalUrl = (ev?: React.FormEvent) => { ev?.preventDefault(); if (probeState.step === 'url' && localUrl.trim()) handleProbe(); }; const submitAuth = (ev?: React.FormEvent) => { ev?.preventDefault(); if (probeState.step !== 'auth') return; const ready = probeState.probe.authType === 'basic' ? authUsername && authPassword : authApiKey; if (ready) handleAuthSubmit(probeState.probe); }; const submitApiKey = (ev: React.FormEvent, providerId: string) => { ev.preventDefault(); if (keyInputs[providerId]) saveApiKey(providerId); }; const removeLocalProvider = async (id: string) => { await client.delete(`/server-settings/chat-providers/local-providers/${id}`); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_HEALTH'] }); queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] }); }; return (
{t('settings.server.harnesses.piMono')} {piMonoLoading ? ( {t('settings.server.harnesses.checking')} ) : piMonoVersion?.version ? (
{piMonoVersion.version}
) : ( )}
{piMonoVersion?.version && ( <> {/* Local Providers */}
{t('settings.server.harnesses.localProviders')}
{localProviders.map((lp: LocalProviderEntry) => (
{lp.name} {lp.url}
))} {addingLocal ? (
{/* Step 1: Name + URL inputs stacked */}
setLocalName(ev.target.value)} onKeyDown={(ev) => { if (ev.key === 'Escape') resetLocalForm(); }} disabled={probeState.step !== 'url'} autoFocus /> setLocalUrl(ev.target.value)} onKeyDown={(ev) => { if (ev.key === 'Escape') resetLocalForm(); }} disabled={probeState.step !== 'url'} />
{probeState.step === 'url' && ( )} {(probeState.step === 'probing' || probeState.step === 'saving') && ( )}
{/* Step 2: Auth form (if needed) */} {probeState.step === 'auth' && (
{probeState.probe.name} requires authentication {probeState.probe.authType === 'basic' ? ( <> setAuthUsername(ev.target.value)} autoFocus /> setAuthPassword(ev.target.value)} /> ) : ( <> setAuthApiKey(ev.target.value)} autoFocus /> )}
)}
) : ( )}
{/* Remote Providers */}
{t('settings.server.harnesses.remoteProviders')}
{connectedProviders.map((provider) => (
{provider.key}
{editingProvider === provider.key && (
submitApiKey(ev, provider.providerId)} className="flex items-center gap-2 pl-4" > setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value })) } onKeyDown={(ev) => { if (ev.key === 'Escape') setEditingProvider(null); }} autoFocus />
)}
))} {editingProvider && (() => { const provider = CHAT_PROVIDERS.find((p) => p.key === editingProvider); if (!provider || connectedProviders.some((cp) => cp.providerId === provider.providerId)) return null; return (
submitApiKey(ev, provider.providerId)} className="flex items-center gap-2" > setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value })) } onKeyDown={(ev) => { if (ev.key === 'Escape') setEditingProvider(null); }} autoFocus />
); })()} {unconnectedProviders.length > 0 && ( )}
{t('settings.server.harnesses.none')} {unconnectedProviders.map((provider) => ( { setEditingProvider(provider.key); setCommandOpen(false); }} > {provider.key} ))}
)}
); };