iris-ir-platform / src /components /layout /RightEvidencePanel.tsx
rajvivan's picture
sync: push iris-ir-platform to HuggingFace Space
1775b97 verified
Raw
History Blame Contribute Delete
19.6 kB
'use client';
import React from 'react';
import { MOCK_DOCUMENTS } from '@/lib/mockData';
import { type Document, getPageImageUrl } from '@/lib/api';
interface RightEvidencePanelProps {
open: boolean;
docId?: string;
docName?: string;
filename?: string;
page?: number;
onClose: () => void;
totalPages?: number;
// Document Library Props
documents: Document[];
selectedDocIds: string[];
onToggleDoc: (docId: string) => void;
activeDocId?: string;
onSelectDoc?: (docId: string) => void;
onOpenPage?: (docName: string, page: number, docId: string, filename: string) => void;
}
export function RightEvidencePanel({
open,
docId,
docName,
filename,
page = 1,
onClose,
totalPages = 36,
documents,
selectedDocIds,
onToggleDoc,
activeDocId,
onSelectDoc,
onOpenPage,
}: RightEvidencePanelProps) {
const [search, setSearch] = React.useState('');
const [zoom, setZoom] = React.useState(1.0);
const [fullscreenOpen, setFullscreenOpen] = React.useState(false);
const [currentPage, setCurrentPage] = React.useState(page);
const [pageInputValue, setPageInputValue] = React.useState(String(page));
const [imageError, setImageError] = React.useState(false);
const viewerContainerRef = React.useRef<HTMLDivElement>(null);
// Sync currentPage with incoming page prop
React.useEffect(() => {
React.startTransition(() => {
setCurrentPage(page);
setPageInputValue(String(page));
});
}, [page]);
// Reset image error on imageUrl change
React.useEffect(() => {
React.startTransition(() => {
setImageError(false);
});
}, [page, docId]);
if (!open) return null;
// Construct slide image URL through the backend resolver.
const imageUrl = docId
? getPageImageUrl(docId, currentPage)
: `/mock-page.png`;
const handleZoomIn = () => setZoom(prev => Math.min(prev + 0.15, 2.0));
const handleZoomOut = () => setZoom(prev => Math.max(prev - 0.15, 0.5));
const handleResetViewer = () => {
const resetPage = Math.max(1, Math.min(page, totalPages));
setZoom(1.0);
setCurrentPage(resetPage);
setPageInputValue(String(resetPage));
setImageError(false);
if (viewerContainerRef.current) {
viewerContainerRef.current.scrollTop = 0;
viewerContainerRef.current.scrollLeft = 0;
}
};
const handlePageChange = (pageNum: number) => {
const validPage = Math.max(1, Math.min(pageNum, totalPages));
setCurrentPage(validPage);
setPageInputValue(String(validPage));
};
const handlePageInputSubmit = (e: React.FormEvent) => {
e.preventDefault();
const val = parseInt(pageInputValue);
if (!isNaN(val)) {
handlePageChange(val);
}
};
const handleOpenFullScreen = () => {
setFullscreenOpen(true);
};
const normalizedDocs: Document[] = documents.length > 0 ? documents : MOCK_DOCUMENTS.map(d => ({
doc_id: d.id,
name: d.name,
doc_type: d.type,
period: d.period,
total_pages: d.pages,
status: d.status,
filename: d.filename,
}));
const filtered = normalizedDocs.filter(d =>
d.name.toLowerCase().includes(search.toLowerCase()) ||
d.doc_type.toLowerCase().includes(search.toLowerCase())
);
return (
<aside
className="app-right"
style={{
display: 'flex',
flexDirection: 'column',
width: 'var(--right-panel)',
borderLeft: '1px solid var(--border-strong)',
}}
>
{/* Header */}
<div className="pdf-panel-header" style={{ padding: '0.75rem 1rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ minWidth: 0, flex: 1 }}>
<span style={{ fontSize: '0.75rem', fontWeight: 800, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--enbd-blue)' }}>
Source Workspace & Library
</span>
<div style={{ fontSize: '0.62rem', color: 'var(--text-disabled)', marginTop: '2px' }}>
Document browser and evidence tools
</div>
</div>
<button
onClick={onClose}
id="close-pdf-panel-btn"
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
color: 'var(--text-muted)',
padding: '0.25rem',
borderRadius: 'var(--radius-sm)',
display: 'flex',
alignItems: 'center',
}}
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
{/* PART A: Document Library Section (Top) */}
<div style={{
background: 'var(--bg-mid)',
borderBottom: '2px solid var(--enbd-blue-border)',
display: 'flex',
flexDirection: 'column',
maxHeight: '40%',
}}>
{/* Search & Counter */}
<div style={{
padding: '0.65rem 0.875rem',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
borderBottom: '1px solid var(--border-subtle)',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '0.4rem',
background: 'var(--bg-input)',
border: '1px solid var(--border-mid)',
borderRadius: 'var(--radius-sm)',
padding: '0.3rem 0.5rem',
flex: 1,
}}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="2.5">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search library documents…"
style={{
background: 'none',
border: 'none',
outline: 'none',
fontSize: '0.72rem',
color: 'var(--text-primary)',
width: '100%',
}}
/>
</div>
<span className="badge badge-gold" style={{ fontSize: '0.62rem', padding: '0.15rem 0.4rem' }}>
{normalizedDocs.length} Docs
</span>
</div>
{/* Tree List Container */}
<div style={{ overflowY: 'auto', padding: '0.5rem 0.875rem' }}>
{filtered.map(doc => {
const isSelected = selectedDocIds.includes(doc.doc_id);
const isIndexed = doc.status === 'indexed';
const isIndexing = doc.status === 'indexing';
const isError = doc.status === 'error';
return (
<div
key={doc.doc_id}
style={{
display: 'flex',
flexDirection: 'column',
borderBottom: '1px solid var(--border-subtle)',
padding: '0.35rem 0',
}}
>
{/* Header Row */}
<div
className={`doc-item ${activeDocId === doc.doc_id ? 'active' : ''}`}
onClick={() => {
if (isIndexed) {
onSelectDoc?.(doc.doc_id);
onOpenPage?.(doc.name, 1, doc.doc_id, doc.filename);
}
}}
style={{
display: 'flex',
alignItems: 'center',
cursor: isIndexed ? 'pointer' : 'default',
opacity: isIndexed ? 1 : 0.75,
padding: '0.35rem 0.4rem',
borderRadius: 'var(--radius-sm)',
}}
>
{/* Scope target checkbox */}
<div
onClick={(e) => {
e.stopPropagation();
if (isIndexed) {
onToggleDoc?.(doc.doc_id);
}
}}
style={{
display: 'flex',
alignItems: 'center',
marginRight: '0.5rem',
}}
>
<input
type="checkbox"
checked={isSelected}
disabled={!isIndexed}
readOnly
style={{
cursor: isIndexed ? 'pointer' : 'not-allowed',
accentColor: 'var(--enbd-blue)',
}}
/>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: isSelected ? 600 : 400, fontSize: '0.76rem', color: 'var(--text-primary)' }}>
📄 {doc.name}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginTop: '0.15rem' }}>
<span className="badge badge-info" style={{ fontSize: '0.58rem' }}>{doc.doc_type}</span>
{doc.period && doc.period !== 'Unknown' && (
<span style={{ fontSize: '0.62rem', color: 'var(--text-disabled)' }}>{doc.period}</span>
)}
{isIndexing && (
<span style={{ fontSize: '0.62rem', color: 'var(--enbd-blue)', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '0.2rem' }}>
<span style={{ width: 6, height: 6, border: '1.2px solid var(--enbd-blue)', borderTopColor: 'transparent', borderRadius: '50%', display: 'inline-block', animation: 'spin 1s linear infinite' }}/>
Indexing...
</span>
)}
{isError && <span style={{ fontSize: '0.62rem', color: 'var(--danger)', fontWeight: 600 }}>Error</span>}
</div>
</div>
</div>
</div>
);
})}
{filtered.length === 0 && (
<div style={{ padding: '1rem 0', textAlign: 'center', color: 'var(--text-disabled)', fontSize: '0.72rem' }}>
No documents found
</div>
)}
</div>
</div>
{/* PART B: PDF Page Viewer Section (Bottom) */}
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
background: '#ECEFF1',
}}>
{docId ? (
<>
{/* Toolbar */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0.4rem 0.875rem',
borderBottom: '1px solid var(--border-subtle)',
background: 'var(--bg-high)',
flexWrap: 'wrap',
gap: '0.35rem',
}}>
{/* Page jump */}
<form onSubmit={handlePageInputSubmit} style={{ display: 'flex', alignItems: 'center', gap: '0.2rem' }}>
<button
type="button"
disabled={currentPage <= 1}
onClick={() => handlePageChange(currentPage - 1)}
style={{ background: 'none', border: 'none', cursor: currentPage <= 1 ? 'not-allowed' : 'pointer', fontSize: '0.75rem', color: 'var(--text-secondary)' }}
>
</button>
<input
value={pageInputValue}
onChange={e => setPageInputValue(e.target.value)}
style={{
width: '30px',
textAlign: 'center',
border: '1px solid var(--border-mid)',
borderRadius: 'var(--radius-sm)',
fontSize: '0.7rem',
padding: '0.1rem',
background: 'var(--bg-card)',
color: 'var(--text-primary)',
}}
/>
<span style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>/ {totalPages}</span>
<button
type="button"
disabled={currentPage >= totalPages}
onClick={() => handlePageChange(currentPage + 1)}
style={{ background: 'none', border: 'none', cursor: currentPage >= totalPages ? 'not-allowed' : 'pointer', fontSize: '0.75rem', color: 'var(--text-secondary)' }}
>
</button>
</form>
{/* PDF Actions & Zoom */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem' }}>
<button onClick={handleZoomOut} style={{ padding: '0.15rem 0.35rem', border: '1px solid var(--border-mid)', borderRadius: 'var(--radius-sm)', background: 'var(--bg-card)', cursor: 'pointer', fontSize: '0.65rem' }}></button>
<span style={{ fontSize: '0.65rem', minWidth: '25px', textAlign: 'center', color: 'var(--text-secondary)', fontFamily: 'var(--font-mono)' }}>{Math.round(zoom * 100)}%</span>
<button onClick={handleZoomIn} style={{ padding: '0.15rem 0.35rem', border: '1px solid var(--border-mid)', borderRadius: 'var(--radius-sm)', background: 'var(--bg-card)', cursor: 'pointer', fontSize: '0.65rem' }}></button>
<button onClick={handleResetViewer} style={{ padding: '0.15rem 0.35rem', border: '1px solid var(--border-mid)', borderRadius: 'var(--radius-sm)', background: 'var(--bg-card)', cursor: 'pointer', fontSize: '0.62rem' }}>Reset</button>
<button
onClick={handleOpenFullScreen}
style={{
padding: '0.15rem 0.35rem',
border: '1px solid var(--border-mid)',
borderRadius: 'var(--radius-sm)',
background: 'var(--bg-card)',
cursor: 'pointer',
fontSize: '0.62rem',
color: 'var(--text-secondary)',
}}
>
📸 Screen
</button>
</div>
</div>
{/* Viewer Screen Canvas */}
<div
ref={viewerContainerRef}
style={{
flex: 1,
overflow: 'auto',
padding: '1rem',
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
position: 'relative',
}}
>
<div style={{
position: 'relative',
transform: `scale(${zoom})`,
transformOrigin: 'top center',
transition: 'transform 0.15s ease-out',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
borderRadius: 'var(--radius-sm)',
background: '#FFFFFF',
width: '100%',
maxWidth: '420px',
}}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={imageError ? '/mock-page.png' : imageUrl}
alt="PDF page snapshot"
onError={() => setImageError(true)}
style={{ width: '100%', display: 'block' }}
/>
</div>
</div>
{/* Viewer Footer Filename info */}
<div style={{
padding: '0.45rem 0.875rem',
background: 'var(--bg-mid)',
borderTop: '1px solid var(--border-subtle)',
fontSize: '0.68rem',
color: 'var(--text-muted)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontWeight: 500,
}}>
Open: <span style={{ color: 'var(--enbd-blue)', fontWeight: 600 }}>{filename || docName}</span> (Page {currentPage})
</div>
</>
) : (
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
textAlign: 'center',
color: 'var(--text-disabled)',
}}>
<span style={{ fontSize: '3rem', marginBottom: '0.5rem' }}>📂</span>
<div style={{ fontSize: '0.82rem', fontWeight: 600, color: 'var(--text-muted)', marginBottom: '0.25rem' }}>
No Source Document Selected
</div>
<p style={{ fontSize: '0.72rem', color: 'var(--text-disabled)', maxWidth: '280px', lineHeight: 1.5 }}>
Select a source PDF file or double-click page screenshots in the library above to load the viewer workspace.
</p>
</div>
)}
</div>
{fullscreenOpen && (
<div className="pdf-screen-modal" role="dialog" aria-modal="true" aria-label="Expanded source screen">
<div className="pdf-screen-modal-header">
<div>
<div className="pdf-screen-modal-title">{filename || docName || 'Source document'}</div>
<div className="pdf-screen-modal-meta">Page {currentPage} of {totalPages}</div>
</div>
<form className="pdf-screen-page-nav" onSubmit={handlePageInputSubmit}>
<button
type="button"
disabled={currentPage <= 1}
onClick={() => handlePageChange(currentPage - 1)}
>
Prev
</button>
<input
value={pageInputValue}
onChange={e => setPageInputValue(e.target.value)}
aria-label="Page number"
/>
<span>/ {totalPages}</span>
<button
type="button"
disabled={currentPage >= totalPages}
onClick={() => handlePageChange(currentPage + 1)}
>
Next
</button>
</form>
<div className="pdf-screen-modal-actions">
<button type="button" onClick={handleZoomOut} aria-label="Zoom out"></button>
<span className="pdf-screen-zoom-value">{Math.round(zoom * 100)}%</span>
<button type="button" onClick={handleZoomIn} aria-label="Zoom in">+</button>
<button type="button" onClick={handleResetViewer}>Reset</button>
<button type="button" onClick={() => setFullscreenOpen(false)}>Close</button>
</div>
</div>
<div className="pdf-screen-modal-body" onClick={() => setFullscreenOpen(false)}>
<div
className="pdf-screen-modal-page"
onClick={e => e.stopPropagation()}
style={{ transform: `scale(${zoom})` }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={imageError ? '/mock-page.png' : imageUrl}
alt={`Expanded PDF page ${currentPage}`}
onError={() => setImageError(true)}
/>
</div>
</div>
</div>
)}
</aside>
);
}