"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { CorpusSummary, DocumentMeta, GroundingSummary, corpusSummary, fetchGroundingSummary, fetchSuggestions, ingestFile, ingestUrl } from "@/lib/api"; import { displaySource, timeAgo } from "@/lib/format"; import { CorpusManageModal } from "@/components/CorpusManageModal"; import { IngestSources } from "@/components/IngestSources"; const ACCEPT = ".pdf,.docx,.html,.htm,.md,.txt,.wav,.mp3,.m4a"; const TYPES = ["PDF", "DOCX", "HTML", "Markdown", "TXT", "WAV", "MP3", "M4A"]; interface Recent { name: string; docs: number; chunks: number; skipped: number; ok: boolean; } export function IngestPanel({ onAsk, onDeleted }: { onAsk?: (q: string) => void; onDeleted?: () => void }) { const [status, setStatus] = useState<{ kind: "ok" | "err" | "info"; msg: string } | null>(null); const [busy, setBusy] = useState(false); const [drag, setDrag] = useState(false); const fileInputRef = useRef(null); const [summary, setSummary] = useState(null); const [groundingSummary, setGroundingSummary] = useState(null); const [showReindexInfo, setShowReindexInfo] = useState(false); const [samples, setSamples] = useState([]); const [recent, setRecent] = useState([]); const [showManage, setShowManage] = useState(false); const [manageAction, setManageAction] = useState<"clear_all" | "delete_last" | undefined>(undefined); const [urlOpen, setUrlOpen] = useState(false); const [urlValue, setUrlValue] = useState(""); const urlInputRef = useRef(null); const refresh = useCallback(async () => { try { const [s, sug] = await Promise.all([corpusSummary(), fetchSuggestions(3)]); setSummary(s); setSamples(sug.suggestions); } catch { /* backend may be warming up */ } try { const gs = await fetchGroundingSummary(); setGroundingSummary(gs); } catch { /* visual grounding endpoint unavailable */ } }, []); useEffect(() => { void refresh(); }, [refresh]); const upload = useCallback( async (file: File) => { setBusy(true); setStatus({ kind: "info", msg: `Uploading ${file.name} — parsing → chunking → indexing…` }); try { const r = await ingestFile(file); setStatus({ kind: "ok", msg: `✓ Indexed ${r.documents} document(s), ${r.chunks} chunk(s)${ r.skipped ? `, ${r.skipped} skipped` : "" }.`, }); setRecent((prev) => [{ name: file.name, docs: r.documents, chunks: r.chunks, skipped: r.skipped || 0, ok: true }, ...prev].slice(0, 5), ); await refresh(); } catch (err) { setStatus({ kind: "err", msg: `✗ ${(err as Error).message}` }); setRecent((prev) => [{ name: file.name, docs: 0, chunks: 0, skipped: 0, ok: false }, ...prev].slice(0, 5)); } finally { setBusy(false); } }, [refresh], ); function onDrop(e: React.DragEvent) { e.preventDefault(); setDrag(false); const file = e.dataTransfer.files?.[0]; if (file && !busy) void upload(file); } const submitUrl = useCallback(async () => { const url = urlValue.trim(); if (!url || busy) return; setBusy(true); setStatus({ kind: "info", msg: `Fetching ${url} — extracting → chunking → indexing…` }); try { const r = await ingestUrl(url); setStatus({ kind: "ok", msg: r.unchanged ? `✓ Already indexed (unchanged): ${r.title || r.url}` : `✓ Indexed “${r.title || r.url}” — ${r.chunks} chunk(s).`, }); setRecent((prev) => [{ name: r.title || url, docs: r.documents, chunks: r.chunks, skipped: r.skipped || 0, ok: true }, ...prev].slice(0, 5)); setUrlValue(""); setUrlOpen(false); await refresh(); } catch (err) { setStatus({ kind: "err", msg: `✗ ${(err as Error).message}` }); } finally { setBusy(false); } }, [urlValue, busy, refresh]); const openUrl = useCallback(() => { setUrlOpen(true); setTimeout(() => urlInputRef.current?.focus(), 50); }, []); const failed = summary?.failed_files || []; const docs: DocumentMeta[] = (summary?.document_titles || []).map((title, i) => ({ doc_id: `doc_${i}`, source: title, title: displaySource(title), source_type: "unknown", })); return (
fileInputRef.current?.click()} onWebUrl={openUrl} /> {/* Web URL input */} {urlOpen && (
setUrlValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void submitUrl(); if (e.key === "Escape") setUrlOpen(false); }} placeholder="https://example.com/article" disabled={busy} className="min-w-0 flex-1 rounded-lg border border-edge bg-panel2 px-3 py-1.5 text-sm text-fg placeholder:text-fg3 focus:border-brand/50 focus:outline-none disabled:opacity-60" />

The page's main content is extracted and indexed with its URL as the citation source. Private/internal hosts are blocked.

)} {/* corpus stats */} {summary && (

Corpus

{summary.indexed && ( )}
{summary.indexed_document_count}
docs
{summary.vector_count}
vectors
{summary.entity_count}
entities

Last indexed: {timeAgo(summary.last_indexed)}

{/* indexed document list */} {summary.document_titles.length > 0 && (

Indexed documents

    {summary.document_titles.slice(0, 8).map((title) => (
  • 📄 {displaySource(title)} {(summary.source_types as Record)?.pdf ? "pdf" : ""}
  • ))} {summary.document_titles.length > 8 && (
  • … {summary.document_titles.length - 8} more
  • )}
)} {/* quick delete actions */} {summary.indexed && (
)} {!summary.indexed && (

No documents indexed yet. Upload a file to begin.

)}
)} {/* visual grounding status */} {groundingSummary && (

Visual Grounding

{groundingSummary.enabled ? "enabled" : "disabled"}
{groundingSummary.total_docs > 0 && (
{groundingSummary.total_docs}
total
{groundingSummary.grounded_docs}
grounded
0 ? "text-warn" : "text-ok"}`}> {groundingSummary.needs_reindex}
need reindex
)} {groundingSummary.needs_reindex > 0 && (
{showReindexInfo && (

How to enable visual grounding

{groundingSummary.needs_reindex} document {groundingSummary.needs_reindex !== 1 ? "s were" : " was"} indexed before visual grounding metadata was available.

Re-upload those documents above — the indexer will automatically attach bounding-box and page metadata so citations show highlighted source regions.

)}
)} {groundingSummary.total_docs === 0 && (

Upload a PDF to enable source highlights with bounding-box grounding.

)}
)} {/* drag-and-drop zone */} {/* indexing progress */} {busy && (
)} {/* supported types */}

Supported file types

{TYPES.map((t) => ( {t} ))}
{status && (

{status.msg}

)} {/* recent uploads */} {recent.length > 0 && (

Recent uploads

    {recent.map((r, i) => (
  • {r.ok ? "✓" : "✕"} {displaySource(r.name)} {r.ok ? `${r.docs}d · ${r.chunks}c${r.skipped ? ` · ${r.skipped} skip` : ""}` : "failed"}
  • ))}
)} {/* failed ingestion files (from corpus) */} {failed.length > 0 && (

Failed to ingest ({failed.length})

    {failed.slice(0, 5).map((f) => (
  • ⚠ {displaySource(f)}
  • ))}
)} {/* sample questions the current corpus can actually answer */} {samples.length > 0 && (

Try these on the current corpus

{samples.map((q) => ( ))}
)} {/* Corpus manage modal */} {showManage && ( { setShowManage(false); setManageAction(undefined); }} onDeleted={() => { void refresh(); setShowManage(false); setManageAction(undefined); onDeleted?.(); }} initialAction={manageAction} /> )}
); }