import { useState, useCallback } from 'react' import API from '../api' function confidenceColor(score) { if (score === null || score === undefined) return 'var(--confidence-none)' if (score >= 0.85) return 'var(--confidence-high)' if (score >= 0.50) return 'var(--confidence-med)' return 'var(--confidence-low)' } export default function TableGrid({ tables, jobId }) { const [activeTab, setActiveTab] = useState(0) const [editingCell, setEditingCell] = useState(null) const [editedCells, setEditedCells] = useState(new Set()) const handleCellDoubleClick = useCallback((tableId, row, col, currentText) => { setEditingCell({ tableId, row, col, text: currentText }) }, []) const handleCellSave = useCallback(async (tableId, row, col, newText) => { setEditingCell(null) if (!jobId) return try { const res = await fetch(`${API}/api/results/${jobId}/cells`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ table_id: tableId, row, col, text: newText }), }) if (res.ok) { // Update local state const table = tables[activeTab] if (table) { const cell = table.cells?.find(c => c.row === row && c.col === col) if (cell) cell.text = newText } setEditedCells(prev => new Set(prev).add(`${tableId}:${row}:${col}`)) } } catch (err) { console.error('Cell edit failed:', err) } }, [jobId, tables, activeTab]) const handleKeyDown = useCallback((e, tableId, row, col) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleCellSave(tableId, row, col, e.target.innerText) } if (e.key === 'Escape') { setEditingCell(null) } }, [handleCellSave]) if (!tables.length) { return (
No tables detected.
) } const table = tables[activeTab] || tables[0] const cells = table.cells || [] if (!cells.length) { return (
No cells in this table.
) } // Build grid const maxRow = Math.max(...cells.map(c => c.row + c.row_span)) const maxCol = Math.max(...cells.map(c => c.col + c.col_span)) // Track covered positions (from spanning cells) const covered = new Set() const cellMap = {} for (const cell of cells) { cellMap[`${cell.row},${cell.col}`] = cell if (cell.row_span > 1 || cell.col_span > 1) { for (let r = cell.row; r < cell.row + cell.row_span; r++) { for (let c = cell.col; c < cell.col + cell.col_span; c++) { if (r !== cell.row || c !== cell.col) { covered.add(`${r},${c}`) } } } } } const isEditing = (row, col) => editingCell && editingCell.tableId === table.table_id && editingCell.row === row && editingCell.col === col const wasEdited = (row, col) => editedCells.has(`${table.table_id}:${row}:${col}`) return (
{tables.length > 1 && (
{tables.map((t, i) => ( ))}
)}
💡 Double-click any cell to edit its text before exporting
{Array.from({ length: maxRow }, (_, r) => ( {Array.from({ length: maxCol }, (_, c) => { const key = `${r},${c}` if (covered.has(key)) return null const cell = cellMap[key] if (!cell) return const editing = isEditing(r, c) const edited = wasEdited(r, c) const score = cell.ocr_score return ( ) })} ))}
1 ? cell.row_span : undefined} colSpan={cell.col_span > 1 ? cell.col_span : undefined} className={`${editing ? 'cell-editing' : ''} ${edited ? 'cell-edited' : ''}`} style={{ borderLeftColor: confidenceColor(score), borderLeftWidth: 3, ...(r === 0 ? { fontWeight: 600, background: 'var(--bg-elevated)' } : {}), }} title={score != null ? `OCR confidence: ${Math.round(score * 100)}%` : 'No OCR score'} onDoubleClick={() => handleCellDoubleClick(table.table_id, r, c, cell.text || '')} > {editing ? (
handleCellSave(table.table_id, r, c, e.target.innerText)} onKeyDown={(e) => handleKeyDown(e, table.table_id, r, c)} ref={(el) => { if (el) { el.focus(); el.innerText = editingCell.text } }} /> ) : ( cell.text || '' )}
) }