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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user