'use client'; import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { CheckCircle2, Eye, EyeOff, Loader2, XCircle } from 'lucide-react'; import { PageHeader } from '@/components/shared/page-header'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { usePermissions } from '@/hooks/use-permissions'; import { apiFetch } from '@/lib/api/client'; import { AiBudgetCard } from '@/components/admin/ai-budget-card'; type Provider = 'openai' | 'claude'; interface ConfigResp { data: { provider: Provider; model: string; hasApiKey: boolean; useGlobal: boolean; aiEnabled: boolean; }; models: Record; } type Scope = 'port' | 'global'; interface SettingsBlockProps { scope: Scope; title: string; description: string; /** Hide the "use global" checkbox on the global tab. */ showUseGlobal?: boolean; } function SettingsBlock({ scope, title, description, showUseGlobal }: SettingsBlockProps) { const queryClient = useQueryClient(); const queryKey = ['ocr-settings', scope]; const { data, isLoading } = useQuery({ queryKey, queryFn: () => apiFetch(`/api/v1/admin/ocr-settings?scope=${scope}`), }); const [provider, setProvider] = useState('openai'); const [model, setModel] = useState('gpt-4o-mini'); const [apiKey, setApiKey] = useState(''); const [showKey, setShowKey] = useState(false); const [useGlobal, setUseGlobal] = useState(false); const [aiEnabled, setAiEnabled] = useState(false); const [testStatus, setTestStatus] = useState( null, ); useEffect(() => { if (!data?.data) return; setProvider(data.data.provider); setModel(data.data.model); setUseGlobal(data.data.useGlobal); setAiEnabled(data.data.aiEnabled); }, [data?.data]); const save = useMutation({ mutationFn: (clearApiKey?: boolean) => apiFetch('/api/v1/admin/ocr-settings', { method: 'PUT', body: { scope, provider, model, apiKey: apiKey.length > 0 ? apiKey : undefined, clearApiKey: Boolean(clearApiKey), useGlobal: scope === 'global' ? false : useGlobal, aiEnabled: scope === 'global' ? false : aiEnabled, }, }), onSuccess: () => { setApiKey(''); queryClient.invalidateQueries({ queryKey }); }, }); const test = useMutation({ mutationFn: () => apiFetch<{ ok: boolean; reason?: string }>(`/api/v1/admin/ocr-settings/test`, { method: 'POST', body: { provider, model, apiKey }, }), onSuccess: (res) => setTestStatus(res.ok ? { ok: true } : { ok: false, reason: res.reason ?? 'Unknown' }), onError: (err: unknown) => setTestStatus({ ok: false, reason: err instanceof Error ? err.message : 'Network error', }), }); const models = data?.models[provider] ?? []; const hasKey = data?.data.hasApiKey ?? false; if (isLoading) { return ( {title} Loading… ); } return ( {title}

{description}

{showUseGlobal ? (
setUseGlobal(v === true)} />

When enabled, this port falls back to the system-wide OCR settings. Per-port provider/model/key are ignored.

) : null} {scope === 'port' ? (
setAiEnabled(v === true)} />

Off by default. Receipts are read on-device using Tesseract.js - accurate enough for most receipts and incurs no AI cost. Turning this on lets the configured provider re-parse receipts server-side for higher accuracy on hard-to-read images.

) : null}
{ setApiKey(e.target.value); setTestStatus(null); }} />

Stored encrypted at rest. Never re-displayed after saving.

{hasKey ? ( ) : null} {testStatus?.ok ? ( Connection OK ) : null} {testStatus && !testStatus.ok ? ( {testStatus.reason} ) : null}
); } export function OcrSettingsForm() { const { isSuperAdmin } = usePermissions(); return (
{isSuperAdmin ? ( ) : null}
); }