Store all mode results (OCR, Describe, Freeform) in a single job record

- DB: add describe_text and freeform_text columns (ALTER TABLE IF NOT EXISTS)
- Backend: commit and review endpoints accept/persist all three text fields
- App: accumulate results per mode in state; tabs appear when >1 mode run;
  all results sent on Commit Job
- JobDetail: tabbed text panel shows whichever fields are populated, all editable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Roberts
2026-06-10 12:28:01 +01:00
parent 4ab87d2e6f
commit ae0ac3af59
4 changed files with 163 additions and 46 deletions

View File

@@ -36,12 +36,15 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [editedText, setEditedText] = useState('')
const [editAuthor, setEditAuthor] = useState('')
const [editBook, setEditBook] = useState('')
const [editChapter, setEditChapter] = useState('')
const [editPage, setEditPage] = useState('')
const [reviewerName, setReviewerName] = useState('')
const [editedText, setEditedText] = useState('')
const [editDescribeText, setEditDescribeText] = useState('')
const [editFreeformText, setEditFreeformText] = useState('')
const [activeTab, setActiveTab] = useState('ocr')
const [editAuthor, setEditAuthor] = useState('')
const [editBook, setEditBook] = useState('')
const [editChapter, setEditChapter] = useState('')
const [editPage, setEditPage] = useState('')
const [reviewerName, setReviewerName] = useState('')
const [submitting, setSubmitting] = useState(false)
const [saveResult, setSaveResult] = useState(null)
@@ -60,11 +63,17 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
const d = res.data
setJob(d)
setEditedText(d.reviewed_text ?? d.ocr_text ?? '')
setEditDescribeText(d.describe_text ?? '')
setEditFreeformText(d.freeform_text ?? '')
setEditAuthor(d.author || '')
setEditBook(d.book || '')
setEditChapter(d.chapter || '')
setEditPage(d.page || '')
setReviewerName(d.reviewer_name || '')
// Default to first tab that has content
if (d.reviewed_text || d.ocr_text) setActiveTab('ocr')
else if (d.describe_text) setActiveTab('describe')
else if (d.freeform_text) setActiveTab('freeform')
}
})
.catch(err => {
@@ -90,6 +99,8 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
book: editBook,
chapter: editChapter,
page: editPage,
describe_text: editDescribeText || null,
freeform_text: editFreeformText || null,
})
setJob(res.data)
setSaveResult({ success: true })
@@ -199,26 +210,72 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
/>
</div>
<div className="glass rounded-2xl p-4 flex flex-col h-full">
{/* Tabs — only show tabs that have content */}
{(() => {
const tabs = [
job.ocr_text || job.reviewed_text ? { id: 'ocr', label: 'OCR Text' } : null,
job.describe_text != null ? { id: 'describe', label: 'Description' } : null,
job.freeform_text != null ? { id: 'freeform', label: 'Freeform' } : null,
].filter(Boolean)
return tabs.length > 1 ? (
<div className="flex gap-1 mb-3 flex-shrink-0">
{tabs.map(t => (
<button
key={t.id}
onClick={() => setActiveTab(t.id)}
className={`px-3 py-1 rounded-lg text-xs font-medium transition-colors ${
activeTab === t.id
? 'bg-purple-600 text-white'
: 'bg-white/5 text-gray-400 hover:bg-white/10'
}`}
>
{t.label}
</button>
))}
</div>
) : null
})()}
<p className="text-xs text-gray-400 mb-2 flex-shrink-0">
{isReviewed ? 'Reviewed Text' : 'OCR Text'}
{{ ocr: isReviewed ? 'Reviewed Text' : 'OCR Text', describe: 'Description', freeform: 'Freeform' }[activeTab]}
<span className="text-purple-400 ml-1">(editable)</span>
</p>
<textarea
value={editedText}
onChange={e => setEditedText(e.target.value)}
className="flex-1 w-full bg-transparent text-sm text-gray-200 font-mono resize-none focus:outline-none min-h-0"
placeholder="Text content..."
/>
{/* Original OCR text collapsed for reviewed jobs */}
{isReviewed && job.ocr_text && (
<details className="flex-shrink-0 mt-2 border-t border-white/10 pt-2">
<summary className="cursor-pointer text-xs text-gray-500 hover:text-gray-400 transition-colors">
Original OCR Text
</summary>
<pre className="text-xs text-gray-600 whitespace-pre-wrap font-mono mt-1 max-h-28 overflow-y-auto">
{job.ocr_text}
</pre>
</details>
{activeTab === 'ocr' && (
<>
<textarea
value={editedText}
onChange={e => setEditedText(e.target.value)}
className="flex-1 w-full bg-transparent text-sm text-gray-200 font-mono resize-none focus:outline-none min-h-0"
placeholder="OCR text..."
/>
{isReviewed && job.ocr_text && (
<details className="flex-shrink-0 mt-2 border-t border-white/10 pt-2">
<summary className="cursor-pointer text-xs text-gray-500 hover:text-gray-400 transition-colors">
Original OCR Text
</summary>
<pre className="text-xs text-gray-600 whitespace-pre-wrap font-mono mt-1 max-h-28 overflow-y-auto">
{job.ocr_text}
</pre>
</details>
)}
</>
)}
{activeTab === 'describe' && (
<textarea
value={editDescribeText}
onChange={e => setEditDescribeText(e.target.value)}
className="flex-1 w-full bg-transparent text-sm text-gray-200 font-mono resize-none focus:outline-none min-h-0"
placeholder="Description text..."
/>
)}
{activeTab === 'freeform' && (
<textarea
value={editFreeformText}
onChange={e => setEditFreeformText(e.target.value)}
className="flex-1 w-full bg-transparent text-sm text-gray-200 font-mono resize-none focus:outline-none min-h-0"
placeholder="Freeform result..."
/>
)}
</div>
</div>