diff --git a/docs/adr/0017-shared-dashboard-sections-format-filter.md b/docs/adr/0017-shared-dashboard-sections-format-filter.md new file mode 100644 index 0000000..c5b7bf8 --- /dev/null +++ b/docs/adr/0017-shared-dashboard-sections-format-filter.md @@ -0,0 +1,11 @@ +# Shared Dashboard sections with format filter + +The PDF Workspace is not a second UI. Documents, Query, Benchmarks, and Chunk Preview are composed from shared section components in `index.html`, parameterized by a format filter (`word` | `pdf`). That keeps Word Tabs and the PDF Tab behaviorally identical while listing different documents, and avoids cloning the single-file Dashboard (ADR-0004). + +Rejected: copy-paste PDF Tab (diverges immediately) and “filter props only with zero extraction” when it would still duplicate large JSX blocks. + +## Consequences + +- Refactor existing Tab bodies to call shared panels before adding the PDF Tab +- Filtering is client-side by filename suffix; APIs stay format-agnostic +- Report generator stays shared; PDF Experiments only get a source-format badge diff --git a/src/static/index.html b/src/static/index.html index 49b1244..ea4c01b 100644 --- a/src/static/index.html +++ b/src/static/index.html @@ -276,6 +276,35 @@ async function api(path, opts = {}) { return res.json(); } +// -- Format filter helpers (ADR-0017) ----------------------- +function isPdfFilename(name) { + return (name || '').toLowerCase().endsWith('.pdf'); +} +function isWordFilename(name) { + const n = (name || '').toLowerCase(); + return n.endsWith('.doc') || n.endsWith('.docx'); +} +function filterDocuments(docs, format) { + if (format === 'pdf') return (docs || []).filter(d => isPdfFilename(d.filename)); + if (format === 'word') return (docs || []).filter(d => isWordFilename(d.filename)); + return docs || []; +} +function filterExperiments(exps, format) { + if (format === 'pdf') return (exps || []).filter(e => isPdfFilename(e.document_filename || '')); + if (format === 'word') return (exps || []).filter(e => isWordFilename(e.document_filename || '')); + return exps || []; +} +function formatAccept(format) { + if (format === 'pdf') return '.pdf'; + if (format === 'word') return '.doc,.docx'; + return '.doc,.docx,.pdf'; +} +function formatDropLabel(format) { + if (format === 'pdf') return '.pdf'; + if (format === 'word') return '.doc or .docx'; + return '.doc, .docx, or .pdf'; +} + // -- Toast system ------------------------------------------- let _toastId = 0; function ToastContainer({ toasts, removeToast }) { @@ -329,10 +358,10 @@ function HomeTab() { React.createElement('div', { className: 'card mt-4' }, React.createElement('div', { className: 'card-title' }, '⚡ Quick Start'), React.createElement('ol', { style: { paddingLeft: '20px', lineHeight: '2' } }, - React.createElement('li', null, 'Upload a document in the ', React.createElement('b', null, 'Documents'), ' tab'), + React.createElement('li', null, 'Upload a Word document in ', React.createElement('b', null, 'Documents'), ' or a Text PDF in ', React.createElement('b', null, 'PDF')), React.createElement('li', null, 'Process it with chunking strategies'), - React.createElement('li', null, 'Ask a question in the ', React.createElement('b', null, 'Query'), ' tab'), - React.createElement('li', null, 'Run a full benchmark in the ', React.createElement('b', null, 'Benchmarks'), ' tab'), + React.createElement('li', null, 'Ask a question in ', React.createElement('b', null, 'Query'), ' (or inside PDF Workspace)'), + React.createElement('li', null, 'Run a full benchmark in ', React.createElement('b', null, 'Benchmarks'), ' / PDF Workspace'), React.createElement('li', null, 'Check system health in the ', React.createElement('b', null, 'Admin'), ' tab') ) ) @@ -340,12 +369,13 @@ function HomeTab() { } // -- Tab: Documents ----------------------------------------- -function DocumentsTab({ documents, setDocuments, strategies, addToast }) { +function DocumentsTab({ documents, setDocuments, strategies, addToast, formatFilter = 'word', hideTitle = false }) { const [uploading, setUploading] = useState(false); const [processing, setProcessing] = useState({}); const [selectedStrategies, setSelectedStrategies] = useState({}); const [dragOver, setDragOver] = useState(false); const fileRef = useRef(null); + const visibleDocs = filterDocuments(documents, formatFilter); const fetchDocs = useCallback(() => { api('/documents').then(d => setDocuments(d.items || [])).catch(() => {}); @@ -355,6 +385,15 @@ function DocumentsTab({ documents, setDocuments, strategies, addToast }) { const handleUpload = async (file) => { if (!file) return; + const name = file.name || ''; + if (formatFilter === 'pdf' && !isPdfFilename(name)) { + addToast('Only .pdf files are allowed here', 'error'); + return; + } + if (formatFilter === 'word' && !isWordFilename(name)) { + addToast('Only .doc / .docx files are allowed here', 'error'); + return; + } setUploading(true); try { const form = new FormData(); @@ -425,7 +464,7 @@ function DocumentsTab({ documents, setDocuments, strategies, addToast }) { }; return React.createElement('div', null, - React.createElement('h1', { style: { color: 'var(--text-heading)', marginBottom: '16px', fontSize: '22px' } }, 'Documents'), + !hideTitle && React.createElement('h1', { style: { color: 'var(--text-heading)', marginBottom: '16px', fontSize: '22px' } }, 'Documents'), // Upload zone React.createElement('div', { @@ -439,9 +478,9 @@ function DocumentsTab({ documents, setDocuments, strategies, addToast }) { React.createElement('div', { className: 'text' }, uploading ? 'Uploading...' - : React.createElement('span', null, 'Drag & drop a ', React.createElement('strong', null, '.doc or .docx'), ' file, or click to browse')), + : React.createElement('span', null, 'Drag & drop a ', React.createElement('strong', null, formatDropLabel(formatFilter)), ' file, or click to browse')), React.createElement('input', { - ref: fileRef, type: 'file', accept: '.doc,.docx', style: { display: 'none' }, + ref: fileRef, type: 'file', accept: formatAccept(formatFilter), style: { display: 'none' }, onChange: (e) => handleUpload(e.target.files[0]) }) ), @@ -449,13 +488,13 @@ function DocumentsTab({ documents, setDocuments, strategies, addToast }) { // Documents table React.createElement('div', { className: 'card mt-4' }, React.createElement('div', { className: 'flex-between mb-2' }, - React.createElement('div', { className: 'card-title mb-0' }, `Uploaded Documents (${documents.length})`), + React.createElement('div', { className: 'card-title mb-0' }, `Uploaded Documents (${visibleDocs.length})`), React.createElement('button', { className: 'btn btn-secondary btn-sm', onClick: fetchDocs }, '↻ Refresh') ), - documents.length === 0 + visibleDocs.length === 0 ? React.createElement('div', { className: 'empty-state' }, React.createElement('div', { className: 'icon' }, '📭'), - React.createElement('div', null, 'No documents uploaded yet')) + React.createElement('div', null, formatFilter === 'pdf' ? 'No PDF documents uploaded yet' : 'No documents uploaded yet')) : React.createElement('table', null, React.createElement('thead', null, React.createElement('tr', null, @@ -468,7 +507,7 @@ function DocumentsTab({ documents, setDocuments, strategies, addToast }) { ) ), React.createElement('tbody', null, - documents.map(doc => + visibleDocs.map(doc => React.createElement('tr', { key: doc.id }, React.createElement('td', null, React.createElement('span', { className: 'truncate', style: { display: 'inline-block' } }, doc.filename)), @@ -534,10 +573,11 @@ function DocumentsTab({ documents, setDocuments, strategies, addToast }) { } // -- Tab: Query --------------------------------------------- -function QueryTab({ documents, strategies, addToast }) { +function QueryTab({ documents, strategies, addToast, formatFilter = 'word', hideTitle = false }) { const [form, setForm] = useState({ document_id: '', strategy: '', question: '', top_k: 5 }); const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); + const visibleDocs = filterDocuments(documents, formatFilter); const handleQuery = async () => { if (!form.document_id || !form.strategy || !form.question.trim()) { @@ -557,7 +597,7 @@ function QueryTab({ documents, strategies, addToast }) { }; return React.createElement('div', null, - React.createElement('h1', { style: { color: 'var(--text-heading)', marginBottom: '16px', fontSize: '22px' } }, 'Query'), + !hideTitle && React.createElement('h1', { style: { color: 'var(--text-heading)', marginBottom: '16px', fontSize: '22px' } }, 'Query'), React.createElement('div', { className: 'card' }, React.createElement('div', { className: 'row' }, @@ -568,7 +608,7 @@ function QueryTab({ documents, strategies, addToast }) { onChange: e => setForm({ ...form, document_id: e.target.value }) }, React.createElement('option', { value: '' }, 'Select document...'), - documents.map(d => React.createElement('option', { key: d.id, value: d.id }, d.filename)) + visibleDocs.map(d => React.createElement('option', { key: d.id, value: d.id }, d.filename)) ) ), React.createElement('div', { className: 'col' }, @@ -868,13 +908,15 @@ function ComparisonView({ experiments, addToast, onBack }) { } // -- Tab: Benchmarks ---------------------------------------- -function BenchmarksTab({ documents, strategies, addToast, questionsFile }) { +function BenchmarksTab({ documents, strategies, addToast, questionsFile, formatFilter = 'word', hideTitle = false }) { const [experiments, setExperiments] = useState([]); const [form, setForm] = useState({ document_id: '', strategies: strategies.map(s => s.name), questions_file: questionsFile, top_k: 5, dry_run: false }); const [running, setRunning] = useState(false); + const visibleDocs = filterDocuments(documents, formatFilter); + const visibleExps = filterExperiments(experiments, formatFilter); // Comparison state const [selectedExps, setSelectedExps] = useState(new Set()); @@ -914,10 +956,10 @@ function BenchmarksTab({ documents, strategies, addToast, questionsFile }) { }; const selectAllExps = () => { - if (selectedExps.size === experiments.length) { + if (selectedExps.size === visibleExps.length) { setSelectedExps(new Set()); } else { - setSelectedExps(new Set(experiments.map(e => e.id))); + setSelectedExps(new Set(visibleExps.map(e => e.id))); } }; @@ -975,7 +1017,7 @@ function BenchmarksTab({ documents, strategies, addToast, questionsFile }) { } setRunning(true); try { - const result = await api('/benchmarks', { method: 'POST', body: form }); + await api('/benchmarks', { method: 'POST', body: form }); addToast(`Benchmark ${form.dry_run ? 'estimate' : 'complete'}`, 'success'); if (!form.dry_run) fetchExperiments(); } catch (e) { @@ -995,7 +1037,7 @@ function BenchmarksTab({ documents, strategies, addToast, questionsFile }) { } return React.createElement('div', null, - React.createElement('h1', { style: { color: 'var(--text-heading)', marginBottom: '16px', fontSize: '22px' } }, 'Benchmarks'), + !hideTitle && React.createElement('h1', { style: { color: 'var(--text-heading)', marginBottom: '16px', fontSize: '22px' } }, 'Benchmarks'), // Run benchmark form React.createElement('div', { className: 'card' }, @@ -1008,7 +1050,7 @@ function BenchmarksTab({ documents, strategies, addToast, questionsFile }) { onChange: e => setForm({ ...form, document_id: e.target.value }) }, React.createElement('option', { value: '' }, 'Select document...'), - documents.map(d => React.createElement('option', { key: d.id, value: d.id }, d.filename)) + visibleDocs.map(d => React.createElement('option', { key: d.id, value: d.id }, d.filename)) ) ), React.createElement('div', { className: 'col', style: { maxWidth: '120px' } }, @@ -1070,7 +1112,7 @@ function BenchmarksTab({ documents, strategies, addToast, questionsFile }) { // Experiments list React.createElement('div', { className: 'card mt-4' }, React.createElement('div', { className: 'flex-between mb-2' }, - React.createElement('div', { className: 'card-title mb-0' }, `Experiments (${experiments.length})`), + React.createElement('div', { className: 'card-title mb-0' }, `Experiments (${visibleExps.length})`), React.createElement('div', { style: { display: 'flex', gap: '6px', alignItems: 'center' } }, selectedExps.size >= 2 ? React.createElement('button', { className: 'btn btn-primary btn-sm', @@ -1080,17 +1122,17 @@ function BenchmarksTab({ documents, strategies, addToast, questionsFile }) { React.createElement('button', { className: 'btn btn-secondary btn-sm', onClick: fetchExperiments }, '↻ Refresh') ) ), - experiments.length === 0 + visibleExps.length === 0 ? React.createElement('div', { className: 'empty-state' }, React.createElement('div', { className: 'icon' }, '📊'), - React.createElement('div', null, 'No experiments yet')) + React.createElement('div', null, formatFilter === 'pdf' ? 'No PDF experiments yet' : 'No experiments yet')) : React.createElement('table', null, React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { style: { width: '36px' } }, React.createElement('input', { type: 'checkbox', - checked: experiments.length > 0 && selectedExps.size === experiments.length, + checked: visibleExps.length > 0 && selectedExps.size === visibleExps.length, onChange: selectAllExps, style: { cursor: 'pointer' } }) @@ -1104,7 +1146,7 @@ function BenchmarksTab({ documents, strategies, addToast, questionsFile }) { ) ), React.createElement('tbody', null, - experiments.map(exp => React.createElement(React.Fragment, { key: exp.id }, + visibleExps.map(exp => React.createElement(React.Fragment, { key: exp.id }, React.createElement('tr', { className: selectedExps.has(exp.id) ? 'cmp-highlight' : '', style: { cursor: 'pointer' }, @@ -1228,6 +1270,165 @@ function CollapseSection({ title, icon, open, onToggle, badge, children }) { ); } +// -- Shared: Chunk Preview Panel ---------------------------- +function ChunkPreviewPanel({ documents, strategies, addToast, formatFilter = 'all' }) { + const visibleDocs = filterDocuments(documents, formatFilter); + const [chunkDocId, setChunkDocId] = useState(''); + const [chunkStrategy, setChunkStrategy] = useState(''); + const [chunkData, setChunkData] = useState(null); + const [chunkLoading, setChunkLoading] = useState(false); + const [expandedChunks, setExpandedChunks] = useState({}); + + const loadChunks = async () => { + if (!chunkDocId) { addToast('Select a document', 'error'); return; } + setChunkLoading(true); + setChunkData(null); + setExpandedChunks({}); + try { + let url = `/admin/chunks/${chunkDocId}`; + if (chunkStrategy) url += `?strategy=${chunkStrategy}`; + const data = await api(url); + setChunkData(data); + } catch (e) { addToast(`Chunk load failed: ${e.message}`, 'error'); } + finally { setChunkLoading(false); } + }; + + return React.createElement(React.Fragment, null, + React.createElement('div', { className: 'row mb-4' }, + React.createElement('div', { className: 'col' }, + React.createElement('label', null, 'Document'), + React.createElement('select', { + value: chunkDocId, + onChange: e => { setChunkDocId(e.target.value); setChunkData(null); } + }, + React.createElement('option', { value: '' }, 'Select document...'), + visibleDocs.map(d => React.createElement('option', { key: d.id, value: d.id }, d.filename)) + ) + ), + React.createElement('div', { className: 'col' }, + React.createElement('label', null, 'Strategy (optional)'), + React.createElement('select', { + value: chunkStrategy, + onChange: e => { setChunkStrategy(e.target.value); setChunkData(null); } + }, + React.createElement('option', { value: '' }, 'All strategies'), + strategies.map(s => React.createElement('option', { key: s.name, value: s.name }, s.name)) + ) + ), + React.createElement('div', { style: { flex: 'none' } }, + React.createElement('button', { + className: 'btn btn-primary', onClick: loadChunks, disabled: chunkLoading || !chunkDocId, + }, chunkLoading ? React.createElement('span', { className: 'spinner' }) : 'Load Chunks') + ) + ), + chunkData && Object.entries(chunkData.strategies || {}).map(([stratName, stratData]) => + React.createElement('div', { key: stratName, style: { marginBottom: '16px' } }, + React.createElement('div', { className: 'flex-between mb-2' }, + React.createElement('h3', { style: { fontSize: '14px', fontWeight: 600, color: 'var(--text-heading)' } }, + `${stratName} `, + React.createElement('span', { className: 'badge badge-accent' }, `${stratData.count} chunks`)), + stratData.error ? React.createElement('span', { className: 'badge badge-danger' }, stratData.error) : null + ), + stratData.count === 0 + ? React.createElement('div', { className: 'text-sm text-muted', style: { padding: '8px 0' } }, 'No chunks for this strategy') + : React.createElement('table', null, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { width: '40px' } }, '#'), + React.createElement('th', null, 'Text'), + React.createElement('th', { style: { width: '60px' } }, 'Tokens'), + React.createElement('th', { style: { width: '60px' } }, 'Chars'), + ) + ), + React.createElement('tbody', null, + (stratData.chunks || []).map((chunk, i) => { + const key = `${stratName}-${chunk.chunk_index ?? i}`; + const isExpanded = expandedChunks[key]; + return React.createElement(React.Fragment, { key }, + React.createElement('tr', { + className: 'chunk-row', + onClick: () => setExpandedChunks(prev => ({ ...prev, [key]: !prev[key] })), + }, + React.createElement('td', { className: 'text-muted text-sm' }, chunk.chunk_index ?? i + 1), + React.createElement('td', { className: 'text-sm' }, + React.createElement('span', { style: { display: '-webkit-box', WebkitLineClamp: isExpanded ? 'unset' : 1, WebkitBoxOrient: 'vertical', overflow: isExpanded ? 'visible' : 'hidden' } }, + chunk.text || '')), + React.createElement('td', { className: 'text-sm text-muted' }, chunk.token_count), + React.createElement('td', { className: 'text-sm text-muted' }, chunk.character_count), + ), + isExpanded ? React.createElement('tr', null, + React.createElement('td', { colSpan: 4 }, + React.createElement('div', { className: 'chunk-detail' }, + React.createElement('div', null, chunk.text || '(empty)'), + React.createElement('div', { className: 'meta' }, + `chunk_id: ${chunk.chunk_id} | parent: ${chunk.parent_id || 'none'}`) + ) + ) + ) : null + ); + }) + ) + ) + ) + ) + ); +} + +// -- Tab: PDF Workspace ------------------------------------- +function PdfWorkspaceTab({ documents, setDocuments, strategies, addToast, questionsFile }) { + const [openSections, setOpenSections] = useState({ + upload: true, chunks: false, query: false, benchmarks: false, + }); + const toggle = (key) => setOpenSections(prev => ({ ...prev, [key]: !prev[key] })); + const pdfCount = filterDocuments(documents, 'pdf').length; + + return React.createElement('div', null, + React.createElement('h1', { style: { color: 'var(--text-heading)', marginBottom: '8px', fontSize: '22px' } }, 'PDF Workspace'), + React.createElement('p', { className: 'text-muted mb-4', style: { maxWidth: '640px' } }, + 'Text PDF only — upload, process, preview chunks, query, and benchmark. Scanned/image PDFs are rejected at upload.'), + + React.createElement(CollapseSection, { + title: 'Upload & Process', icon: '📄', open: openSections.upload, + onToggle: () => toggle('upload'), + badge: pdfCount ? `${pdfCount} PDFs` : '0', + }, + React.createElement(DocumentsTab, { + documents, setDocuments, strategies, addToast, + formatFilter: 'pdf', hideTitle: true, + }) + ), + + React.createElement(CollapseSection, { + title: 'Chunk Preview', icon: '📦', open: openSections.chunks, + onToggle: () => toggle('chunks'), + }, + React.createElement(ChunkPreviewPanel, { + documents, strategies, addToast, formatFilter: 'pdf', + }) + ), + + React.createElement(CollapseSection, { + title: 'Query', icon: '⚡', open: openSections.query, + onToggle: () => toggle('query'), + }, + React.createElement(QueryTab, { + documents, strategies, addToast, + formatFilter: 'pdf', hideTitle: true, + }) + ), + + React.createElement(CollapseSection, { + title: 'Benchmarks', icon: '📊', open: openSections.benchmarks, + onToggle: () => toggle('benchmarks'), + }, + React.createElement(BenchmarksTab, { + documents, strategies, addToast, questionsFile, + formatFilter: 'pdf', hideTitle: true, + }) + ) + ); +} + // -- Tab: Admin --------------------------------------------- function AdminTab({ addToast, documents, strategies, setActiveTab, setQuestionsFile }) { // Collapse state for each section @@ -1253,27 +1454,6 @@ function AdminTab({ addToast, documents, strategies, setActiveTab, setQuestionsF // -- Qdrant state -- const [collectionsData, setCollectionsData] = useState([]); - // -- Chunk Preview state -- - const [chunkDocId, setChunkDocId] = useState(''); - const [chunkStrategy, setChunkStrategy] = useState(''); - const [chunkData, setChunkData] = useState(null); - const [chunkLoading, setChunkLoading] = useState(false); - const [expandedChunks, setExpandedChunks] = useState({}); - - const loadChunks = async () => { - if (!chunkDocId) { addToast('Select a document', 'error'); return; } - setChunkLoading(true); - setChunkData(null); - setExpandedChunks({}); - try { - let url = `/admin/chunks/${chunkDocId}`; - if (chunkStrategy) url += `?strategy=${chunkStrategy}`; - const data = await api(url); - setChunkData(data); - } catch (e) { addToast(`Chunk load failed: ${e.message}`, 'error'); } - finally { setChunkLoading(false); } - }; - // -- Questions state -- const [qFiles, setQFiles] = useState([]); const [qLoading, setQLoading] = useState(false); @@ -1425,85 +1605,10 @@ function AdminTab({ addToast, documents, strategies, setActiveTab, setQuestionsF React.createElement(CollapseSection, { title: '📦 Chunk Preview', icon: '', open: openSections.chunks, onToggle: () => toggle('chunks'), - badge: chunkData ? `${chunkData.strategies ? Object.values(chunkData.strategies).reduce((a, s) => a + (s.count || 0), 0) : 0} chunks` : null, }, - React.createElement('div', { className: 'row mb-4' }, - React.createElement('div', { className: 'col' }, - React.createElement('label', null, 'Document'), - React.createElement('select', { - value: chunkDocId, - onChange: e => { setChunkDocId(e.target.value); setChunkData(null); } - }, - React.createElement('option', { value: '' }, 'Select document...'), - documents.map(d => React.createElement('option', { key: d.id, value: d.id }, d.filename)) - ) - ), - React.createElement('div', { className: 'col' }, - React.createElement('label', null, 'Strategy (optional)'), - React.createElement('select', { - value: chunkStrategy, - onChange: e => { setChunkStrategy(e.target.value); setChunkData(null); } - }, - React.createElement('option', { value: '' }, 'All strategies'), - strategies.map(s => React.createElement('option', { key: s.name, value: s.name }, s.name)) - ) - ), - React.createElement('div', { style: { flex: 'none' } }, - React.createElement('button', { - className: 'btn btn-primary', onClick: loadChunks, disabled: chunkLoading || !chunkDocId, - }, chunkLoading ? React.createElement('span', { className: 'spinner' }) : 'Load Chunks') - ) - ), - chunkData && Object.entries(chunkData.strategies || {}).map(([stratName, stratData]) => - React.createElement('div', { key: stratName, style: { marginBottom: '16px' } }, - React.createElement('div', { className: 'flex-between mb-2' }, - React.createElement('h3', { style: { fontSize: '14px', fontWeight: 600, color: 'var(--text-heading)' } }, - `${stratName} `, - React.createElement('span', { className: 'badge badge-accent' }, `${stratData.count} chunks`)), - stratData.error ? React.createElement('span', { className: 'badge badge-danger' }, stratData.error) : null - ), - stratData.count === 0 - ? React.createElement('div', { className: 'text-sm text-muted', style: { padding: '8px 0' } }, 'No chunks for this strategy') - : React.createElement('table', null, - React.createElement('thead', null, - React.createElement('tr', null, - React.createElement('th', { style: { width: '40px' } }, '#'), - React.createElement('th', null, 'Text'), - React.createElement('th', { style: { width: '60px' } }, 'Tokens'), - React.createElement('th', { style: { width: '60px' } }, 'Chars'), - ) - ), - React.createElement('tbody', null, - (stratData.chunks || []).map((chunk, i) => { - const key = `${stratName}-${chunk.chunk_index ?? i}`; - const isExpanded = expandedChunks[key]; - return React.createElement(React.Fragment, { key }, - React.createElement('tr', { - className: 'chunk-row', - onClick: () => setExpandedChunks(prev => ({ ...prev, [key]: !prev[key] })), - }, - React.createElement('td', { className: 'text-muted text-sm' }, chunk.chunk_index ?? i + 1), - React.createElement('td', { className: 'text-sm' }, - React.createElement('span', { style: { display: '-webkit-box', WebkitLineClamp: isExpanded ? 'unset' : 1, WebkitBoxOrient: 'vertical', overflow: isExpanded ? 'visible' : 'hidden' } }, - chunk.text || '')), - React.createElement('td', { className: 'text-sm text-muted' }, chunk.token_count), - React.createElement('td', { className: 'text-sm text-muted' }, chunk.character_count), - ), - isExpanded ? React.createElement('tr', null, - React.createElement('td', { colSpan: 4 }, - React.createElement('div', { className: 'chunk-detail' }, - React.createElement('div', null, chunk.text || '(empty)'), - React.createElement('div', { className: 'meta' }, - `chunk_id: ${chunk.chunk_id} | parent: ${chunk.parent_id || 'none'}`) - ) - ) - ) : null - ); - }) - ) - ) - ) - ) + React.createElement(ChunkPreviewPanel, { + documents, strategies, addToast, formatFilter: 'all', + }) ), // -- 4. Questions Management -- @@ -1651,6 +1756,7 @@ function App() { useEffect(() => { api('/strategies').then(d => setStrategies(d.strategies || [])).catch(() => {}); + api('/documents').then(d => setDocuments(d.items || [])).catch(() => {}); }, []); const addToast = useCallback((message, type = 'success') => { @@ -1666,6 +1772,7 @@ function App() { const tabs = [ { id: 'home', label: 'Home' }, { id: 'documents', label: 'Documents' }, + { id: 'pdf', label: 'PDF' }, { id: 'query', label: 'Query' }, { id: 'benchmarks', label: 'Benchmarks' }, { id: 'admin', label: 'Admin' }, @@ -1675,11 +1782,13 @@ function App() { switch (activeTab) { case 'home': return React.createElement(HomeTab); case 'documents': return React.createElement(DocumentsTab, { - documents, setDocuments, strategies, addToast }); + documents, setDocuments, strategies, addToast, formatFilter: 'word' }); + case 'pdf': return React.createElement(PdfWorkspaceTab, { + documents, setDocuments, strategies, addToast, questionsFile }); case 'query': return React.createElement(QueryTab, { - documents, strategies, addToast }); + documents, strategies, addToast, formatFilter: 'word' }); case 'benchmarks': return React.createElement(BenchmarksTab, { - documents, strategies, addToast, questionsFile }); + documents, strategies, addToast, questionsFile, formatFilter: 'word' }); case 'admin': return React.createElement(AdminTab, { addToast, documents, strategies, setActiveTab, setQuestionsFile }); default: return null;