"use client"; import { useState, useCallback, useRef } from "react"; import type { VisualGrounding, VisualHighlight, ClaimGrounding } from "@/lib/api"; import { API_BASE } from "@/lib/api"; // Citation color palette (matches citation markers 1-8) const CITATION_COLORS = [ "rgba(59,130,246,0.35)", // blue [1] "rgba(16,185,129,0.35)", // green [2] "rgba(245,158,11,0.35)", // amber [3] "rgba(239,68,68,0.35)", // red [4] "rgba(139,92,246,0.35)", // purple[5] "rgba(236,72,153,0.35)", // pink [6] "rgba(14,165,233,0.35)", // sky [7] "rgba(251,191,36,0.35)", // yellow[8] ]; const CITATION_BORDERS = [ "rgb(59,130,246)", "rgb(16,185,129)", "rgb(245,158,11)", "rgb(239,68,68)", "rgb(139,92,246)", "rgb(236,72,153)", "rgb(14,165,233)", "rgb(251,191,36)", ]; function colorFor(idx: number) { return CITATION_COLORS[(idx - 1) % CITATION_COLORS.length]; } function borderFor(idx: number) { return CITATION_BORDERS[(idx - 1) % CITATION_BORDERS.length]; } const SUPPORT_CLS: Record = { supported: "text-ok border-ok/40 bg-ok/5", partial: "text-warn border-warn/40 bg-warn/5", weak: "text-warn border-warn/40 bg-warn/5", unsupported: "text-bad border-bad/40 bg-bad/5", }; const SUPPORT_LABEL: Record = { supported: "Supported", partial: "Partial", weak: "Weak", unsupported: "Unsupported", }; interface HighlightBoxProps { ev: VisualHighlight; containerW: number; containerH: number; hovered: string | null; onHover: (id: string | null) => void; } function HighlightBox({ ev, containerW, containerH, hovered, onHover }: HighlightBoxProps) { if (!ev.normalized_bbox) return null; const [x0, y0, x1, y1] = ev.normalized_bbox; const left = `${(x0 * 100).toFixed(2)}%`; const top = `${(y0 * 100).toFixed(2)}%`; const width = `${((x1 - x0) * 100).toFixed(2)}%`; const height = `${((y1 - y0) * 100).toFixed(2)}%`; const isHovered = hovered === ev.citation_id; const color = colorFor(ev.color_index); const border = borderFor(ev.color_index); return (
onHover(ev.citation_id)} onMouseLeave={() => onHover(null)} title={`[${ev.citation_id}] ${ev.snippet || ev.block_type} · ${(ev.relevance * 100).toFixed(0)}% relevance`} > [{ev.citation_id}]
); } interface PageViewerProps { docId: string; page: number; highlights: VisualHighlight[]; hoveredCitation: string | null; onHoverCitation: (id: string | null) => void; zoom: number; } function PageViewer({ docId, page, highlights, hoveredCitation, onHoverCitation, zoom }: PageViewerProps) { const [imgLoaded, setImgLoaded] = useState(false); const [imgError, setImgError] = useState(false); const imgRef = useRef(null); const imgUrl = `${API_BASE}/documents/${encodeURIComponent(docId)}/pages/${page}/image`; const pageHighlights = highlights.filter( h => h.page === page && h.normalized_bbox && h.support_type !== "unavailable" ); return (
{imgError ? (

Page image not available

This document may need reindexing (reindex_required) to generate source page images.
Use the Ingest panel to reindex and enable source highlights.

) : ( <> {`Page setImgLoaded(true)} onError={() => setImgError(true)} /> {imgLoaded && pageHighlights.map((ev, i) => ( ))} {!imgLoaded && !imgError && (
Loading page…
)} )}
); } interface ClaimGroundingViewProps { claims: ClaimGrounding[]; onClickCitation: (cid: string) => void; } function ClaimGroundingView({ claims, onClickCitation }: ClaimGroundingViewProps) { if (!claims.length) return null; return (

Claim grounding

{claims.map(cg => (
[{SUPPORT_LABEL[cg.support_status] || cg.support_status}] {cg.text.length > 100 ? cg.text.slice(0, 100) + "…" : cg.text} {cg.citation_ids.length > 0 && ( {cg.citation_ids.map(cid => ( ))} )}
))}
); } interface SourceViewPanelProps { grounding: VisualGrounding | null | undefined; activeCitation: string | null; onClose?: () => void; onOpenWorkspace?: () => void; } export function SourceViewPanel({ grounding, activeCitation, onClose, onOpenWorkspace }: SourceViewPanelProps) { const [zoom, setZoom] = useState(1.0); const [hoveredCitation, setHoveredCitation] = useState(activeCitation); const [showGrounding, setShowGrounding] = useState(false); const highlights = grounding?.highlights ?? []; const claims = grounding?.claim_grounding ?? []; const warnings = grounding?.warnings ?? []; // Group highlights by (doc_id, page) const pageGroups = new Map(); for (const h of highlights) { if (!h.doc_id || h.page == null) continue; const key = `${h.doc_id}:${h.page}`; if (!pageGroups.has(key)) { pageGroups.set(key, { docId: h.doc_id, page: h.page, highlights: [] }); } pageGroups.get(key)!.highlights.push(h); } const pages = Array.from(pageGroups.values()); const handleCitationClick = useCallback((cid: string) => { setHoveredCitation(cid); }, []); if (!grounding) { return (
📄

Ask a question to see source grounding.

Source View shows the original PDF page with highlighted evidence regions linked to your answer citations.

); } if (!grounding.visual_grounding_available) { return (
🔍

Visual grounding not available

{warnings.length > 0 ? (
{warnings.map((w, i) => (

{w}

))}
) : (

This document may have been indexed without visual grounding metadata. Reindex to enable source highlights.

)}
); } return (
{/* Header controls */}
Source View {grounding.grounding_stage}
{onOpenWorkspace && ( )} {Math.round(zoom * 100)}%
{/* Citation legend */} {highlights.length > 0 && (
{Array.from(new Set(highlights.map(h => h.color_index))).sort().map(idx => ( ))}
)} {/* Warnings */} {warnings.map((w, i) => (

{w}

))} {/* Claim grounding */} {showGrounding && ( )} {/* Page viewers */}
{pages.length === 0 ? (
No source pages to display for this answer.
) : ( pages.map(({ docId, page, highlights: ph }) => (
Page {page} · {ph.filter(h => h.normalized_bbox).length} highlight(s)
h.page === page)} hoveredCitation={hoveredCitation} onHoverCitation={setHoveredCitation} zoom={zoom} />
)) )}

{grounding.grounding_stage === "span" ? "Exact text spans highlighted from PDF layout extraction." : grounding.grounding_stage === "page" ? "Page-level grounding: exact span unavailable; full page shown." : "Visual grounding experimental. Text grounding is the default."}

); }