Agent_PDF / web /src /components /TableGrid.jsx
Ag27 Deployer
Deploy Ag27 Table Extractor: 2026-04-29 21:58:59
b1ae7de
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 (
<div style={{ padding: 32, textAlign: 'center', color: 'var(--text-muted)' }}>
No tables detected.
</div>
)
}
const table = tables[activeTab] || tables[0]
const cells = table.cells || []
if (!cells.length) {
return (
<div style={{ padding: 32, textAlign: 'center', color: 'var(--text-muted)' }}>
No cells in this table.
</div>
)
}
// 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 (
<div>
{tables.length > 1 && (
<div className="table-tabs">
{tables.map((t, i) => (
<button
key={t.table_id}
className={`table-tab${i === activeTab ? ' active' : ''}`}
onClick={() => setActiveTab(i)}
>
Table {t.table_id}
{t.td_score !== null && t.td_score !== undefined && (
<span className="table-tab-score" style={{ color: confidenceColor(t.td_score) }}>
{Math.round(t.td_score * 100)}%
</span>
)}
</button>
))}
</div>
)}
<div className="table-edit-hint">
💡 Double-click any cell to edit its text before exporting
</div>
<div className="table-grid-container">
<table className="extracted-table">
<tbody>
{Array.from({ length: maxRow }, (_, r) => (
<tr key={r}>
{Array.from({ length: maxCol }, (_, c) => {
const key = `${r},${c}`
if (covered.has(key)) return null
const cell = cellMap[key]
if (!cell) return <td key={c}></td>
const editing = isEditing(r, c)
const edited = wasEdited(r, c)
const score = cell.ocr_score
return (
<td
key={c}
rowSpan={cell.row_span > 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 ? (
<div
className="cell-editor"
contentEditable
suppressContentEditableWarning
onBlur={(e) => 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 || ''
)}
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}