"use client"; import { useState, useCallback } from "react"; import { useSelfHostedConfig, useSetSelfHostedConfig, useTestSelfHosted, useSelfHostedModels, useRefreshSelfHostedModels, } from "@/hooks/use-providers"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { HelpTip } from "@/components/ui/help-tip"; import { AlertTriangle, CheckCircle2, Eye, EyeOff, RefreshCw, Server, XCircle, } from "lucide-react"; import { toast } from "sonner"; import type { SelfHostedTestResult } from "@/lib/api/providers"; // Relative-time helper (no date-fns dependency). function relativeTime(isoDate: string | null): string { if (!isoDate) return "never"; const diff = Date.now() - new Date(isoDate).getTime(); const s = Math.floor(diff / 1000); if (s < 60) return `${s}s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; return `${Math.floor(h / 24)}d ago`; } function errMsg(e: unknown): string { return e instanceof Error ? e.message : "Unknown error"; } /** Props exposed to the parent (ai-routing-card) so it can read test status. */ export interface SelfHostedSectionProps { /** Called whenever a successful connection test completes. */ onTestSuccess?: (modelCount: number) => void; /** The last known test result — driven by parent if the parent stores it. */ testResult?: SelfHostedTestResult | null; /** Called when the user clicks "Test Connection" and we get any result. */ onTestResult?: (result: SelfHostedTestResult) => void; } export function SelfHostedSection({ onTestSuccess, testResult, onTestResult, }: SelfHostedSectionProps) { const { data: config } = useSelfHostedConfig(); const { data: models = [] } = useSelfHostedModels(); const saveConfig = useSetSelfHostedConfig(); const testConnection = useTestSelfHosted(); const refreshModels = useRefreshSelfHostedModels(); // Local form state const [baseUrl, setBaseUrl] = useState(""); const [authToken, setAuthToken] = useState(""); const [showToken, setShowToken] = useState(false); // Timestamps for "Last refreshed" label const [lastRefreshed, setLastRefreshed] = useState(null); const hasSavedUrl = !!config?.base_url; // ---- Save handler -------------------------------------------------------- const handleSave = async () => { const url = baseUrl.trim(); if (!url) { toast.error("Enter a base URL first"); return; } try { await saveConfig.mutateAsync({ base_url: url, ...(authToken ? { auth_token: authToken } : {}), }); toast.success("Self-hosted config saved"); setBaseUrl(""); setAuthToken(""); } catch (e) { toast.error("Save failed: " + errMsg(e)); } }; // ---- Test Connection handler --------------------------------------------- const handleTest = async () => { try { const result = await testConnection.mutateAsync(); onTestResult?.(result); if (result.ok) { onTestSuccess?.(result.model_count ?? 0); setLastRefreshed(new Date().toISOString()); const count = result.model_count ?? 0; toast.success( `Connected — ${count} model${count === 1 ? "" : "s"} available`, ); } else { toast.error(`Connection failed: ${result.error ?? "unknown error"}`); } } catch (e) { toast.error("Test failed: " + errMsg(e)); } }; // ---- Refresh Models handler ---------------------------------------------- const handleRefresh = useCallback(async () => { try { await refreshModels.mutateAsync(); setLastRefreshed(new Date().toISOString()); toast.success("Model list refreshed"); } catch (e) { toast.error("Refresh failed: " + errMsg(e)); } }, [refreshModels]); // ---- Retry handler (from error empty state) ------------------------------ const handleRetry = () => handleTest(); // Determine which empty state to show, if any. const showNoUrlState = !hasSavedUrl; const showErrorState = hasSavedUrl && testResult?.ok === false && !showNoUrlState; const showNoModelsState = hasSavedUrl && testResult?.ok === true && models.length === 0 && !showNoUrlState; const showModelList = hasSavedUrl && testResult?.ok === true && models.length > 0; return (
{testResult?.ok === true && ( connected )} {testResult?.ok === false && ( error )}
setBaseUrl(e.target.value)} placeholder={ hasSavedUrl ? (config.base_url ?? "http://localhost:11434") : "http://localhost:11434" } className="font-mono text-sm" />
setAuthToken(e.target.value)} placeholder={ config?.has_token ? "•••••••••••• (leave blank to keep)" : "sk-… or Bearer token" } className="pr-10" />
{config?.has_token && (

A token is stored. Type a new value to update it.

)} {!config?.has_token && (

Stored Fernet-encrypted server-side; never returned by the API.

)}
{testResult?.ok === true && ( Connected — {testResult.model_count ?? 0} model {(testResult.model_count ?? 0) === 1 ? "" : "s"} available )} {testResult?.ok === false && ( {testResult.error ?? "Connection failed"} )}
{showNoUrlState && (

No base URL configured

Enter the URL of your self-hosted LLM endpoint (e.g.{" "} http://localhost:11434 for Ollama) and click{" "} Save. Then click Test Connection{" "} to verify and discover available models.

)} {showErrorState && (

Last test failed

{testResult?.error ?? "Unknown error"}

)} {showNoModelsState && (

Connected but no models found. Pull a model first (e.g.{" "} ollama pull llama3.2) then click Refresh Models.

)} {showModelList && (

Last refreshed:{" "} {relativeTime(lastRefreshed)}

{models.map((m) => (
{m.display_name} {m.model_name}
auto-discovered
))}
)}
); }