Fix commit job and OCR text editing

- OCR text is now shown in an editable textarea (plain_ocr mode) so
  users can correct it before committing
- editedOcrText state tracks edits; commit job sends the edited value
  instead of the original result.text
- Remove silent early-return guard that blocked commit when text was empty
- Copy and download also use the edited text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Roberts
2026-06-09 17:11:49 +01:00
parent fd747e6c23
commit da7957d7d5
2 changed files with 48 additions and 27 deletions

View File

@@ -39,6 +39,9 @@ function App() {
// Job metadata
const [metadata, setMetadata] = useState({ author: '', book: '', chapter: '', page: '' })
// Editable OCR text (for plain_ocr mode, editable before commit)
const [editedOcrText, setEditedOcrText] = useState('')
// Job commit state
const [commitLoading, setCommitLoading] = useState(false)
const [commitResult, setCommitResult] = useState(null)
@@ -64,6 +67,7 @@ function App() {
setImagePreview(fileType === 'image' ? URL.createObjectURL(file) : file)
setError(null)
setResult(null)
setEditedOcrText('')
setCommitResult(null)
}
}, [imagePreview, fileType])
@@ -95,6 +99,8 @@ function App() {
headers: { 'Content-Type': 'multipart/form-data' },
})
setResult(response.data)
setEditedOcrText(response.data.text || '')
setCommitResult(null)
} catch (err) {
setError(err.response?.data?.detail || err.message || 'An error occurred')
} finally {
@@ -103,7 +109,7 @@ function App() {
}
const handleCommitJob = useCallback(async () => {
if (!image || !result?.text) return
if (!image) return
setCommitLoading(true)
setCommitResult(null)
try {
@@ -113,7 +119,7 @@ function App() {
formData.append('book', metadata.book)
formData.append('chapter', metadata.chapter)
formData.append('page', metadata.page)
formData.append('ocr_text', result.text)
formData.append('ocr_text', editedOcrText)
formData.append('mode', mode)
const response = await axios.post(`${API_BASE}/jobs`, formData, {
@@ -125,16 +131,18 @@ function App() {
} finally {
setCommitLoading(false)
}
}, [image, result, metadata, mode])
}, [image, editedOcrText, metadata, mode])
const handleCopy = useCallback(() => {
if (result?.text) navigator.clipboard.writeText(result.text)
}, [result])
const text = editedOcrText || result?.text
if (text) navigator.clipboard.writeText(text)
}, [editedOcrText, result])
const handleDownload = useCallback(() => {
if (!result?.text) return
const text = editedOcrText || result?.text
if (!text) return
const ext = { plain_ocr: 'txt', describe: 'txt', find_ref: 'txt', freeform: 'txt' }[mode] || 'txt'
const blob = new Blob([result.text], { type: 'text/plain' })
const blob = new Blob([text], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
@@ -391,6 +399,8 @@ function App() {
onCommitJob={mode === 'plain_ocr' && result ? handleCommitJob : null}
commitLoading={commitLoading}
commitResult={commitResult}
editedOcrText={editedOcrText}
onOcrTextChange={setEditedOcrText}
/>
</motion.div>
</div>

View File

@@ -4,7 +4,7 @@ import { Copy, Download, Sparkles, Loader2, CheckCircle2, ChevronDown, Database
import ReactMarkdown from 'react-markdown'
import DOMPurify from 'dompurify'
export default function ResultPanel({ result, loading, imagePreview, onCopy, onDownload, onCommitJob, commitLoading, commitResult }) {
export default function ResultPanel({ result, loading, imagePreview, onCopy, onDownload, onCommitJob, commitLoading, commitResult, editedOcrText, onOcrTextChange }) {
const canvasRef = useRef(null)
const imgRef = useRef(null)
const [showAdvanced, setShowAdvanced] = useState(false)
@@ -226,26 +226,37 @@ export default function ResultPanel({ result, loading, imagePreview, onCopy, onD
</div>
)}
{/* Text result */}
<div className="bg-white/5 border border-white/10 rounded-xl p-4 max-h-96 overflow-y-auto">
{isHTML ? (
<div
className="prose prose-invert prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(result.text) }}
style={{
color: '#e5e7eb',
}}
{/* Text result — editable textarea in plain_ocr/commit mode, rendered otherwise */}
{onCommitJob ? (
<div className="space-y-1">
<p className="text-xs text-gray-400">OCR Text <span className="text-purple-400">(editable correct before committing)</span></p>
<textarea
value={editedOcrText}
onChange={e => onOcrTextChange(e.target.value)}
rows={10}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm text-gray-200 font-mono resize-y focus:outline-none focus:border-purple-500/50 transition-colors"
placeholder="OCR text will appear here..."
/>
) : isMarkdown ? (
<div className="prose prose-invert prose-sm max-w-none">
<ReactMarkdown>{result.text}</ReactMarkdown>
</div>
) : (
<pre className="text-sm text-gray-200 whitespace-pre-wrap font-mono">
{result.text}
</pre>
)}
</div>
</div>
) : (
<div className="bg-white/5 border border-white/10 rounded-xl p-4 max-h-96 overflow-y-auto">
{isHTML ? (
<div
className="prose prose-invert prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(result.text) }}
style={{ color: '#e5e7eb' }}
/>
) : isMarkdown ? (
<div className="prose prose-invert prose-sm max-w-none">
<ReactMarkdown>{result.text}</ReactMarkdown>
</div>
) : (
<pre className="text-sm text-gray-200 whitespace-pre-wrap font-mono">
{result.text}
</pre>
)}
</div>
)}
{/* Raw Response Viewer */}
{result.raw_text && (