"use client"; import { useState, useEffect, useCallback } from "react"; import { useTranslations } from "next-intl"; import { Card } from "@/shared/components"; interface QdrantSettings { enabled: boolean; host: string; port: number; collection: string; embeddingModel: string; hasApiKey: boolean; apiKeyMasked: string | null; } interface EmbeddingModelOption { value: string; label: string; } export default function QdrantConfigCard() { const t = useTranslations("memory"); const [qdrant, setQdrant] = useState({ enabled: false, host: "", port: 6333, collection: "omniroute_memory", embeddingModel: "openai/text-embedding-3-small", hasApiKey: false, apiKeyMasked: null, }); const [apiKeyInput, setApiKeyInput] = useState(""); const [saving, setSaving] = useState(false); const [saveStatus, setSaveStatus] = useState<"" | "saved" | "error">(""); const [health, setHealth] = useState<{ ok: boolean; latencyMs: number; error?: string } | null>( null, ); const [checking, setChecking] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [searching, setSearching] = useState(false); const [searchResults, setSearchResults] = useState< Array<{ id: string; score: number; payload?: Record }> >([]); const [cleanupLoading, setCleanupLoading] = useState(false); const [cleanupMsg, setCleanupMsg] = useState(""); const [embeddingOptions, setEmbeddingOptions] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { Promise.all([ fetch("/api/settings/qdrant").then((r) => (r.ok ? r.json() : null)), fetch("/api/settings/qdrant/embedding-models").then((r) => (r.ok ? r.json() : null)), ]) .then(([qdrantData, embeddingData]) => { if (qdrantData) { setQdrant(qdrantData); setApiKeyInput(""); } if (embeddingData?.models) { setEmbeddingOptions(embeddingData.models); } }) .catch(() => {}) .finally(() => setLoading(false)); }, []); const save = useCallback( async (updates: Partial & { apiKey?: string }) => { const prev = qdrant; const next = { ...qdrant, ...updates }; setQdrant(next); setSaving(true); setSaveStatus(""); try { const body: Record = { enabled: next.enabled, host: next.host, port: next.port, collection: next.collection, embeddingModel: next.embeddingModel, }; if (updates.apiKey !== undefined) body.apiKey = updates.apiKey; const res = await fetch("/api/settings/qdrant", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (res.ok) { const data = await res.json().catch(() => next); setQdrant(data); setApiKeyInput(""); setSaveStatus("saved"); setTimeout(() => setSaveStatus(""), 2000); } else { setQdrant(prev); setSaveStatus("error"); } } catch { setQdrant(prev); setSaveStatus("error"); } finally { setSaving(false); } }, [qdrant], ); const checkHealth = useCallback(async () => { setChecking(true); try { const res = await fetch("/api/settings/qdrant/health"); if (res.ok) setHealth(await res.json()); else setHealth({ ok: false, latencyMs: 0, error: "HTTP error" }); } catch (e) { setHealth({ ok: false, latencyMs: 0, error: e instanceof Error ? e.message : String(e), }); } finally { setChecking(false); } }, []); const runSearch = useCallback(async () => { const q = searchQuery.trim(); if (!q) return; setSearching(true); setSearchResults([]); try { const res = await fetch("/api/settings/qdrant/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: q, topK: 5 }), }); const data = await res.json().catch(() => null); if (res.ok && data?.ok) { setSearchResults(Array.isArray(data.results) ? data.results : []); } } catch { setSearchResults([]); } finally { setSearching(false); } }, [searchQuery]); const runCleanup = useCallback(async () => { setCleanupLoading(true); setCleanupMsg(""); try { const res = await fetch("/api/settings/qdrant/cleanup", { method: "POST" }); const data = await res.json().catch(() => null); if (res.ok && data?.ok) { setCleanupMsg(t("qdrant.cleanupSuccess", { count: data.deletedCount ?? 0 })); } else { setCleanupMsg(t("qdrant.cleanupFailed")); } } catch { setCleanupMsg(t("qdrant.cleanupFailed")); } finally { setCleanupLoading(false); } }, [t]); if (loading) { return (
{t("loading")}
); } return (

{t("qdrant.title")}

{t("qdrant.description")}

{qdrant.enabled ? health?.ok ? t("qdrant.statusActive") : t("qdrant.statusError") : t("qdrant.statusDisabled")}
{/* Enable toggle + test connection */}

{t("qdrant.enableLabel")}

{t("qdrant.enableDesc")}

{health && (
{health.ok ? "check_circle" : "error"} {health.ok ? t("qdrant.healthOk", { latencyMs: health.latencyMs }) : (health.error ?? t("qdrant.healthError"))}
)} {saveStatus === "saved" && (
check_circle {t("qdrant.saved")}
)} {saveStatus === "error" && (
{t("qdrant.saveError")}
)}
setQdrant((s) => ({ ...s, host: e.target.value }))} placeholder="http://127.0.0.1" className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm font-mono focus:outline-none focus:ring-1 focus:ring-emerald-500" />
setQdrant((s) => ({ ...s, port: Math.max(1, Math.min(65535, Number(e.target.value) || 1)) })) } placeholder="6333" className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm font-mono focus:outline-none focus:ring-1 focus:ring-emerald-500" />
setQdrant((s) => ({ ...s, collection: e.target.value }))} placeholder="omniroute_memory" className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm font-mono focus:outline-none focus:ring-1 focus:ring-emerald-500" />
{embeddingOptions.length > 0 && ( )} setQdrant((s) => ({ ...s, embeddingModel: e.target.value }))} placeholder="openai/text-embedding-3-small" className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm font-mono focus:outline-none focus:ring-1 focus:ring-emerald-500" />
setApiKeyInput(e.target.value)} placeholder={ qdrant.hasApiKey ? t("qdrant.apiKeyKeepPlaceholder") : t("qdrant.apiKeyOptional") } className="flex-1 px-3 py-2 rounded-lg bg-background border border-border text-sm font-mono focus:outline-none focus:ring-1 focus:ring-emerald-500" /> {qdrant.hasApiKey && ( )}
{/* Search test */}

{t("qdrant.searchTestTitle")}

setSearchQuery(e.target.value)} placeholder={t("qdrant.searchPlaceholder")} onKeyDown={(e) => e.key === "Enter" && runSearch()} className="flex-1 px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-emerald-500" />
{searchResults.length > 0 && (
{searchResults.map((r) => (
{r.id} {r.score.toFixed(4)}
))}
)}
{/* Cleanup */}

{t("qdrant.cleanupTitle")}

{t("qdrant.cleanupDesc")}

{cleanupMsg &&

{cleanupMsg}

}
); }