AI-PolicyTrace / ui /src /PDFPane.tsx
teja141290's picture
Deploy PolicyTrace Hugging Face Space
be54038
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Document, Page } from 'react-pdf'
import type { FieldProvenance } from './types'
import { useStore } from './store'
import { api } from './api'
interface Props {
sessionId: string
}
export function PDFPane({ sessionId }: Props) {
const sessionData = useStore((s) => s.sessionData)
const activePdfFile = useStore((s) => s.activePdfFile)
const activeProvenance = useStore((s) => s.activeProvenance)
const setActivePdf = useStore((s) => s.setActivePdf)
const [numPages, setNumPages] = useState(0)
const [renderedPages, setRenderedPages] = useState<Set<number>>(new Set())
const [containerWidth, setContainerWidth] = useState(600)
const containerRef = useRef<HTMLDivElement>(null)
const pageRefs = useRef<Map<number, HTMLDivElement>>(new Map())
// Track which PDF URL we last requested a scroll for, to avoid re-firing
const pendingScrollRef = useRef<{ page: number; pdfFile: string } | null>(null)
// Unique PDF filenames from provenance
const pdfFiles = useMemo(() => {
const seen = new Set<string>()
return (sessionData?.provenance ?? [])
.map((p) => p.source_filename)
.filter((f) => { const fresh = !seen.has(f); seen.add(f); return fresh })
}, [sessionData?.provenance])
// Set container width on resize
useEffect(() => {
const el = containerRef.current
if (!el) return
const obs = new ResizeObserver(([entry]) => {
setContainerWidth(Math.floor(entry.contentRect.width) - 24)
})
obs.observe(el)
setContainerWidth(Math.floor(el.clientWidth) - 24)
return () => obs.disconnect()
}, [])
// When active provenance changes: enqueue a scroll request
useEffect(() => {
if (!activeProvenance) return
pendingScrollRef.current = {
page: activeProvenance.location.page,
pdfFile: activeProvenance.source_filename,
}
// Reset rendered-pages set when switching documents
if (activeProvenance.source_filename !== activePdfFile) {
setRenderedPages(new Set())
}
// Try immediately (page already rendered)
tryScroll()
}, [activeProvenance]) // eslint-disable-line react-hooks/exhaustive-deps
// When a page finishes rendering, check if a scroll is pending for it
const handlePageRenderSuccess = useCallback((pageNum: number) => {
setRenderedPages((prev) => new Set([...prev, pageNum]))
const pending = pendingScrollRef.current
if (pending && pending.page === pageNum && pending.pdfFile === activePdfFile) {
const el = pageRefs.current.get(pageNum)
el?.scrollIntoView({ behavior: 'smooth', block: 'center' })
pendingScrollRef.current = null
}
}, [activePdfFile])
function tryScroll() {
const pending = pendingScrollRef.current
if (!pending) return
if (pending.pdfFile !== activePdfFile) return
const el = pageRefs.current.get(pending.page)
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
pendingScrollRef.current = null
}
}
// Reset rendered pages when the PDF URL changes
const pdfUrl = activePdfFile ? api.pdfUrl(sessionId, activePdfFile) : null
const prevPdfUrlRef = useRef<string | null>(null)
if (pdfUrl !== prevPdfUrlRef.current) {
prevPdfUrlRef.current = pdfUrl
// Clear page refs — old page elements are stale after document switch
pageRefs.current.clear()
}
// Highlights for the currently displayed PDF
const highlights = useMemo((): FieldProvenance[] => {
if (!sessionData || !activePdfFile) return []
return sessionData.provenance.filter(
(p) => p.source_filename === activePdfFile,
)
}, [sessionData, activePdfFile])
return (
<div className="flex flex-col h-full">
{/* PDF file selector */}
{pdfFiles.length > 1 && (
<div className="flex flex-wrap gap-2 p-3 border-b flex-shrink-0" style={{ backgroundColor: '#1F2937' }}>
{pdfFiles.map((f) => (
<button
key={f}
onClick={() => setActivePdf(f)}
className="px-3 py-1 rounded text-xs font-medium transition-colors"
style={activePdfFile === f
? { backgroundColor: '#008080', color: '#ffffff' }
: { backgroundColor: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.15)', color: 'rgba(255,255,255,0.7)' }
}
>
{f}
</button>
))}
</div>
)}
{/* PDF scroll area */}
<div
ref={containerRef}
className="flex-1 overflow-y-auto pdf-scroll-container bg-gray-200 p-3 space-y-4"
>
{pdfUrl ? (
<Document
file={pdfUrl}
onLoadSuccess={({ numPages: n }) => {
setNumPages(n)
setRenderedPages(new Set())
}}
loading={<LoadingPlaceholder />}
error={<ErrorPlaceholder />}
>
{Array.from({ length: numPages }, (_, i) => i + 1).map((pageNum) => {
const pageHighlights = highlights.filter(
(h) => h.location.page === pageNum,
)
const hasActive =
activeProvenance?.location.page === pageNum &&
activeProvenance.source_filename === activePdfFile
return (
<div
key={pageNum}
ref={(el) => {
if (el) pageRefs.current.set(pageNum, el)
else pageRefs.current.delete(pageNum)
}}
// Use block + explicit width so the overlay div always matches
// the canvas dimensions exactly (inline-block can shrink-wrap)
style={{ position: 'relative', width: containerWidth }}
className={`rounded shadow-md transition-shadow overflow-hidden ${
hasActive ? 'ring-4 ring-blue-500' : ''
}`}
>
<Page
pageNumber={pageNum}
width={containerWidth}
renderTextLayer={false}
renderAnnotationLayer={false}
onRenderSuccess={() => handlePageRenderSuccess(pageNum)}
/>
{/* Highlight overlay — percentage-based, top-left origin */}
<div
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
aria-hidden
>
{pageHighlights.map((h) => {
const [x0, y0, x1, y1] = h.location.bbox
const isActive = activeProvenance?.field_path === h.field_path
return (
<div
key={h.field_path}
style={{
position: 'absolute',
left: `${x0}%`,
top: `${y0}%`,
width: `${x1 - x0}%`,
height: `${y1 - y0}%`,
background: isActive
? 'rgba(59, 130, 246, 0.35)' /* blue-500 fill */
: 'rgba(134, 239, 172, 0.35)', /* green-300 fill */
border: isActive
? '3px solid rgba(37, 99, 235, 1)' /* blue-700 solid */
: '2px solid rgba(22, 163, 74, 0.9)', /* green-600 */
borderRadius: 3,
boxShadow: isActive
? '0 0 0 2px rgba(147, 197, 253, 0.6)' /* blue glow */
: 'none',
transition: 'background 0.15s, border 0.15s',
}}
title={`${h.field_path}: ${h.extracted_value}`}
/>
)
})}
</div>
</div>
)
})}
</Document>
) : (
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
No PDF selected
</div>
)}
</div>
</div>
)
}
function LoadingPlaceholder() {
return (
<div className="flex items-center justify-center p-12 text-gray-400 text-sm">
Loading PDF…
</div>
)
}
function ErrorPlaceholder() {
return (
<div className="flex items-center justify-center p-12 text-red-400 text-sm">
Failed to load PDF.
</div>
)
}