"use client"; import * as React from "react"; import { toast } from "sonner"; import { Loader2, Save, Wifi, WifiOff, Trash2, Brain } from "lucide-react"; import { Header } from "@/components/layout/header"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; const AI_PROVIDERS = [ { value: "openai", label: "OpenAI", defaultModel: "gpt-4o-mini", hint: "GPT-4o, GPT-4o-mini" }, { value: "google", label: "Google Gemini", defaultModel: "gemini-2.5-flash", hint: "Gemini 2.5 Flash, Gemini 2.5 Pro" }, { value: "anthropic", label: "Anthropic", defaultModel: "claude-sonnet-4-20250514", hint: "Claude Sonnet, Claude Haiku" }, { value: "ollama", label: "Ollama (Local)", defaultModel: "llava:7b", hint: "llava:7b, moondream, llama3.2-vision" }, ] as const; type Settings = { ollamaUrl: string; model: string; watchDir: string; watching: boolean; sourceRetentionDays: number; imageRetentionDays: number; aiProvider: string; aiModel: string; }; export default function SettingsPage() { const [settings, setSettings] = React.useState({ ollamaUrl: "", model: "", watchDir: "", watching: false, sourceRetentionDays: 30, imageRetentionDays: 180, aiProvider: "ollama", aiModel: "", }); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); const [aiTestStatus, setAiTestStatus] = React.useState<"idle" | "testing" | "success" | "error">("idle"); const [cleanupStatus, setCleanupStatus] = React.useState<{ sourcesEligible: number; imagesEligible: number } | null>(null); const [cleaning, setCleaning] = React.useState(false); const fetchCleanupStatus = React.useCallback(() => { fetch("/api/cleanup") .then((r) => r.json()) .then((data) => { if (data.sourcesEligible !== undefined) setCleanupStatus(data); }) .catch(() => {}); }, []); React.useEffect(() => { fetch("/api/settings") .then((r) => r.json()) .then((data) => { setSettings(data); setLoading(false); }) .catch(() => { setLoading(false); }); fetchCleanupStatus(); }, [fetchCleanupStatus]); const handleSave = async () => { setSaving(true); try { const res = await fetch("/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings), }); if (!res.ok) throw new Error(); toast.success("Settings saved"); } catch { toast.error("Failed to save settings"); } finally { setSaving(false); } }; const testAiProvider = async () => { setAiTestStatus("testing"); try { const res = await fetch("/api/ai-test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: settings.aiProvider, model: settings.aiModel, ollamaUrl: settings.ollamaUrl, }), signal: AbortSignal.timeout(15000), }); if (res.ok) { setAiTestStatus("success"); toast.success("AI provider connected successfully"); } else { const data = await res.json().catch(() => ({})); setAiTestStatus("error"); toast.error(data.error || "AI provider test failed"); } } catch { setAiTestStatus("error"); toast.error("Cannot reach AI provider. Check your configuration and API keys."); } }; const handleProviderChange = (value: string | null) => { if (!value) return; const provider = AI_PROVIDERS.find((p) => p.value === value); setSettings((s) => ({ ...s, aiProvider: value, aiModel: provider?.defaultModel || "", })); setAiTestStatus("idle"); }; const toggleWatch = async () => { try { const action = settings.watching ? "stop" : "start"; const res = await fetch("/api/watch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, watchDir: settings.watchDir }), }); if (!res.ok) throw new Error(); setSettings((s) => ({ ...s, watching: !s.watching })); toast.success(action === "start" ? "Folder watching started" : "Folder watching stopped"); } catch { toast.error("Failed to toggle folder watching"); } }; const runCleanup = async () => { setCleaning(true); try { const res = await fetch("/api/cleanup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dryRun: false }), }); if (!res.ok) throw new Error(); const data = await res.json(); toast.success( `Cleanup complete: ${data.sourcesDeleted} source files, ${data.imagesDeleted} images, ${data.jobsPurged} jobs removed` ); fetchCleanupStatus(); } catch { toast.error("Cleanup failed"); } finally { setCleaning(false); } }; if (loading) { return (
); } return (
{/* AI Provider */}
AI Provider Choose the AI model for OCR processing
{aiTestStatus === "success" && } {aiTestStatus === "error" && } {aiTestStatus === "testing" && } {aiTestStatus === "success" ? "Connected" : aiTestStatus === "error" ? "Error" : aiTestStatus === "testing" ? "Testing..." : "Not tested"}
setSettings((s) => ({ ...s, aiModel: e.target.value }))} placeholder={AI_PROVIDERS.find((p) => p.value === settings.aiProvider)?.defaultModel || ""} />

{AI_PROVIDERS.find((p) => p.value === settings.aiProvider)?.hint || ""}

{settings.aiProvider === "ollama" && (
setSettings((s) => ({ ...s, ollamaUrl: e.target.value }))} placeholder="http://192.168.68.108:11434" />
)} {settings.aiProvider !== "ollama" && (

API key is read from the environment variable:{" "} {settings.aiProvider === "openai" ? "OPENAI_API_KEY" : settings.aiProvider === "google" ? "GOOGLE_GENERATIVE_AI_API_KEY" : "ANTHROPIC_API_KEY"}

)}
{/* Folder Watch */} Folder Monitoring Automatically process new PDFs dropped into a folder
setSettings((s) => ({ ...s, watchDir: e.target.value }))} placeholder="/data/watch" />

Absolute path on the server. Mount a host folder into the container.

{settings.watching && ( Active )}
{/* Storage & Cleanup */} Storage & Cleanup Auto-purge uploaded files and images to save storage
setSettings((s) => ({ ...s, sourceRetentionDays: parseInt(e.target.value) || 30, })) } />

Original uploaded PDFs are deleted after this many days. Card data is kept.

setSettings((s) => ({ ...s, imageRetentionDays: parseInt(e.target.value) || 180, })) } />

Scanned card images are removed after this many days. Card data is kept.

{cleanupStatus ? ( <> {cleanupStatus.sourcesEligible} source files {" and "} {cleanupStatus.imagesEligible} card images eligible ) : ( "Checking..." )}
{/* Monday.com Integration (placeholder) */} Monday.com Integration Push scanned card data to Monday.com boards (coming soon)

API endpoints are ready. Configure Monday.com connection in a future update.

Use the REST API at /api/cards to integrate with n8n, Zapier, or Monday.com directly.

); }