"use client"; import { useCallback, useEffect, useState } from "react"; import { useIsMobile } from "@/hooks/useIsMobile"; import { useI18n } from "@/hooks/useI18n"; interface VisionConfigFile { backend?: "ollama" | "openai"; ollama?: { host?: string; model?: string }; openai?: { baseUrl?: string; apiKey?: string; model?: string }; } const inputStyle: React.CSSProperties = { padding: "6px 9px", background: "var(--bg-panel)", border: "1px solid var(--border)", borderRadius: 5, color: "var(--text)", fontSize: 12, outline: "none", width: "100%", boxSizing: "border-box", }; function Field({ label, children, }: { label: string; children: React.ReactNode; }) { return (
{children}
); } function TextInput({ value, onChange, placeholder, mono, type, }: { value: string; onChange: (v: string) => void; placeholder?: string; mono?: boolean; type?: string; }) { return ( onChange(e.target.value)} placeholder={placeholder} type={type ?? "text"} style={{ ...inputStyle, fontFamily: mono ? "var(--font-mono)" : "inherit", }} /> ); } const saveButtonStyle = (primary: boolean): React.CSSProperties => ({ padding: "6px 14px", borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: "pointer", border: "1px solid var(--border)", background: primary ? "var(--accent)" : "var(--bg-panel)", color: primary ? "#fff" : "var(--text)", }); /** * Vision backend settings (backend picker + fields + save), without modal * chrome. Embedded in the Models config panel and reused by the standalone * modal below. */ export function VisionConfigContent() { const { t } = useI18n(); const [config, setConfig] = useState({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [savedOk, setSavedOk] = useState(false); const load = useCallback(async () => { setLoading(true); setSaveError(null); try { const res = await fetch("/api/vision-config"); const data = (await res.json()) as { config?: VisionConfigFile; error?: string; }; if (!res.ok || data.error) throw new Error(data.error ?? `HTTP ${res.status}`); setConfig(data.config ?? {}); } catch (e) { setSaveError(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const save = useCallback(async () => { setSaving(true); setSaveError(null); setSavedOk(false); try { const res = await fetch("/api/vision-config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(config), }); const data = (await res.json()) as { success?: boolean; error?: string }; if (!res.ok || !data.success) throw new Error(data.error ?? `HTTP ${res.status}`); setSavedOk(true); } catch (e) { setSaveError(e instanceof Error ? e.message : String(e)); } finally { setSaving(false); } }, [config]); const backend = config.backend ?? "ollama"; const ollama = config.ollama ?? {}; const openai = config.openai ?? {}; return (
{loading ? (
{t("vision.loading")}
) : ( <> {/* Backend picker */}
{(["ollama", "openai"] as const).map((b) => ( ))}
{/* Ollama settings */} {backend === "ollama" && (
setConfig((prev) => ({ ...prev, ollama: { ...prev.ollama, host: v }, })) } placeholder={t("vision.ollama.hostPlaceholder")} mono /> setConfig((prev) => ({ ...prev, ollama: { ...prev.ollama, model: v }, })) } placeholder={t("vision.ollama.modelPlaceholder")} mono />
)} {/* OpenAI-compatible settings */} {backend === "openai" && (
setConfig((prev) => ({ ...prev, openai: { ...prev.openai, baseUrl: v }, })) } placeholder={t("vision.openai.baseUrlPlaceholder")} mono /> setConfig((prev) => ({ ...prev, openai: { ...prev.openai, apiKey: v }, })) } type="password" mono /> setConfig((prev) => ({ ...prev, openai: { ...prev.openai, model: v }, })) } placeholder={t("vision.openai.modelPlaceholder")} mono />
)}
{t("vision.effective")}
)} {saveError && (
{t("vision.loadFailed")}: {saveError}
)} {savedOk && (
{t("vision.saved")}
)}
); } /** Standalone modal wrapper (kept for backward compatibility). */ export function VisionConfig({ onClose }: { onClose: () => void }) { const isMobile = useIsMobile(); const { t } = useI18n(); const [configPath, setConfigPath] = useState(""); useEffect(() => { fetch("/api/vision-config") .then((r) => r.json()) .then((d: { path?: string }) => setConfigPath(d.path ?? "")) .catch(() => {}); }, []); return (
{ if (e.target === e.currentTarget) onClose(); }} >
{t("vision.title")} {configPath || "data/agent/vision.json"}
); }