diff --git a/apps/web/app/(dashboard)/dashboard/playground/page.tsx b/apps/web/app/(dashboard)/dashboard/playground/page.tsx index 0696a4a..36424b7 100644 --- a/apps/web/app/(dashboard)/dashboard/playground/page.tsx +++ b/apps/web/app/(dashboard)/dashboard/playground/page.tsx @@ -1,730 +1,167 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { Textarea } from "@/components/ui/textarea"; -import { Badge } from "@/components/ui/badge"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Send, Square, RotateCcw } from "lucide-react"; +import { DocumentPicker } from "@/components/playground/DocumentPicker"; +import { ChatThread } from "@/components/playground/ChatThread"; +import { RetrievalInspector } from "@/components/playground/RetrievalInspector"; import { - AlertCircle, - Play, - FileText, - Search, - ChevronDown, - ChevronRight, - Copy, - Loader2, - Sparkles, -} from "lucide-react"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { PageHeader } from "@/components/dashboard/PageHeader"; -import { runPlaygroundQuery } from "@/lib/actions/playground"; - -// ---------- Types ---------- - -interface Document { - doc_id: string; - title: string; - source_type: string; - section_count: number; - status: string; -} - -interface SelectedSection { - section_id: string; - title: string; - relevance_score: number; - page_range: string; - summary: string; - content_preview: string; -} - -interface QueryTiming { - total_ms: number; - toc_retrieval_ms: number; - section_selection_ms: number; - content_fetch_ms: number; -} - -interface QueryResult { - toc?: any; - selected_section_ids?: string[]; - sections?: any[]; - timing: QueryTiming; - reasoning: string; - // Legacy fields - query?: string; - doc_id?: string | null; - strategy?: string; - selected_sections?: SelectedSection[]; -} - -type RetrievalMode = "vectorless" | "hybrid" | "vector"; - -// Maps the UI retrieval modes to the API strategy values -const modeToStrategy: Record = { - vectorless: "extract", - hybrid: "hybrid", - vector: "generate", -}; - -const modeDescriptions: Record = { - vectorless: - "ToC-first retrieval: uses the document manifest to select sections semantically", - hybrid: - "Combines ToC-based selection with vector similarity for best coverage", - vector: - "Pure vector similarity search across all embedded chunks", -}; - -// ---------- Sub-components ---------- - -function CollapsibleJson({ - data, - defaultOpen = false, -}: { - data: unknown; - defaultOpen?: boolean; -}) { - const [open, setOpen] = useState(defaultOpen); - const [copied, setCopied] = useState(false); - const jsonStr = JSON.stringify(data, null, 2); - - const handleCopy = async () => { - await navigator.clipboard.writeText(jsonStr); - setCopied(true); - setTimeout(() => setCopied(false), 1500); - }; - - return ( -
- - {open && ( -
- - {copied && ( - - Copied - - )} -
-            {jsonStr}
-          
-
- )} -
- ); -} - -function SectionCard({ section }: { section: SelectedSection }) { - const [expanded, setExpanded] = useState(false); - - return ( -
- - {expanded && ( -
-
-

- Summary -

-

{section.summary}

-
- -
-

- Content Preview -

-

- {section.content_preview} -

-
-
- )} -
- ); -} - -function TraceStep({ - stepNumber, - title, - subtitle, - timing, - status, - children, -}: { - stepNumber: number; - title: string; - subtitle: string; - timing?: string; - status: "pending" | "running" | "done"; - children?: React.ReactNode; -}) { - const [open, setOpen] = useState(status === "done"); - - useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - if (status === "done") setOpen(true); - }, [status]); - - return ( -
- {/* Connector line */} - {stepNumber < 3 && ( -
- )} - -
- {/* Step circle */} -
- {status === "running" ? ( - - ) : ( - stepNumber - )} -
- - {/* Content */} -
- - - {status === "running" && ( -
- - - -
- )} - - {status === "done" && open && ( -
{children}
- )} -
-
-
- ); -} - -// ---------- Main page ---------- + useTreewalkStream, + type AssistantMessage, +} from "@/lib/hooks/useTreewalkStream"; +import type { PlaygroundDoc } from "@/app/api/dashboard/playground/documents/route"; export default function PlaygroundPage() { - const [query, setQuery] = useState(""); - const [selectedDoc, setSelectedDoc] = useState("all"); - const [mode, setMode] = useState("hybrid"); - - // Documents fetched from API - const [documents, setDocuments] = useState([]); + const [demo, setDemo] = useState([]); + const [mine, setMine] = useState([]); const [loadingDocs, setLoadingDocs] = useState(true); + const [source, setSource] = useState<"demo" | "mine">("demo"); + const [docId, setDocId] = useState(""); + const [input, setInput] = useState(""); - // Query execution state - const [isRunning, setIsRunning] = useState(false); - const [activeStep, setActiveStep] = useState(0); // 0 = not started, 1-3 = running step - const [result, setResult] = useState(null); - const [error, setError] = useState(null); + const { messages, isStreaming, send, stop, reset } = useTreewalkStream(); - // Fetch documents on mount useEffect(() => { - async function fetchDocuments() { - setLoadingDocs(true); - try { - const res = await fetch("/api/dashboard/documents"); - if (!res.ok) throw new Error("Failed to fetch documents"); - const data = await res.json(); - setDocuments( - (data.documents || []).filter( - (d: Document) => d.status === "ready" - ) - ); - } catch { - // Fall back silently -- the user can still query across all documents - setDocuments([]); - } finally { - setLoadingDocs(false); - } - } - fetchDocuments(); + let cancelled = false; + fetch("/api/dashboard/playground/documents") + .then((r) => r.json()) + .then((d: { demo?: PlaygroundDoc[]; mine?: PlaygroundDoc[] }) => { + if (cancelled) return; + setDemo(d.demo ?? []); + setMine(d.mine ?? []); + if ((d.demo ?? []).length > 0) setDocId(d.demo![0].doc_id); + else if ((d.mine ?? []).length > 0) { + setSource("mine"); + setDocId(d.mine![0].doc_id); + } + }) + .catch(() => {}) + .finally(() => !cancelled && setLoadingDocs(false)); + return () => { + cancelled = true; + }; }, []); - const handleRunQuery = useCallback(async () => { - if (!query.trim() || isRunning) return; - - setError(null); - setResult(null); - setIsRunning(true); - - // Animate through the steps for visual feedback - setActiveStep(1); - await new Promise((r) => setTimeout(r, 400)); - - setActiveStep(2); - await new Promise((r) => setTimeout(r, 600)); - - setActiveStep(3); - - // Actually run the query - const docIds = - selectedDoc && selectedDoc !== "all" ? [selectedDoc] : []; - const response = await runPlaygroundQuery({ - query, - docIds, - strategy: modeToStrategy[mode], - }); - - if (response.error) { - setError(response.error); - setIsRunning(false); - setActiveStep(0); - return; - } - - setResult(response.data as unknown as QueryResult); - setActiveStep(4); // All done - setIsRunning(false); - }, [query, selectedDoc, mode, isRunning]); + const onSourceChange = useCallback( + (s: "demo" | "mine") => { + setSource(s); + const list = s === "demo" ? demo : mine; + setDocId(list[0]?.doc_id ?? ""); + }, + [demo, mine], + ); - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - handleRunQuery(); + const activeAssistant = useMemo(() => { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === "assistant") + return messages[i] as AssistantMessage; } - }; + return null; + }, [messages]); - const stepStatus = (step: number): "pending" | "running" | "done" => { - if (activeStep === 0) return "pending"; - if (step < activeStep) return "done"; - if (step === activeStep) return isRunning ? "running" : "done"; - return "pending"; - }; + const submit = useCallback(() => { + if (!input.trim() || !docId || isStreaming) return; + send(input.trim(), { docId, source }); + setInput(""); + }, [input, docId, isStreaming, send, source]); - // Build the ToC manifest view from the result - const tocManifest = result - ? { - document: result.doc_id || "all", - strategy: result.strategy, - sections: (result.selected_sections || []).map((s) => ({ - id: s.section_id, - title: s.title, - page_range: s.page_range, - summary: s.summary, - })), - } - : null; + const canSend = input.trim().length > 0 && !!docId && !isStreaming; return ( -
- - Query, live. - - } - description="Run real retrieval queries against your documents and inspect every step of the trace." - actions={ - - } - /> - -
- {/* Left panel - Query configuration (2 cols) */} -
- - - - - Query - - - Configure and run a retrieval query - - - - {/* Document selector */} -
- - {loadingDocs ? ( - - ) : ( - - )} -
- - {/* Query input */} -
- -