import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { Box, CircularProgress, IconButton, Typography, Collapse, Chip, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; import ExpandMore from '@mui/icons-material/ExpandMore'; import ChevronRight from '@mui/icons-material/ChevronRight'; import Description from '@mui/icons-material/Description'; import Check from '@mui/icons-material/Check'; import Schedule from '@mui/icons-material/Schedule'; import Visibility from '@mui/icons-material/Visibility'; import Psychology from '@mui/icons-material/Psychology'; import Overview from '@mui/icons-material/Home'; import { supabase } from '../../../supabaseClient'; // Types type PageImagesManifest = { version: number; file_id: string; page_count: number; bucket?: string; base_dir?: string; page_images: Array<{ page: number; full_image_path: string; thumbnail_path: string; full_dimensions?: { width: number; height: number }; thumbnail_dimensions?: { width: number; height: number }; }> }; type OutlineSection = { id: string; title: string; level: number; start_page: number; end_page: number; parent_id?: string | null; children?: string[]; }; type ProcessingStatus = { tika: boolean; frontmatter: boolean; structure_analysis: boolean; split_map: boolean; page_images: boolean; docling_ocr: boolean; docling_no_ocr: boolean; docling_vlm: boolean; }; type SectionNode = { sec: OutlineSection; children: SectionNode[]; }; interface CCEnhancedFilePanelProps { fileId: string; selectedPage: number; onSelectPage: (page: number) => void; currentSection?: { start: number; end: number }; } export const CCEnhancedFilePanel: React.FC = ({ fileId, selectedPage, onSelectPage, currentSection }) => { // State const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [manifest, setManifest] = useState(null); const [outline, setOutline] = useState([]); const [processingStatus, setProcessingStatus] = useState({ tika: false, frontmatter: false, structure_analysis: false, split_map: false, page_images: false, docling_ocr: false, docling_no_ocr: false, docling_vlm: false }); const [collapsed, setCollapsed] = useState>(() => new Set()); const [selectedView, setSelectedView] = useState<'overview' | 'structure' | 'thumbnails'>('structure'); const [thumbUrls] = useState>(() => new Map()); // Refs for scroll syncing const thumbnailsRef = useRef(null); const API_BASE = useMemo(() => import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'), [] ); // Load data useEffect(() => { const loadData = async () => { if (!fileId) return; setLoading(true); setError(null); try { const token = (await supabase.auth.getSession()).data.session?.access_token || ''; // Load page images manifest const manifestRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(fileId)}/page-images/manifest`, { headers: { 'Authorization': `Bearer ${token}` } }); if (manifestRes.ok) { const m: PageImagesManifest = await manifestRes.json(); setManifest(m); } // Load artefacts to determine processing status and structure const artefactsRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(fileId)}/artefacts`, { headers: { 'Authorization': `Bearer ${token}` } }); if (artefactsRes.ok) { const artefacts: Array<{ id: string; type: string; status: string; extra?: { config?: { do_ocr?: boolean; }; }; }> = await artefactsRes.json(); // Determine processing status const status: ProcessingStatus = { tika: artefacts.some((a) => a.type === 'tika_json' && a.status === 'completed'), frontmatter: artefacts.some((a) => a.type === 'docling_frontmatter_json' && a.status === 'completed'), structure_analysis: artefacts.some((a) => a.type === 'document_outline_hierarchy' && a.status === 'completed'), split_map: artefacts.some((a) => a.type === 'split_map_json' && a.status === 'completed'), page_images: artefacts.some((a) => a.type === 'page_images' && a.status === 'completed'), docling_ocr: artefacts.some((a) => a.type === 'docling_standard' && (a.extra?.config?.do_ocr === true) && a.status === 'completed'), docling_no_ocr: artefacts.some((a) => a.type === 'docling_standard' && (a.extra?.config?.do_ocr === false) && a.status === 'completed'), docling_vlm: artefacts.some((a) => a.type === 'docling_vlm' && a.status === 'completed') }; setProcessingStatus(status); // Load document outline/structure const outlineArt = artefacts.find((a) => a.type === 'document_outline_hierarchy' && a.status === 'completed'); if (outlineArt) { const structureRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(fileId)}/artefacts/${encodeURIComponent(outlineArt.id)}/json`, { headers: { 'Authorization': `Bearer ${token}` } }); if (structureRes.ok) { const structureData = await structureRes.json(); const sections = (structureData.sections || []) as OutlineSection[]; setOutline(sections); } } } } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to load file data'); } finally { setLoading(false); } }; loadData(); }, [fileId, API_BASE]); // Build hierarchical section tree const sectionTree = useMemo(() => { const buildTree = (sections: OutlineSection[]): SectionNode[] => { const roots: SectionNode[] = []; const stack: SectionNode[] = []; const sorted = [...sections].sort((a, b) => a.start_page - b.start_page); for (const sec of sorted) { const level = Math.max(1, Number(sec.level || 1)); const node: SectionNode = { sec, children: [] }; // Maintain proper hierarchy based on level while (stack.length && stack.length >= level) stack.pop(); const parent = stack[stack.length - 1]; if (parent) { parent.children.push(node); } else { roots.push(node); } stack.push(node); } return roots; }; return buildTree(outline); }, [outline]); // Thumbnail fetching with lazy loading const fetchThumbnail = useCallback(async (page: number): Promise => { if (!manifest) return undefined; const cached = thumbUrls.get(page); if (cached) return cached; const pageIndex = Math.max(0, Math.min((manifest.page_count || 1) - 1, page - 1)); const pageInfo = manifest.page_images[pageIndex]; if (!pageInfo) return undefined; try { const url = `${API_BASE}/database/files/proxy?bucket=${encodeURIComponent(manifest.bucket || '')}&path=${encodeURIComponent(pageInfo.thumbnail_path)}`; const token = (await supabase.auth.getSession()).data.session?.access_token || ''; const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); if (!response.ok) return undefined; const blob = await response.blob(); const objectUrl = URL.createObjectURL(blob); thumbUrls.set(page, objectUrl); return objectUrl; } catch { return undefined; } }, [manifest, API_BASE, thumbUrls]); // Render overview panel const renderOverview = () => ( Processing Status Phase 1: Structure Discovery Phase 2: Content Processing } /> } /> } /> {manifest && ( Document Info {manifest.page_count} pages {outline.length > 0 && ( {outline.length} sections identified )} )} ); // Render document structure tree const renderStructureTree = () => ( {sectionTree.length === 0 ? ( {processingStatus.structure_analysis ? 'No document structure detected' : 'Structure analysis pending...'} ) : ( {sectionTree.map((node) => ( { const newCollapsed = new Set(collapsed); if (newCollapsed.has(id)) { newCollapsed.delete(id); } else { newCollapsed.add(id); } setCollapsed(newCollapsed); }} processingStatus={processingStatus} /> ))} )} ); // Render page thumbnails with lazy loading const renderThumbnails = () => { if (!manifest) return null; const pages = Array.from({ length: manifest.page_count }, (_, i) => i + 1); return ( {pages.map((page) => ( = currentSection.start && page <= currentSection.end : false} fetchThumbnail={fetchThumbnail} onSelect={onSelectPage} /> ))} ); }; if (loading) { return ( ); } if (error) { return ( {error} ); } return ( {/* Header with navigation tabs */} setSelectedView('overview')} color={selectedView === 'overview' ? 'primary' : 'default'} title="Processing Overview" > setSelectedView('structure')} color={selectedView === 'structure' ? 'primary' : 'default'} title="Document Structure" > setSelectedView('thumbnails')} color={selectedView === 'thumbnails' ? 'primary' : 'default'} title="Page Thumbnails" > {/* Content based on selected view */} {selectedView === 'overview' && renderOverview()} {selectedView === 'structure' && renderStructureTree()} {selectedView === 'thumbnails' && renderThumbnails()} ); }; // Status indicator component const StatusItem: React.FC<{ label: string; status: boolean; icon?: React.ReactNode; }> = ({ label, status, icon }) => ( {icon || } {label} {status ? ( ) : ( )} ); // Section tree item component const SectionTreeItem: React.FC<{ node: SectionNode; level: number; selectedPage: number; onSelectPage: (page: number) => void; collapsed: Set; onToggleCollapse: (id: string) => void; processingStatus: ProcessingStatus; }> = ({ node, level, selectedPage, onSelectPage, collapsed, onToggleCollapse, processingStatus }) => { const isCollapsed = collapsed.has(node.sec.id); const hasChildren = node.children.length > 0; const isCurrentSection = selectedPage >= node.sec.start_page && selectedPage <= node.sec.end_page; return ( <> onSelectPage(node.sec.start_page)} > {hasChildren ? ( { e.stopPropagation(); onToggleCollapse(node.sec.id); }} > {isCollapsed ? : } ) : ( )} {/* Processing indicators */} {processingStatus.docling_ocr && } {processingStatus.docling_no_ocr && } {processingStatus.docling_vlm && } {hasChildren && !isCollapsed && ( {node.children.map((child) => ( ))} )} ); }; // Lazy loading thumbnail component const LazyThumbnail: React.FC<{ page: number; isSelected: boolean; isInSection: boolean; fetchThumbnail: (page: number) => Promise; onSelect: (page: number) => void; }> = ({ page, isSelected, isInSection, fetchThumbnail, onSelect }) => { const [src, setSrc] = useState(undefined); const [isVisible, setIsVisible] = useState(false); const imgRef = useRef(null); // Intersection observer for lazy loading useEffect(() => { if (!imgRef.current) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }, { threshold: 0.1, rootMargin: '50px' } ); observer.observe(imgRef.current); return () => observer.disconnect(); }, []); // Load thumbnail when visible useEffect(() => { if (isVisible && !src) { fetchThumbnail(page).then(setSrc); } }, [isVisible, page, fetchThumbnail, src]); return ( onSelect(page)} sx={{ aspectRatio: '3/4', border: isSelected ? '2px solid var(--color-primary)' : '1px solid var(--color-divider)', borderRadius: 1, overflow: 'hidden', cursor: 'pointer', position: 'relative', bgcolor: isInSection ? 'var(--color-selected)' : 'var(--color-panel)', '&:hover': { borderColor: 'var(--color-primary-light)' }, transition: 'border-color 0.2s' }} > {src ? ( {`Page ) : isVisible ? ( ) : null} {page} ); }; export default CCEnhancedFilePanel;