Files
app/src/pages/tldraw/CCDocumentIntelligence/CCDocumentIntelligence.tsx
T
kcar fedbd903ff
app-ci-deploy / test-build-deploy (push) Has been cancelled
fix: centralize app API URL fallbacks
2026-05-28 19:26:00 +01:00

607 lines
34 KiB
TypeScript

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<number>(1);
const [outlineOptions, setOutlineOptions] = useState<Array<{ id: string; title: string; start_page: number; end_page: number }>>([]);
const [profile, setProfile] = useState<Profile>('default');
const [pipeline, setPipeline] = useState<Pipeline>('standard');
// VLM pipeline config (mutually exclusive options)
type VlmMode = 'preset' | 'local' | 'api';
const [vlmMode, setVlmMode] = useState<VlmMode>('preset');
const [vlmPreset, setVlmPreset] = useState<string>('smoldocling');
const [vlmLocalJson, setVlmLocalJson] = useState<string>('');
const [vlmApiJson, setVlmApiJson] = useState<string>('');
type VlmProvider = 'ollama' | 'openai' | '';
const [vlmProvider, setVlmProvider] = useState<VlmProvider>('');
const [vlmProviderModel, setVlmProviderModel] = useState<string>('');
const [vlmProviderBaseUrl, setVlmProviderBaseUrl] = useState<string>('');
const [ollamaModels, setOllamaModels] = useState<string[]>([]);
const [pdfBackend, setPdfBackend] = useState<PdfBackend>('dlparse_v4');
const [doOCR, setDoOCR] = useState(true);
const [forceOCR, setForceOCR] = useState(false);
const [tableMode, setTableMode] = useState<TableMode>('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<PicDescMode>('local');
const [picDescLocalJson, setPicDescLocalJson] = useState<string>('');
const [picDescApiJson, setPicDescApiJson] = useState<string>('');
const [busy, setBusy] = useState(false);
// Split sections (from split_map)
const [splitSections, setSplitSections] = useState<Array<{ id: string; title: string; start: number; end: number }>>([]);
const [selectedSectionId, setSelectedSectionId] = useState<string>('full');
// Load available canonical bundles
type Artefact = { id: string; type: string; rel_path: string; extra?: Record<string, unknown>; created_at?: string };
const [bundles, setBundles] = useState<Artefact[]>([]);
const [currentBundle, setCurrentBundle] = useState<string>('');
const [combineSplit, setCombineSplit] = useState<boolean>(false);
// Batch selection (group of split bundles or single bundle)
type BundleGroup = { key: string; label: string; bundleIds: string[]; isGroup: boolean };
const groupItems = useMemo<BundleGroup[]>(() => {
if (!bundles.length) return [];
const byGroup: Record<string, { ids: string[]; meta: { created_at?: string; pipeline?: string; group_pack_type?: string; producer?: string; ocr_mode?: string; processing_mode?: string; bundle_type?: string }[] }> = {};
const singles: BundleGroup[] = [];
for (const b of bundles) {
const ex = (b.extra as Record<string, unknown>) || {};
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<string, unknown>)?.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<string>('');
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<string, unknown>)||{}).group_id);
if (withGroup.length) {
const gid = ((withGroup[0].extra as Record<string, unknown>).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<string, unknown> || {};
const groupId = extra.group_id as string;
if (groupId) {
setSelectedGroupKey(`group:${groupId}`);
} else {
setSelectedGroupKey(`single:${currentBundle}`);
}
}
}
}, [bundles, currentBundle, selectedGroupKey]);
const [splitThreshold] = useState<number>(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<boolean>(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<Record<string, unknown>>)
.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 (
<Box sx={{ width: '100%', height: '100%', display: 'flex', overflow: 'hidden' }}>
<Box sx={{ width: 320, height: '100%', borderRight: '1px solid var(--color-divider)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ flex: 1, minHeight: 0 }}>
<CCEnhancedFilePanel
fileId={validFileId}
selectedPage={page}
onSelectPage={setPage}
currentSection={(function(){
const s = [...outlineOptions].sort((a,b)=>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;
})()}
/>
</Box>
</Box>
<Box sx={{ flex: 1, height: '100%', position: 'relative', display: 'flex', flexDirection: 'row' }}>
<Box sx={{ flex: 1, minWidth: 0, borderRight: '1px solid var(--color-divider)', display: 'flex', flexDirection: 'column' }}>
<CCDoclingViewer
fileId={validFileId}
currentPage={page}
onPageChange={setPage}
hideToolbar
sectionRange={(function(){
const s = [...outlineOptions].sort((a,b)=>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;
})()}
/>
</Box>
<Box sx={{ width: '42%', minWidth: 320, display: 'flex', flexDirection: 'column' }}>
<CCBundleViewer
fileId={validFileId}
bundleId={!combineSplit ? currentBundle : undefined}
currentPage={page}
combinedBundles={combineSplit ? (function(){
const grp = groupItems.find(g => 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<string, unknown>)||{}).split_order) || 0;
const bo = Number(((b.extra as Record<string, unknown>)||{}).split_order) || 0;
return ao - bo;
});
return ordered.map(b => ({ id: b.id }));
})() : undefined}
/>
</Box>
</Box>
<Box sx={{ width: 360, height: '100%', borderLeft: '1px solid var(--color-divider)', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ p: 2, fontWeight: 600 }}>AI Document Intelligence</Box>
<Divider />
<Box sx={{ p: 2, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ color: 'var(--color-text-2)', fontWeight: 600 }}>Canonical Docling</Typography>
{bundles.length > 0 && (
<>
<Typography variant="body2" sx={{ color: 'var(--color-text-2)' }}>Existing bundles</Typography>
{/* Batch selector (groups and singles) */}
<Select size="small" value={selectedGroupKey} onChange={(e: SelectChangeEvent<string>) => {
const key = e.target.value as string;
setSelectedGroupKey(key);
const grp = groupItems.find(g => g.key === key);
if (grp && grp.bundleIds.length) setCurrentBundle(grp.bundleIds[0]);
setCombineSplit(Boolean(grp && grp.isGroup));
}}>
{groupItems.map(g => (
<MenuItem key={g.key} value={g.key}>{g.label}</MenuItem>
))}
</Select>
{/* Only show combine toggle if multi-bundle group selected */}
{(() => {
const grp = groupItems.find(g => g.key === selectedGroupKey);
return grp && grp.isGroup ? (
<FormControlLabel control={<Switch checked={combineSplit} onChange={(e) => 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
})() && (
<Select size="small" value={currentBundle} onChange={(e) => setCurrentBundle(e.target.value as string)}>
{bundles.filter(b => {
const grp = groupItems.find(g => g.key === selectedGroupKey);
return grp ? grp.bundleIds.includes(b.id) : true;
}).sort((a,b) => {
const ao = Number(((a.extra as Record<string, unknown>)||{}).split_order) || 0;
const bo = Number(((b.extra as Record<string, unknown>)||{}).split_order) || 0;
return ao - bo;
}).map(b => {
const ex = (b.extra as Record<string, unknown>) || {};
const splitOrder = Number(ex.split_order ?? NaN);
const heading = ex.split_heading as string | undefined;
const pipeline = (ex.pipeline as string) || (b.type === 'docling_vlm' ? 'vlm' : 'standard');
const base = heading ? `${heading}${Number.isFinite(splitOrder) ? ` (#${splitOrder})` : ''}` : (new Date(b.created_at || '').toLocaleString() || b.id);
return (<MenuItem key={b.id} value={b.id}>{`${base} [${pipeline}]`}</MenuItem>);
})}
</Select>
)}
</>
)}
<Select size="small" value={profile} onChange={(e: SelectChangeEvent<Profile>) => setProfile(e.target.value as Profile)}>
<MenuItem value="default">Default</MenuItem>
<MenuItem value="simple">Simple</MenuItem>
<MenuItem value="aggressive">Aggressive</MenuItem>
</Select>
<Typography variant="body2" sx={{ color: 'var(--color-text-2)' }}>Pipeline</Typography>
<Select size="small" value={pipeline} onChange={(e: SelectChangeEvent<Pipeline>) => setPipeline(e.target.value as Pipeline)}>
<MenuItem value="standard">Standard</MenuItem>
<MenuItem value="vlm">VLM</MenuItem>
<MenuItem value="asr">ASR</MenuItem>
</Select>
{pipeline === 'vlm' && (
<>
<Typography variant="body2" sx={{ color: 'var(--color-text-2)' }}>VLM configuration</Typography>
<Select size="small" value={vlmMode} onChange={(e: SelectChangeEvent<VlmMode>) => setVlmMode(e.target.value as VlmMode)}>
<MenuItem value="preset">Preset</MenuItem>
<MenuItem value="local">Local (JSON)</MenuItem>
<MenuItem value="api">API (JSON)</MenuItem>
</Select>
{vlmMode === 'preset' && (
<Select size="small" value={vlmPreset} onChange={(e) => setVlmPreset(e.target.value as string)}>
<MenuItem value="smoldocling">smoldocling</MenuItem>
<MenuItem value="smoldocling_vllm">smoldocling_vllm</MenuItem>
<MenuItem value="granite_vision">granite_vision</MenuItem>
<MenuItem value="granite_vision_vllm">granite_vision_vllm</MenuItem>
<MenuItem value="granite_vision_ollama">granite_vision_ollama</MenuItem>
<MenuItem value="got_ocr_2">got_ocr_2</MenuItem>
</Select>
)}
{vlmMode === 'local' && (
<TextField size="small" label="VLM Local JSON" placeholder='{"repo_id":"..."}' value={vlmLocalJson} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setVlmLocalJson(e.target.value)} multiline minRows={2} />
)}
{vlmMode === 'api' && (
<>
<Select size="small" value={vlmProvider} onChange={(e: SelectChangeEvent<VlmProvider>) => setVlmProvider(e.target.value as VlmProvider)}>
<MenuItem value="">Custom JSON</MenuItem>
<MenuItem value="ollama">Ollama</MenuItem>
<MenuItem value="openai">OpenAI</MenuItem>
</Select>
{vlmProvider === 'ollama' && (
<>
<TextField size="small" label="Ollama Base URL" placeholder="http://localhost:11434" value={vlmProviderBaseUrl} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setVlmProviderBaseUrl(e.target.value)} />
<Select size="small" value={vlmProviderModel} onOpen={async () => {
try {
const base = vlmProviderBaseUrl || (import.meta.env.VITE_OLLAMA_BASE_URL || 'http://localhost:11434');
const resp = await fetch(`${base.replace(/\/$/, '')}/api/tags`);
if (resp.ok) {
const data = await resp.json();
const models = Array.isArray(data.models) ? (data.models as Array<{ model?: string; name?: string }>).map((m) => m.model || m.name || '').filter(Boolean) : [];
setOllamaModels(models);
}
} catch (_e) { /* no-op */ }
}} onChange={(e) => setVlmProviderModel(e.target.value as string)}>
{ollamaModels.map(m => (<MenuItem key={m} value={m}>{m}</MenuItem>))}
</Select>
</>
)}
{vlmProvider === 'openai' && (
<>
<TextField size="small" label="OpenAI Base URL (optional)" placeholder="https://api.openai.com/v1" value={vlmProviderBaseUrl} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setVlmProviderBaseUrl(e.target.value)} />
<Select size="small" value={vlmProviderModel} onChange={(e) => setVlmProviderModel(e.target.value as string)}>
<MenuItem value="gpt-4o-mini">gpt-4o-mini</MenuItem>
<MenuItem value="gpt-4o">gpt-4o</MenuItem>
<MenuItem value="gpt-4.1-mini">gpt-4.1-mini</MenuItem>
</Select>
</>
)}
{vlmProvider === '' && (
<TextField size="small" label="VLM API JSON" placeholder='{"provider":"ollama","base_url":"http://...","model":"..."}' value={vlmApiJson} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setVlmApiJson(e.target.value)} multiline minRows={2} />
)}
</>
)}
</>
)}
<Typography variant="body2" sx={{ color: 'var(--color-text-2)' }}>PDF Backend</Typography>
<Select size="small" value={pdfBackend} onChange={(e: SelectChangeEvent<PdfBackend>) => setPdfBackend(e.target.value as PdfBackend)}>
<MenuItem value="dlparse_v4">dlparse_v4 (default)</MenuItem>
<MenuItem value="pypdfium2">pypdfium2</MenuItem>
<MenuItem value="dlparse_v1">dlparse_v1</MenuItem>
<MenuItem value="dlparse_v2">dlparse_v2</MenuItem>
</Select>
<FormControlLabel control={<Switch checked={doOCR} onChange={(e) => setDoOCR(e.target.checked)} />} label="OCR" />
<FormControlLabel control={<Switch checked={forceOCR} onChange={(e) => setForceOCR(e.target.checked)} />} label="Force OCR" />
<Typography variant="body2" sx={{ color: 'var(--color-text-2)' }}>Table Mode</Typography>
<Select size="small" value={tableMode} onChange={(e: SelectChangeEvent<TableMode>) => setTableMode(e.target.value as TableMode)}>
<MenuItem value="fast">Fast</MenuItem>
<MenuItem value="accurate">Accurate</MenuItem>
</Select>
<Typography variant="body2" sx={{ color: 'var(--color-text-2)' }}>Section</Typography>
<Select size="small" value={selectedSectionId} onChange={(e: SelectChangeEvent<string>) => setSelectedSectionId(e.target.value as string)}>
<MenuItem value="full">{autoSplit ? 'Full document (auto split)' : 'Full document'}</MenuItem>
{splitSections.map(sec => (
<MenuItem key={sec.id} value={sec.id}>{sec.title ? `${sec.title} (${sec.start}-${sec.end})` : `Pages ${sec.start}-${sec.end}`}</MenuItem>
))}
</Select>
<FormControlLabel control={<Switch checked={doPicClass} onChange={(e) => setDoPicClass(e.target.checked)} />} label="Picture classification" />
<FormControlLabel control={<Switch checked={doPicDesc} onChange={(e) => setDoPicDesc(e.target.checked)} />} label="Picture description" />
{doPicDesc && (
<>
<TextField size="small" label="Description prompt" value={picDescPrompt} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPicDescPrompt(e.target.value)} />
<Typography variant="body2" sx={{ color: 'var(--color-text-2)' }}>Picture description configuration</Typography>
<Select size="small" value={picDescMode} onChange={(e: SelectChangeEvent<PicDescMode>) => setPicDescMode(e.target.value as PicDescMode)}>
<MenuItem value="local">Local (JSON)</MenuItem>
<MenuItem value="api">API (JSON)</MenuItem>
</Select>
{picDescMode === 'local' && (
<TextField size="small" label="Picture Description Local JSON" placeholder='{"repo_id":"..."}' value={picDescLocalJson} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPicDescLocalJson(e.target.value)} multiline minRows={2} />
)}
{picDescMode === 'api' && (
<TextField size="small" label="Picture Description API JSON" placeholder='{"base_url":"..."}' value={picDescApiJson} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPicDescApiJson(e.target.value)} multiline minRows={2} />
)}
</>
)}
<FormControlLabel control={<Switch checked={doFormula} onChange={(e) => setDoFormula(e.target.checked)} />} label="Formula enrichment" />
<FormControlLabel control={<Switch checked={doCode} onChange={(e) => setDoCode(e.target.checked)} />} label="Code enrichment" />
<FormControlLabel control={<Switch checked={tableCellMatching} onChange={(e) => setTableCellMatching(e.target.checked)} />} label="Table cell matching" />
{/* Outputs are always all formats for canonical bundles; UI omitted */}
<Button variant="contained" disabled={busy || !validFileId} onClick={async () => {
try {
setBusy(true);
const API_BASE = import.meta.env.VITE_API_BASE || '/api';
const token = accessToken || '';
const body: CanonicalDoclingRequest = {
use_split_map: selectedSectionId === 'full' ? autoSplit : false,
config: {
pipeline,
pdf_backend: pdfBackend,
do_ocr: doOCR,
force_ocr: forceOCR,
table_mode: tableMode,
do_picture_classification: doPicClass,
do_picture_description: doPicDesc,
picture_description_prompt: doPicDesc ? picDescPrompt : undefined,
target_type: 'zip',
image_export_mode: 'referenced',
table_cell_matching: tableCellMatching
},
threshold: splitThreshold
};
body.config.to_formats = ['json','html','text','md','doctags'];
body.config.do_formula_enrichment = doFormula;
body.config.do_code_enrichment = doCode;
// Apply selected section as custom range
const sel = selectedSectionId !== 'full' ? splitSections.find(s => s.id === selectedSectionId) : undefined;
if (sel) {
(body as unknown as { custom_range: [number, number]; custom_label: string; selected_section_id: string; selected_section_title: string }).custom_range = [sel.start, sel.end];
(body as unknown as { custom_range: [number, number]; custom_label: string; selected_section_id: string; selected_section_title: string }).custom_label = sel.title || `Pages ${sel.start}-${sel.end}`;
(body as unknown as { custom_range: [number, number]; custom_label: string; selected_section_id: string; selected_section_title: string }).selected_section_id = sel.id;
(body as unknown as { custom_range: [number, number]; custom_label: string; selected_section_id: string; selected_section_title: string }).selected_section_title = sel.title || '';
}
// If full and autoSplit, ensure threshold present
if (selectedSectionId === 'full' && autoSplit) {
(body as unknown as { threshold: number }).threshold = splitThreshold;
}
// Picture description mutually exclusive config
if (doPicDesc) {
if (picDescMode === 'local' && picDescLocalJson.trim()) {
body.config.picture_description_local = picDescLocalJson.trim();
body.config.picture_description_api = undefined;
} else if (picDescMode === 'api' && picDescApiJson.trim()) {
body.config.picture_description_api = picDescApiJson.trim();
body.config.picture_description_local = undefined;
} else {
body.config.picture_description_local = undefined;
body.config.picture_description_api = undefined;
}
} else {
body.config.picture_description_local = undefined;
body.config.picture_description_api = undefined;
}
// VLM mutually exclusive config + provider presets
if (pipeline === 'vlm') {
if (vlmMode === 'preset') {
body.config.vlm_pipeline_model = vlmPreset;
body.config.vlm_pipeline_model_local = undefined;
body.config.vlm_pipeline_model_api = undefined;
} else if (vlmMode === 'local' && vlmLocalJson.trim()) {
body.config.vlm_pipeline_model_local = vlmLocalJson.trim();
body.config.vlm_pipeline_model = undefined;
body.config.vlm_pipeline_model_api = undefined;
} else if (vlmMode === 'api') {
if (vlmProvider) {
(body.config as unknown as { vlm_provider: string; vlm_provider_model: string; vlm_provider_base_url: string }).vlm_provider = vlmProvider;
(body.config as unknown as { vlm_provider: string; vlm_provider_model: string; vlm_provider_base_url: string }).vlm_provider_model = vlmProviderModel.trim();
(body.config as unknown as { vlm_provider: string; vlm_provider_model: string; vlm_provider_base_url: string }).vlm_provider_base_url = vlmProviderBaseUrl.trim();
body.config.vlm_pipeline_model_api = undefined;
body.config.vlm_pipeline_model = undefined;
body.config.vlm_pipeline_model_local = undefined;
} else if (vlmApiJson.trim()) {
body.config.vlm_pipeline_model_api = vlmApiJson.trim();
body.config.vlm_pipeline_model = undefined;
body.config.vlm_pipeline_model_local = undefined;
} else {
body.config.vlm_pipeline_model = undefined;
body.config.vlm_pipeline_model_local = undefined;
body.config.vlm_pipeline_model_api = undefined;
}
}
} else {
body.config.vlm_pipeline_model = undefined;
body.config.vlm_pipeline_model_local = undefined;
body.config.vlm_pipeline_model_api = undefined;
}
const resp = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts/canonical-docling`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await resp.json();
console.log('canonical-docling:', data);
// Refresh bundles list
try {
const res = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) {
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);
if (list.length && !currentBundle) setCurrentBundle(list[0].id);
}
} catch (_err: unknown) { void 0; }
} finally {
setBusy(false);
}
}}>Generate Doclings</Button>
</Box>
</Box>
</Box>
);
};
export default CCDocumentIntelligence;