import { AnimatePresence, motion } from "framer-motion"; import { AlertCircle, CheckCircle, Loader, Lock, LockOpen, Wifi, WifiOff } from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input, Textarea } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Muted } from "@/components/ui/typography"; import { useInstances } from "@/hooks/useInstances"; import { checkConnection, type HealthStatus, type Instance, instanceSchema } from "@/lib/config"; import { COLOR } from "@/lib/constants"; interface SettingsFormProps { /** Instance to edit; pass `null` to create a new one. */ instance: Instance | null; /** Called after a successful save. Receives the saved instance id. */ onSaved?: (id: string) => void; /** Called when the user cancels (only meaningful when there's something to cancel back to). */ onCancel?: () => void; /** Hide the cancel button. */ hideCancel?: boolean; /** Override the submit button label. */ submitLabel?: string; } const statusConfig = { ok: { icon: CheckCircle, color: COLOR.success, label: "Connected" }, "auth-required": { icon: AlertCircle, color: COLOR.warning, label: "Auth required" }, unreachable: { icon: WifiOff, color: COLOR.destructive, label: "Unreachable" }, checking: { icon: Loader, color: COLOR.accentText, label: "Checking..." }, }; export function SettingsForm({ instance, onSaved, onCancel, hideCancel, submitLabel, }: SettingsFormProps) { const { add, update, activate } = useInstances(); const [name, setName] = useState(instance?.name ?? ""); const [baseUrl, setBaseUrl] = useState(instance?.baseUrl ?? "http://localhost:8000"); const [token, setToken] = useState(instance?.token ?? ""); const [errors, setErrors] = useState>>({}); const [saved, setSaved] = useState(false); const [health, setHealth] = useState<{ status: HealthStatus; message: string } | null>(null); const [checking, setChecking] = useState(false); const isCreate = instance === null; async function handleTest() { setChecking(true); setHealth({ status: "checking", message: "Connecting..." }); const result = await checkConnection(baseUrl, token || undefined); setHealth(result); setChecking(false); if (result.status === "auth-required" && !token) { document.getElementById("honcho-token")?.focus(); } } function handleSubmit(e: React.SyntheticEvent) { e.preventDefault(); const candidate = { id: instance?.id ?? "placeholder", name: name.trim() || "Default", baseUrl, token, }; const result = instanceSchema.safeParse(candidate); if (!result.success) { const fieldErrors: typeof errors = {}; for (const issue of result.error.issues) { const key = issue.path[0] as keyof Instance; fieldErrors[key] = issue.message; } setErrors(fieldErrors); return; } setErrors({}); let id: string; if (isCreate) { const created = add({ name: result.data.name, baseUrl: result.data.baseUrl, token: result.data.token, }); activate(created.id); id = created.id; } else { update(instance.id, { name: result.data.name, baseUrl: result.data.baseUrl, token: result.data.token, }); id = instance.id; } setSaved(true); setTimeout(() => { setSaved(false); onSaved?.(id); }, 600); } const StatusIcon = health ? statusConfig[health.status].icon : null; return (
{/* Name */}
setName(e.target.value)} placeholder="e.g. Local, Staging, Production" className="rounded-xl" /> {errors.name && (

{errors.name}

)} A short label to identify this connection
{/* Base URL */}
{ setBaseUrl(e.target.value); setHealth(null); }} placeholder="http://localhost:8000" className="flex-1 font-mono rounded-xl" />
{errors.baseUrl && (

{errors.baseUrl}

)} URL of your self-hosted Honcho instance
{/* Health status */} {health && (
{StatusIcon && ( )}

{statusConfig[health.status].label}

{health.message}
)}
{/* Token */}