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:
@@ -43,15 +43,21 @@ function App() {
|
||||
const suggestions = useSuggestions()
|
||||
|
||||
const [metadata, setMetadata] = useState({ author: '', book: '', chapter: '', page: '' })
|
||||
const [editedOcrText, setEditedOcrText] = useState('')
|
||||
// Results accumulated per mode: { plain_ocr: 'text', describe: 'text', freeform: 'text' }
|
||||
const [modeResults, setModeResults] = useState({})
|
||||
const [editedResults, setEditedResults] = useState({})
|
||||
const [activeResultMode, setActiveResultMode] = useState(null)
|
||||
const [commitLoading, setCommitLoading] = useState(false)
|
||||
const [commitResult, setCommitResult] = useState(null)
|
||||
|
||||
// Modes that produce editable text output and can be committed to the DB
|
||||
const COMMITTABLE_MODES = new Set(['plain_ocr', 'describe', 'freeform'])
|
||||
const MODE_LABELS = { plain_ocr: 'OCR Text', describe: 'Description', freeform: 'Freeform' }
|
||||
|
||||
// Whether to show the full-screen result view
|
||||
const showResultView = view === 'new_job' && COMMITTABLE_MODES.has(mode) && !!result
|
||||
// Show the full-screen result view when any committable mode has a result (or is loading)
|
||||
const showResultView = view === 'new_job' && (
|
||||
Object.keys(modeResults).length > 0 || (loading && COMMITTABLE_MODES.has(mode))
|
||||
)
|
||||
|
||||
const handleFileTypeChange = useCallback((newType) => {
|
||||
setImage(null)
|
||||
@@ -69,14 +75,18 @@ function App() {
|
||||
setImagePreview(null)
|
||||
setError(null)
|
||||
setResult(null)
|
||||
setEditedOcrText('')
|
||||
setModeResults({})
|
||||
setEditedResults({})
|
||||
setActiveResultMode(null)
|
||||
setCommitResult(null)
|
||||
} else {
|
||||
setImage(file)
|
||||
setImagePreview(fileType === 'image' ? URL.createObjectURL(file) : file)
|
||||
setError(null)
|
||||
setResult(null)
|
||||
setEditedOcrText('')
|
||||
setModeResults({})
|
||||
setEditedResults({})
|
||||
setActiveResultMode(null)
|
||||
setCommitResult(null)
|
||||
}
|
||||
}, [imagePreview, fileType])
|
||||
@@ -104,7 +114,12 @@ function App() {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
setResult(response.data)
|
||||
setEditedOcrText(response.data.text || '')
|
||||
if (COMMITTABLE_MODES.has(mode)) {
|
||||
const text = response.data.text || ''
|
||||
setModeResults(prev => ({ ...prev, [mode]: text }))
|
||||
setEditedResults(prev => ({ ...prev, [mode]: text }))
|
||||
setActiveResultMode(mode)
|
||||
}
|
||||
setCommitResult(null)
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.detail || err.message || 'An error occurred')
|
||||
@@ -115,7 +130,9 @@ function App() {
|
||||
|
||||
const handleNewAnalysis = () => {
|
||||
setResult(null)
|
||||
setEditedOcrText('')
|
||||
setModeResults({})
|
||||
setEditedResults({})
|
||||
setActiveResultMode(null)
|
||||
setCommitResult(null)
|
||||
}
|
||||
|
||||
@@ -130,7 +147,9 @@ function App() {
|
||||
formData.append('book', metadata.book)
|
||||
formData.append('chapter', metadata.chapter)
|
||||
formData.append('page', metadata.page)
|
||||
formData.append('ocr_text', editedOcrText)
|
||||
formData.append('ocr_text', editedResults.plain_ocr || '')
|
||||
formData.append('describe_text', editedResults.describe || '')
|
||||
formData.append('freeform_text', editedResults.freeform || '')
|
||||
formData.append('mode', mode)
|
||||
|
||||
const response = await axios.post(`${API_BASE}/jobs`, formData, {
|
||||
@@ -142,15 +161,15 @@ function App() {
|
||||
} finally {
|
||||
setCommitLoading(false)
|
||||
}
|
||||
}, [image, editedOcrText, metadata, mode])
|
||||
}, [image, editedResults, metadata, mode])
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const text = editedOcrText || result?.text
|
||||
const text = (activeResultMode && editedResults[activeResultMode]) || result?.text
|
||||
if (text) navigator.clipboard.writeText(text)
|
||||
}, [editedOcrText, result])
|
||||
}, [activeResultMode, editedResults, result])
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
const text = editedOcrText || result?.text
|
||||
const text = (activeResultMode && editedResults[activeResultMode]) || result?.text
|
||||
if (!text) return
|
||||
const ext = { plain_ocr: 'txt', describe: 'txt', find_ref: 'txt', freeform: 'txt' }[mode] || 'txt'
|
||||
const blob = new Blob([text], { type: 'text/plain' })
|
||||
@@ -260,16 +279,40 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
<div className="glass rounded-2xl p-4 flex flex-col h-full">
|
||||
{/* Mode tabs — only shown when multiple modes have results */}
|
||||
{Object.keys(modeResults).length > 1 && (
|
||||
<div className="flex gap-1 mb-3 flex-shrink-0">
|
||||
{Object.keys(modeResults).map(m => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setActiveResultMode(m)}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium transition-colors ${
|
||||
activeResultMode === m
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-white/5 text-gray-400 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
{MODE_LABELS[m] || m}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mb-2 flex-shrink-0">
|
||||
{{ plain_ocr: 'OCR Text', describe: 'Description', freeform: 'Result' }[mode] || 'Result'}
|
||||
{MODE_LABELS[activeResultMode] || 'Result'}
|
||||
<span className="text-purple-400 ml-1">(edit before committing)</span>
|
||||
</p>
|
||||
<textarea
|
||||
value={editedOcrText}
|
||||
onChange={e => setEditedOcrText(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 appears here..."
|
||||
/>
|
||||
{loading && COMMITTABLE_MODES.has(mode) ? (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-purple-400" />
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
value={activeResultMode ? (editedResults[activeResultMode] ?? '') : ''}
|
||||
onChange={e => setEditedResults(prev => ({ ...prev, [activeResultMode]: 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="Run a mode to see results here..."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user