import React, { useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Box, Button, Divider, FormControlLabel, MenuItem, Select, Switch, TextField, Typography } from '@mui/material'; import { SelectChangeEvent } from '@mui/material/Select'; // import { CCFilesPanel } from '../../../utils/tldraw/ui-overrides/components/shared/CCFilesPanel'; import { CCDoclingViewer } from './CCDoclingViewer.tsx'; import CCEnhancedFilePanel from './CCEnhancedFilePanel.tsx'; import CCBundleViewer from './CCBundleViewer.tsx'; import { useAuth } from '../../../contexts/AuthContext'; type CanonicalDoclingConfig = { pipeline: 'standard' | 'vlm' | 'asr'; pdf_backend: 'dlparse_v4' | 'pypdfium2' | 'dlparse_v1' | 'dlparse_v2'; do_ocr: boolean; force_ocr: boolean; table_mode: 'fast' | 'accurate'; do_picture_classification: boolean; do_picture_description: boolean; picture_description_prompt?: string; // Extended options target_type?: 'inbody' | 'zip'; image_export_mode?: 'placeholder' | 'embedded' | 'referenced'; table_cell_matching?: boolean; picture_description_local?: string; // JSON string per API picture_description_api?: string; // JSON string per API vlm_pipeline_model?: string; vlm_pipeline_model_local?: string; // JSON string per API vlm_pipeline_model_api?: string; // JSON string per API to_formats?: string[]; do_formula_enrichment?: boolean; do_code_enrichment?: boolean; }; type CanonicalDoclingRequest = { use_split_map: boolean; config: CanonicalDoclingConfig; threshold: number; }; type Profile = 'default' | 'simple' | 'aggressive'; type Pipeline = 'standard' | 'vlm' | 'asr'; type PdfBackend = 'dlparse_v4' | 'pypdfium2' | 'dlparse_v1' | 'dlparse_v2'; type TableMode = 'fast' | 'accurate'; export const CCDocumentIntelligence: React.FC = () => { const { fileId } = useParams<{ fileId: string }>(); const validFileId = useMemo(() => fileId || '', [fileId]); const { accessToken } = useAuth(); const [page, setPage] = useState(1); const [outlineOptions, setOutlineOptions] = useState>([]); const [profile, setProfile] = useState('default'); const [pipeline, setPipeline] = useState('standard'); // VLM pipeline config (mutually exclusive options) type VlmMode = 'preset' | 'local' | 'api'; const [vlmMode, setVlmMode] = useState('preset'); const [vlmPreset, setVlmPreset] = useState('smoldocling'); const [vlmLocalJson, setVlmLocalJson] = useState(''); const [vlmApiJson, setVlmApiJson] = useState(''); type VlmProvider = 'ollama' | 'openai' | ''; const [vlmProvider, setVlmProvider] = useState(''); const [vlmProviderModel, setVlmProviderModel] = useState(''); const [vlmProviderBaseUrl, setVlmProviderBaseUrl] = useState(''); const [ollamaModels, setOllamaModels] = useState([]); const [pdfBackend, setPdfBackend] = useState('dlparse_v4'); const [doOCR, setDoOCR] = useState(true); const [forceOCR, setForceOCR] = useState(false); const [tableMode, setTableMode] = useState('fast'); const [doPicClass, setDoPicClass] = useState(false); const [doPicDesc, setDoPicDesc] = useState(false); const [picDescPrompt, setPicDescPrompt] = useState('Describe the image succinctly for study notes.'); // Picture description config (mutually exclusive local/api) type PicDescMode = 'local' | 'api'; const [picDescMode, setPicDescMode] = useState('local'); const [picDescLocalJson, setPicDescLocalJson] = useState(''); const [picDescApiJson, setPicDescApiJson] = useState(''); const [busy, setBusy] = useState(false); // Split sections (from split_map) const [splitSections, setSplitSections] = useState>([]); const [selectedSectionId, setSelectedSectionId] = useState('full'); // Load available canonical bundles type Artefact = { id: string; type: string; rel_path: string; extra?: Record; created_at?: string }; const [bundles, setBundles] = useState([]); const [currentBundle, setCurrentBundle] = useState(''); const [combineSplit, setCombineSplit] = useState(false); // Batch selection (group of split bundles or single bundle) type BundleGroup = { key: string; label: string; bundleIds: string[]; isGroup: boolean }; const groupItems = useMemo(() => { if (!bundles.length) return []; const byGroup: Record = {}; const singles: BundleGroup[] = []; for (const b of bundles) { const ex = (b.extra as Record) || {}; const gid = (ex.group_id as string | undefined) || ''; if (gid) { if (!byGroup[gid]) byGroup[gid] = { ids: [], meta: [] }; byGroup[gid].ids.push(b.id); const pipeline = (ex.pipeline as string | undefined) || (b.type === 'docling_vlm' ? 'vlm' : (b.type === 'vlm_section_page_bundle' ? 'vlm-pages' : 'standard')); const producer = (ex.producer as string | undefined) || 'manual'; const do_ocr = ((ex.config as Record)?.do_ocr as boolean) ?? true; const ocrLabel = do_ocr ? 'OCR' : 'no-OCR'; byGroup[gid].meta.push({ created_at: b.created_at, pipeline, group_pack_type: ex.group_pack_type as string | undefined, producer, ocr_mode: ocrLabel, processing_mode: ex.processing_mode as string | undefined, bundle_type: ex.bundle_type as string | undefined }); } else { const pipeline = (ex.pipeline as string | undefined) || (b.type === 'docling_vlm' ? 'vlm' : (b.type === 'vlm_section_page_bundle' ? 'vlm-pages' : 'standard')); const producer = (ex.producer as string | undefined) || 'manual'; const producerLabel = producer === 'auto_split' ? 'auto' : 'manual'; singles.push({ key: `single:${b.id}`, label: `${new Date(b.created_at || '').toLocaleString()} • ${pipeline} • ${producerLabel}`, bundleIds: [b.id], isGroup: false }); } } const groups: BundleGroup[] = Object.entries(byGroup) .map(([gid, v]) => { const newest = v.meta.sort((a,b)=> new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())[0]; const producerLabel = newest.producer === 'auto_split' ? 'auto' : 'manual'; // Determine pack type - use processing_mode as fallback for better detection let packType = newest.group_pack_type; if (!packType) { // Smart fallback based on bundle characteristics if (newest.processing_mode === 'whole_document' || newest.bundle_type === 'docling_bundle') { packType = 'whole'; } else if (v.ids.length === 1) { packType = 'single'; } else { packType = 'split'; } } const ocrInfo = v.meta.length > 0 ? `${newest.ocr_mode || 'mixed'}` : ''; const label = `${new Date(newest.created_at || '').toLocaleString()} • ${packType} • ${newest.pipeline || 'standard'} • ${ocrInfo} • ${v.ids.length} parts • ${producerLabel}`; return { key: `group:${gid}`, label, bundleIds: v.ids, isGroup: v.ids.length > 1 }; }) .sort((a,b)=> new Date(byGroup[b.key.split(':')[1]]?.meta[0]?.created_at || 0).getTime() - new Date(byGroup[a.key.split(':')[1]]?.meta[0]?.created_at || 0).getTime()); return [...groups, ...singles]; }, [bundles]); const [selectedGroupKey, setSelectedGroupKey] = useState(''); useEffect(() => { const loadBundles = async () => { if (!validFileId) return; const API_BASE = import.meta.env.VITE_API_BASE || '/api'; const token = accessToken || ''; const res = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts`, { headers: { Authorization: `Bearer ${token}` } }); if (!res.ok) return; const arts: Artefact[] = await res.json(); const list = arts.filter(a => a.type === 'docling_standard' || a.type === 'docling_vlm' || a.type === 'vlm_section_page_bundle' || a.type === 'docling_bundle' || a.type === 'docling_bundle_split' || a.type === 'docling_bundle_split_pages' || a.type === 'canonical_docling_json') .sort((a, b) => { // Sort by creation time, newest first const ta = new Date(a.created_at || 0).getTime(); const tb = new Date(b.created_at || 0).getTime(); return tb - ta; }); setBundles(list); // Initialize currentBundle if not set if (list.length && !currentBundle) { setCurrentBundle(list[0].id); } // Initialize selected group key to latest group or single const gi = (() => { const arr = list; const withGroup = arr.filter(a => ((a.extra as Record)||{}).group_id); if (withGroup.length) { const gid = ((withGroup[0].extra as Record).group_id as string); return `group:${gid}`; } return `single:${arr[0]?.id || ''}`; })(); if (!selectedGroupKey && gi) { setSelectedGroupKey(gi); } }; loadBundles(); }, [validFileId]); // Remove circular dependencies to prevent timing issues // eslint-disable-next-line react-hooks/exhaustive-deps // Separate effect to handle initialization after bundles are loaded useEffect(() => { if (bundles.length > 0 && !currentBundle) { setCurrentBundle(bundles[0].id); } }, [bundles, currentBundle]); // Separate effect to sync selectedGroupKey with currentBundle useEffect(() => { if (bundles.length > 0 && currentBundle && !selectedGroupKey) { const bundle = bundles.find(b => b.id === currentBundle); if (bundle) { const extra = bundle.extra as Record || {}; const groupId = extra.group_id as string; if (groupId) { setSelectedGroupKey(`group:${groupId}`); } else { setSelectedGroupKey(`single:${currentBundle}`); } } } }, [bundles, currentBundle, selectedGroupKey]); const [splitThreshold] = useState(50); const autoSplit = useMemo(() => { const pages = splitSections.reduce((m, s) => Math.max(m, s.end), 0); return pages >= splitThreshold && splitSections.length > 0; }, [splitSections, splitThreshold]); const [doFormula, setDoFormula] = useState(false); const [doCode, setDoCode] = useState(false); const [tableCellMatching, setTableCellMatching] = useState(false); // Outputs are fixed to all formats for canonical bundles useEffect(() => { const run = async () => { if (!validFileId) return; setOutlineOptions([]); const API_BASE = import.meta.env.VITE_API_BASE || '/api'; try { const artsRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts`, { headers: { 'Authorization': `Bearer ${accessToken || ''}` } }); if (!artsRes.ok) return; const arts: Array<{ id: string; type: string; rel_path?: string }> = await artsRes.json(); const outlineArt = arts.find(a => a.type === 'document_outline_hierarchy'); if (!outlineArt) return; const jsonRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts/${encodeURIComponent(outlineArt.id)}/json`, { headers: { 'Authorization': `Bearer ${accessToken || ''}` } }); if (!jsonRes.ok) return; const doc = await jsonRes.json(); const sections = (doc.sections || []) as Array<{ id: string; title: string; start_page: number; end_page: number }>; setOutlineOptions(sections.map(s => ({ id: s.id, title: s.title, start_page: s.start_page, end_page: s.end_page }))); // Load split map const splitArt = arts.find(a => a.type === 'split_map_json'); if (splitArt) { const smRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts/${encodeURIComponent(splitArt.id)}/json`, { headers: { 'Authorization': `Bearer ${accessToken || ''}` } }); if (smRes.ok) { const sm = await smRes.json(); const entries = Array.isArray(sm.entries) ? sm.entries : []; const secs = (entries as Array>) .map((e) => ({ id: String((e.id as string) || `${e.start_page as number}-${e.end_page as number}`), title: String((e.title as string) || ''), start: Number((e.start_page as number) || 1), end: Number((e.end_page as number) || 1) })) .filter((e) => Number.isFinite(e.start) && Number.isFinite(e.end)); setSplitSections(secs); } } } catch { // ignore } }; run(); }, [validFileId]); return ( a.start_page-b.start_page).find(x => page >= x.start_page && page <= x.end_page); return s ? { start: s.start_page, end: s.end_page } : undefined; })()} /> a.start_page-b.start_page).find(x => page >= x.start_page && page <= x.end_page); return s ? { start: s.start_page, end: s.end_page } : undefined; })()} /> g.key === selectedGroupKey); if (!grp) return []; // Order split parts by split_order if present const inGroup = bundles.filter(b => grp.bundleIds.includes(b.id)); const ordered = inGroup.sort((a,b) => { const ao = Number(((a.extra as Record)||{}).split_order) || 0; const bo = Number(((b.extra as Record)||{}).split_order) || 0; return ao - bo; }); return ordered.map(b => ({ id: b.id })); })() : undefined} /> AI Document Intelligence Canonical Docling {bundles.length > 0 && ( <> Existing bundles {/* Batch selector (groups and singles) */} {/* Only show combine toggle if multi-bundle group selected */} {(() => { const grp = groupItems.find(g => g.key === selectedGroupKey); return grp && grp.isGroup ? ( setCombineSplit(e.target.checked)} />} label="Combine split bundles" /> ) : null; })()} {/* When not combining, allow selecting a single bundle within selected group */} {!combineSplit && (() => { const grp = groupItems.find(g => g.key === selectedGroupKey); return grp && grp.isGroup; // Only show for groups with multiple bundles })() && ( )} )} Pipeline {pipeline === 'vlm' && ( <> VLM configuration {vlmMode === 'preset' && ( )} {vlmMode === 'local' && ( ) => setVlmLocalJson(e.target.value)} multiline minRows={2} /> )} {vlmMode === 'api' && ( <> {vlmProvider === 'ollama' && ( <> ) => setVlmProviderBaseUrl(e.target.value)} /> )} {vlmProvider === 'openai' && ( <> ) => setVlmProviderBaseUrl(e.target.value)} /> )} {vlmProvider === '' && ( ) => setVlmApiJson(e.target.value)} multiline minRows={2} /> )} )} )} PDF Backend setDoOCR(e.target.checked)} />} label="OCR" /> setForceOCR(e.target.checked)} />} label="Force OCR" /> Table Mode Section setDoPicClass(e.target.checked)} />} label="Picture classification" /> setDoPicDesc(e.target.checked)} />} label="Picture description" /> {doPicDesc && ( <> ) => setPicDescPrompt(e.target.value)} /> Picture description configuration {picDescMode === 'local' && ( ) => setPicDescLocalJson(e.target.value)} multiline minRows={2} /> )} {picDescMode === 'api' && ( ) => setPicDescApiJson(e.target.value)} multiline minRows={2} /> )} )} setDoFormula(e.target.checked)} />} label="Formula enrichment" /> setDoCode(e.target.checked)} />} label="Code enrichment" /> setTableCellMatching(e.target.checked)} />} label="Table cell matching" /> {/* Outputs are always all formats for canonical bundles; UI omitted */} ); }; export default CCDocumentIntelligence;