607 lines
34 KiB
TypeScript
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;
|
|
|