feat(dashboard): add pdf workspace via shared format filter

Why:
- Avoid a forked PDF UI while scoping Word vs PDF workflows.

Changes:
- formatFilter on shared tabs, PDF Workspace accordion tab, word-only defaults elsewhere.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-02 16:59:42 +03:30
parent 55907a8dec
commit 9ce2072c23
2 changed files with 247 additions and 127 deletions

View File

@@ -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

View File

@@ -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;