import { useState, useEffect, useRef } from "react"; import NavBar from "../components/NavBar"; import api from "../api/client"; import { useToastContext } from "../context/ToastContext"; interface Document { job_id: string; filename: string; file_type: string; chunk_count?: number; } interface Citation { source: string; page?: string; excerpt: string; index?: number; page_or_segment?: string; filename?: string; } interface QueryResult { answer: string; citations: Citation[]; confidence_gate_passed: boolean; prompt_tokens?: number; completion_tokens?: number; latency_ms?: number; ragas_scores?: Record; } interface HistoryItem { question: string; result: QueryResult; } const FILE_ICONS: Record = { pdf: "PDF", docx: "DOC", xlsx: "XLS", csv: "CSV", image: "IMG", video: "VID", audio: "AUD", }; const SUGGESTED_QUERIES = [ "Summarize the key findings", "What are the main risks?", "List all action items", "What conclusions were drawn?", ]; function ragasBadgeClass(v: number, isFaith = false): string { const hi = isFaith ? 0.8 : 0.7; const mid = isFaith ? 0.7 : 0.5; if (v >= hi) return "badge badge-ok"; if (v >= mid) return "badge badge-warn"; return "badge badge-err"; } function renderAnswer(text: string) { const parts = text.split(/(\[\d+\])/g); return parts.map((p, i) => { const m = p.match(/^\[(\d+)\]$/); if (m) { return ( document.getElementById(`cite-${m[1]}`)?.scrollIntoView({ behavior: "smooth" })} > [{m[1]}] ); } return {p}; }); } export default function QueryPage() { const [docs, setDocs] = useState([]); const [docsLoading, setDocsLoading] = useState(true); const [docsError, setDocsError] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [question, setQuestion] = useState(""); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [streamingText, setStreamingText] = useState(""); const [streaming, setStreaming] = useState(false); const [streamMode, setStreamMode] = useState(false); const [error, setError] = useState(""); const [queryHistory, setQueryHistory] = useState([]); const [copied, setCopied] = useState(false); const textareaRef = useRef(null); const { addToast } = useToastContext(); const loadDocs = () => { setDocsLoading(true); setDocsError(false); api.get("/v1/documents") .then(r => setDocs(r.data)) .catch(() => setDocsError(true)) .finally(() => setDocsLoading(false)); }; useEffect(() => { loadDocs(); }, []); const toggleDoc = (id: string) => setSelectedIds(prev => { const s = new Set(prev); s.has(id) ? s.delete(id) : s.add(id); return s; }); const toggleAll = () => setSelectedIds(prev => prev.size === docs.length ? new Set() : new Set(docs.map(d => d.job_id))); const submitStream = async () => { setError(""); setLoading(true); setResult(null); setStreamingText(""); setStreaming(true); addToast("Query submitted", "info"); const body: { question: string; job_ids?: string[] } = { question }; if (selectedIds.size > 0) body.job_ids = [...selectedIds]; try { const token = (api.defaults.headers.common["Authorization"] as string | undefined)?.replace("Bearer ", ""); const resp = await fetch(`${api.defaults.baseURL}/v1/query/stream`, { method: "POST", headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(body), }); if (!resp.ok) { const err = await resp.json().catch(() => ({ detail: "Stream request failed" })); throw new Error(err.detail || "Stream request failed"); } const reader = resp.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const data = JSON.parse(line.slice(6)); if (data.type === "chunk") { setStreamingText(prev => prev + data.text); } else if (data.type === "done") { const finalResult: QueryResult = { answer: data.answer, citations: (data.citations || []).map((c: { filename?: string; page_or_segment?: string; excerpt?: string }) => ({ source: c.filename || "", page: c.page_or_segment, excerpt: c.excerpt || "", })), confidence_gate_passed: data.confidence_gate_passed, ragas_scores: data.ragas_scores, }; setResult(finalResult); setStreamingText(""); setQueryHistory(prev => [{ question, result: finalResult }, ...prev.slice(0, 9)]); } else if (data.type === "error") { throw new Error(data.message); } } } } catch (e: unknown) { const err = e as Error; setError(err.message || "Stream query failed"); } finally { setLoading(false); setStreaming(false); } }; const submit = async () => { if (!question.trim()) return; if (streamMode) { await submitStream(); return; } setError(""); setLoading(true); setResult(null); addToast("Query submitted", "info"); try { const body: { question: string; job_ids?: string[] } = { question }; if (selectedIds.size > 0) body.job_ids = [...selectedIds]; const r = await api.post("/v1/query", body); setResult(r.data); setQueryHistory(prev => [{ question, result: r.data }, ...prev.slice(0, 9)]); } catch (e: unknown) { const err = e as { response?: { data?: { detail?: string } } }; setError(err.response?.data?.detail || "Query failed"); } finally { setLoading(false); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && e.ctrlKey) submit(); }; const copyAnswer = () => { if (!result?.answer) return; navigator.clipboard.writeText(result.answer).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }; const displayAnswer = result?.answer || streamingText; const latencyDisplay = result?.latency_ms ? `${(result.latency_ms / 1000).toFixed(1)}s` : "—"; const confidenceDisplay = result ? (result.confidence_gate_passed ? "High" : "Low") : "—"; return (
{/* Page header */}

Query your documents

Ask questions across your indexed documents using semantic search and RAG.

{/* Stat strip */}
Indexed Documents
{docs.length}
ready to query
Queries Today
{queryHistory.length}
this session
Confidence
{confidenceDisplay}
last result
Latency
{latencyDisplay}
last query
{/* Query grid: sidebar + main */}
{/* LEFT: Document panel */} {/* RIGHT: Main panel */}
{/* Ask card */}