From d56f299698e0250a68e0dbb331c9f61b4b306403 Mon Sep 17 00:00:00 2001 From: mblanke Date: Tue, 17 Feb 2026 08:25:14 -0500 Subject: [PATCH] fix: dashboardId -> dashboardUid prop name --- public/.gitkeep | 0 src/app/page.tsx | 8 +- src/components/SemanticSearch.tsx | 507 ++++++++++++++++++++++++++++++ 3 files changed, 511 insertions(+), 4 deletions(-) create mode 100644 public/.gitkeep create mode 100644 src/components/SemanticSearch.tsx diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/page.tsx b/src/app/page.tsx index ac42d77..c890963 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -180,7 +180,7 @@ export default function Home() { @@ -189,17 +189,17 @@ export default function Home() {
diff --git a/src/components/SemanticSearch.tsx b/src/components/SemanticSearch.tsx new file mode 100644 index 0000000..1f8229f --- /dev/null +++ b/src/components/SemanticSearch.tsx @@ -0,0 +1,507 @@ +"use client"; + +import { useState, useEffect, useCallback, FormEvent } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + Search, + Brain, + Tag, + ChevronDown, + ChevronUp, + FileText, + Clock, + Zap, +} from "lucide-react"; +import clsx from "clsx"; + +/* ---------- types ---------- */ +interface RetrieveResult { + title?: string; + text: string; + source?: string; + page?: number; + score?: number; + tags?: string[]; +} + +interface AgentStep { + thought?: string; + action?: string; + observation?: string; +} + +interface AgentResponse { + answer: string; + steps?: AgentStep[]; + sources?: string[]; +} + +/* ---------- helpers ---------- */ +async function ragFetch(body: Record) { + const res = await fetch("/api/rag", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || `Request failed (${res.status})`); + } + return res.json(); +} + +function truncate(text: string, max = 220) { + if (text.length <= max) return text; + return text.slice(0, max).trimEnd() + "…"; +} + +function scoreColor(score: number) { + if (score >= 0.85) return "bg-green-500/20 text-green-400"; + if (score >= 0.7) return "bg-yellow-500/20 text-yellow-400"; + return "bg-gray-500/20 text-gray-400"; +} + +/* ---------- sub-components ---------- */ +function TagPill({ + label, + active, + onClick, +}: { + label: string; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} + +function ResultCard({ r, index }: { r: RetrieveResult; index: number }) { + return ( + +
+

+ + {r.title || r.source?.split("/").pop() || "Untitled"} +

+ {r.score != null && ( + + {(r.score * 100).toFixed(1)}% + + )} +
+ +

+ {truncate(r.text)} +

+ +
+ {r.source && ( + + + {r.source} + + )} + {r.page != null && ( + + p.{r.page} + + )} + {r.tags?.map((t) => ( + + {t} + + ))} +
+
+ ); +} + +function StepAccordion({ steps }: { steps: AgentStep[] }) { + const [open, setOpen] = useState(false); + + return ( +
+ + + {open && ( + +
+ {steps.map((step, i) => ( +
+
+ + Step {i + 1} + +
+ {step.thought && ( +

+ + Thought:{" "} + + {step.thought} +

+ )} + {step.action && ( +

+ + Action:{" "} + + + {step.action} + +

+ )} + {step.observation && ( +

+ + Observation:{" "} + + {step.observation} +

+ )} +
+ ))} +
+
+ )} +
+
+ ); +} + +/* ---------- main component ---------- */ +export default function SemanticSearch() { + const [mode, setMode] = useState<"search" | "agent">("search"); + const [query, setQuery] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // tags + const [availableTags, setAvailableTags] = useState([]); + const [selectedTags, setSelectedTags] = useState([]); + + // results + const [searchResults, setSearchResults] = useState([]); + const [agentResult, setAgentResult] = useState(null); + + // fetch tags on mount + useEffect(() => { + ragFetch({ action: "tags" }) + .then((data) => { + const tags: string[] = Array.isArray(data) + ? data + : Array.isArray(data.tags) + ? data.tags + : []; + setAvailableTags(tags); + }) + .catch(() => { + /* silently ignore – tags are optional */ + }); + }, []); + + const toggleTag = useCallback((tag: string) => { + setSelectedTags((prev) => + prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] + ); + }, []); + + const handleSubmit = useCallback( + async (e?: FormEvent) => { + e?.preventDefault(); + const trimmed = query.trim(); + if (!trimmed) return; + + setError(null); + setLoading(true); + setSearchResults([]); + setAgentResult(null); + + try { + if (mode === "search") { + const data = await ragFetch({ + action: "retrieve", + question: trimmed, + top_k: 8, + tags: selectedTags.length > 0 ? selectedTags : undefined, + }); + const results: RetrieveResult[] = Array.isArray(data) + ? data + : Array.isArray(data.results) + ? data.results + : []; + setSearchResults(results); + } else { + const data = await ragFetch({ + action: "agent", + question: trimmed, + max_steps: 5, + }); + setAgentResult(data); + } + } catch (err: unknown) { + setError( + err instanceof Error ? err.message : "An unexpected error occurred" + ); + } finally { + setLoading(false); + } + }, + [query, mode, selectedTags] + ); + + return ( +
+ {/* Header */} +
+

+ + Semantic Search +

+ + {/* Mode toggle */} +
+ + +
+
+ +
+ {/* Search bar */} +
+
+ setQuery(e.target.value)} + placeholder={ + mode === "search" + ? "Search your documents…" + : "Ask the AI agent a question…" + } + className="w-full bg-gray-900/60 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/30 transition-colors" + /> + {mode === "search" ? ( + + ) : ( + + )} +
+ +
+ + {/* Tag filter */} + {availableTags.length > 0 && mode === "search" && ( +
+ Filter: + {availableTags.map((tag) => ( + toggleTag(tag)} + /> + ))} +
+ )} + + {/* Error */} + + {error && ( + + {error} + + )} + + + {/* Loading */} + {loading && ( +
+
+
+ + {mode === "search" + ? "Searching documents…" + : "Agent is thinking…"} + +
+
+ )} + + {/* Search results */} + {!loading && mode === "search" && searchResults.length > 0 && ( + +

+ {searchResults.length} result + {searchResults.length !== 1 ? "s" : ""} found +

+ + {searchResults.map((r, i) => ( + + ))} + +
+ )} + + {/* Agent result */} + {!loading && mode === "agent" && agentResult && ( + + {/* Answer card */} +
+
+ + + Agent Answer + +
+
+ {agentResult.answer} +
+ {/* Source citations */} + {agentResult.sources && agentResult.sources.length > 0 && ( +
+

+ Sources +

+
+ {agentResult.sources.map((src, i) => ( + + {src} + + ))} +
+
+ )} +
+ + {/* Reasoning steps */} + {agentResult.steps && agentResult.steps.length > 0 && ( + + )} +
+ )} + + {/* Empty state */} + {!loading && + !error && + searchResults.length === 0 && + !agentResult && + query.trim() === "" && ( +
+ +

+ {mode === "search" + ? "Search across your ingested documents" + : "Ask the AI agent to research and answer questions"} +

+
+ )} +
+
+ ); +}