diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 6d08cc1..3be8e74 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,6 +1,9 @@
import { useState, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
-import { Sparkles, Zap, Loader2, Settings, Image as ImageIcon, FileText, Layers } from 'lucide-react'
+import {
+ Sparkles, Zap, Loader2, Settings, Image as ImageIcon, FileText,
+ Layers, ChevronLeft, CheckCircle2, Database,
+} from 'lucide-react'
import ImageUpload from './components/ImageUpload'
import ModeSelector from './components/ModeSelector'
import ResultPanel from './components/ResultPanel'
@@ -12,12 +15,16 @@ import axios from 'axios'
const API_BASE = import.meta.env.VITE_API_URL || '/api'
+const INPUT_CLASS =
+ 'w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-gray-200 ' +
+ 'placeholder-gray-600 focus:outline-none focus:border-purple-500/50 transition-colors'
+
function App() {
- const [view, setView] = useState('new_job') // 'new_job' | 'jobs'
+ const [view, setView] = useState('new_job')
// OCR state
const [mode, setMode] = useState('plain_ocr')
- const [fileType, setFileType] = useState('image') // 'image' or 'pdf'
+ const [fileType, setFileType] = useState('image')
const [image, setImage] = useState(null)
const [imagePreview, setImagePreview] = useState(null)
const [result, setResult] = useState(null)
@@ -26,26 +33,20 @@ function App() {
const [showAdvanced, setShowAdvanced] = useState(false)
const [includeCaption, setIncludeCaption] = useState(false)
- // Form state
const [prompt, setPrompt] = useState('')
const [findTerm, setFindTerm] = useState('')
const [advancedSettings, setAdvancedSettings] = useState({
- base_size: 1024,
- image_size: 640,
- crop_mode: true,
- test_compress: false
+ base_size: 1024, image_size: 640, crop_mode: true, test_compress: false,
})
- // 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)
+ // Whether to show the full-screen result view
+ const showResultView = view === 'new_job' && mode === 'plain_ocr' && !!result
+
const handleFileTypeChange = useCallback((newType) => {
setImage(null)
if (imagePreview) URL.revokeObjectURL(imagePreview)
@@ -62,6 +63,8 @@ function App() {
setImagePreview(null)
setError(null)
setResult(null)
+ setEditedOcrText('')
+ setCommitResult(null)
} else {
setImage(file)
setImagePreview(fileType === 'image' ? URL.createObjectURL(file) : file)
@@ -73,14 +76,10 @@ function App() {
}, [imagePreview, fileType])
const handleSubmit = async () => {
- if (!image) {
- setError('Please upload an image first')
- return
- }
+ if (!image) { setError('Please upload an image first'); return }
setLoading(true)
setError(null)
setCommitResult(null)
-
try {
const formData = new FormData()
formData.append('image', image)
@@ -108,6 +107,12 @@ function App() {
}
}
+ const handleNewAnalysis = () => {
+ setResult(null)
+ setEditedOcrText('')
+ setCommitResult(null)
+ }
+
const handleCommitJob = useCallback(async () => {
if (!image) return
setCommitLoading(true)
@@ -149,7 +154,9 @@ function App() {
a.download = `deepseek-ocr-result.${ext}`
a.click()
URL.revokeObjectURL(url)
- }, [result, mode])
+ }, [editedOcrText, result, mode])
+
+ const metaField = (key) => (e) => setMetadata(m => ({ ...m, [key]: e.target.value }))
return (
@@ -160,12 +167,12 @@ function App() {
@@ -173,11 +180,7 @@ function App() {
-
+
@@ -190,30 +193,29 @@ function App() {
- {/* Navigation */}
- {/* Job Metadata */}
- {/* Mode Selector with integrated inputs */}
- {/* Image/PDF Upload */}
-
+
- {/* Advanced Settings Toggle */}
setShowAdvanced(!showAdvanced)}
className="w-full glass px-4 py-3 rounded-2xl flex items-center justify-between hover:bg-white/5 transition-colors"
- whileHover={{ scale: 1.01 }}
- whileTap={{ scale: 0.99 }}
+ whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.99 }}
>
Advanced Settings
-
+
- {/* Advanced Settings Panel */}
{showAdvanced && (
)}
- {/* Action Button / PDF Processor */}
{fileType === 'pdf' ? (
) : (
<>
{loading ? (
- <>
-
- Processing Magic...
- >
+ <>Processing Magic...>
) : (
- <>
-
- Analyze Image
- >
+ <>Analyze Image>
)}
{error && (
{error}
@@ -384,7 +451,7 @@ function App() {
)}
- {/* Right Panel - Results */}
+ {/* Right Panel - Results (non-plain_ocr modes or loading) */}
diff --git a/frontend/src/components/JobsPanel.jsx b/frontend/src/components/JobsPanel.jsx
index 30999d1..0e4a9ec 100644
--- a/frontend/src/components/JobsPanel.jsx
+++ b/frontend/src/components/JobsPanel.jsx
@@ -2,25 +2,23 @@ import { useState, useEffect, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import {
Search, ChevronLeft, ChevronRight, CheckCircle2, Clock,
- User, BookOpen, FileText, Calendar, Hash, Loader2, X, Save,
- RefreshCw, Image as ImageIcon,
+ FileText, Loader2, Save, RefreshCw,
} from 'lucide-react'
import axios from 'axios'
const API_BASE = import.meta.env.VITE_API_URL || '/api'
+const INPUT_CLASS =
+ 'w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-gray-200 ' +
+ 'placeholder-gray-600 focus:outline-none focus:border-purple-500/50 transition-colors'
+
const STATUS_COLORS = {
unreviewed: 'text-amber-400 bg-amber-400/10 border-amber-400/30',
- reviewed: 'text-green-400 bg-green-400/10 border-green-400/30',
-}
-
-const STATUS_ICONS = {
- unreviewed: Clock,
- reviewed: CheckCircle2,
+ reviewed: 'text-green-400 bg-green-400/10 border-green-400/30',
}
function StatusBadge({ status }) {
- const Icon = STATUS_ICONS[status] || Clock
+ const Icon = status === 'reviewed' ? CheckCircle2 : Clock
return (
@@ -29,17 +27,19 @@ function StatusBadge({ status }) {
)
}
+// ─────────────────────────────────────────────────────────────
+// Full-screen Job Detail
+// ─────────────────────────────────────────────────────────────
function JobDetail({ jobId, onClose, onReviewed }) {
const [job, setJob] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
- // Editable fields
- const [editedText, setEditedText] = useState('')
- const [editAuthor, setEditAuthor] = useState('')
- const [editBook, setEditBook] = useState('')
+ const [editedText, setEditedText] = useState('')
+ const [editAuthor, setEditAuthor] = useState('')
+ const [editBook, setEditBook] = useState('')
const [editChapter, setEditChapter] = useState('')
- const [editPage, setEditPage] = useState('')
+ const [editPage, setEditPage] = useState('')
const [reviewerName, setReviewerName] = useState('')
const [submitting, setSubmitting] = useState(false)
@@ -67,9 +67,7 @@ function JobDetail({ jobId, onClose, onReviewed }) {
.catch(err => {
if (!cancelled) setError(err.response?.data?.detail || err.message)
})
- .finally(() => {
- if (!cancelled) setLoading(false)
- })
+ .finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [jobId])
@@ -100,193 +98,163 @@ function JobDetail({ jobId, onClose, onReviewed }) {
}
}
- const inputClass =
- 'w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-gray-200 ' +
- 'placeholder-gray-600 focus:outline-none focus:border-purple-500/50 transition-colors'
-
const isReviewed = job?.status === 'reviewed'
return (
-
- {/* Header */}
-
-
- {job && }
-
Job Detail
-
-
+
+ {/* Top bar */}
+
+
+
+ Back to results
+
+ {job && (
+ <>
+
+ {job.id}
+ >
+ )}
{loading && (
-
+
)}
{error && (
-
+
)}
{job && !loading && (
-
-
- {/* ── Left column: image + read-only info ── */}
-
-
-
- Source Image
-
+ <>
+ {/* Image + Text */}
+
+

{ e.target.style.display = 'none' }}
/>
-
- {/* Read-only job info */}
-
-
{job.id}
-
Submitted: {new Date(job.submitted_at).toLocaleString()}
- {job.mode &&
Mode: {job.mode}
}
- {isReviewed && job.reviewed_at && (
-
Last reviewed: {new Date(job.reviewed_at).toLocaleString()}
- )}
-
-
- {/* Original OCR text (collapsed, for reviewed jobs) */}
- {isReviewed && job.ocr_text && (
-
-
- Original OCR Text
-
-
-
- )}
-
-
- {/* ── Right column: all editable fields ── */}
-
-
- {/* Metadata */}
-
-
Metadata
-
-
- setEditAuthor(e.target.value)}
- placeholder="Author name"
- className={inputClass}
- />
-
-
-
- setEditBook(e.target.value)}
- placeholder="Book title"
- className={inputClass}
- />
-
-
-
-
- {/* OCR / reviewed text */}
-
-
-
+ {/* Metadata + reviewer row */}
+
+
+
+ {saveResult && (
+
+ {saveResult.success
+ ? (isReviewed ? 'Changes saved!' : 'Job marked as reviewed!')
+ : saveResult.error}
+
+ )}
+
+ {/* Read-only info row */}
+
+ {job.submitted_at && (
+ Submitted: {new Date(job.submitted_at).toLocaleString()}
+ )}
+ {isReviewed && job.reviewed_at && (
+ Last reviewed: {new Date(job.reviewed_at).toLocaleString()}
+ )}
+ {job.mode && Mode: {job.mode}}
+
+
+ >
)}
-
+
)
}
+// ─────────────────────────────────────────────────────────────
+// Search / List view
+// ─────────────────────────────────────────────────────────────
export default function JobsPanel() {
const [search, setSearch] = useState('')
const [filterStatus, setFilterStatus] = useState('')
@@ -324,14 +292,7 @@ export default function JobsPanel() {
}
}, [search, filterStatus, filterAuthor, filterBook])
- useEffect(() => {
- fetchJobs(0)
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
-
- const handleSearch = (e) => {
- e.preventDefault()
- fetchJobs(0)
- }
+ useEffect(() => { fetchJobs(0) }, []) // eslint-disable-line react-hooks/exhaustive-deps
const handleReviewed = (updatedJob) => {
setJobs(prev => prev.map(j => j.id === updatedJob.id ? { ...j, ...updatedJob } : j))
@@ -339,196 +300,122 @@ export default function JobsPanel() {
const totalPages = Math.ceil(total / LIMIT)
- const inputClass =
- 'bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-gray-200 ' +
- 'placeholder-gray-600 focus:outline-none focus:border-purple-500/50 transition-colors'
+ // When a job is selected show full-screen detail
+ if (selectedJobId) {
+ return (
+
+ setSelectedJobId(null)}
+ onReviewed={handleReviewed}
+ />
+
+ )
+ }
return (
- // 1/3 list — 2/3 detail on large screens
-
+
+ {/* Search form */}
+
+
- {/* ── Left: Search + List ── */}
-
-
-
-
-
-
- setFilterAuthor(e.target.value)}
- placeholder="Author..."
- className={inputClass}
- />
- setFilterBook(e.target.value)}
- placeholder="Book..."
- className={inputClass}
- />
-
-
-
-
- {total} job{total !== 1 ? 's' : ''} found
-
-
-
+
+
+ setFilterAuthor(e.target.value)} placeholder="Author..." className={INPUT_CLASS} />
+ setFilterBook(e.target.value)} placeholder="Book..." className={INPUT_CLASS} />
- {loading && (
-
-
-
- )}
-
- {error && (
-
- )}
-
- {!loading && !error && jobs.length === 0 && (
-
-
-
No jobs found
-
Commit your first OCR job from the New Job tab
-
- )}
-
-
-
- {jobs.map(job => (
- setSelectedJobId(job.id === selectedJobId ? null : job.id)}
- className={`w-full text-left glass p-4 rounded-xl transition-all border ${
- selectedJobId === job.id
- ? 'border-purple-500/50 bg-purple-500/5'
- : 'border-white/5 hover:border-white/20 hover:bg-white/5'
- }`}
- initial={{ opacity: 0, y: 10 }}
- animate={{ opacity: 1, y: 0 }}
- exit={{ opacity: 0 }}
- layout
- >
-
-
-
- {job.book && (
- {job.book}
- )}
- {job.chapter && (
- Ch. {job.chapter}
- )}
- {job.page && (
- p. {job.page}
- )}
-
- {job.author && (
-
{job.author}
- )}
-
- {new Date(job.submitted_at).toLocaleString()}
-
-
-
-
-
-
-
- ))}
-
+
+ {total} job{total !== 1 ? 's' : ''} found
+
-
- {totalPages > 1 && (
-
-
-
- Page {page + 1} of {totalPages}
-
-
-
- )}
- {/* ── Right: Detail panel ── */}
-
-
- {selectedJobId ? (
-
- setSelectedJobId(null)}
- onReviewed={handleReviewed}
- />
-
- ) : (
- }
+
+ {error && (
+
+ )}
+
+ {!loading && !error && jobs.length === 0 && (
+
+
+
No jobs found
+
Commit your first OCR job from the New Job tab
+
+ )}
+
+ {/* Results grid */}
+
+
+ {jobs.map(job => (
+ setSelectedJobId(job.id)}
+ className="text-left glass p-4 rounded-xl border border-white/5 hover:border-white/20 hover:bg-white/5 transition-all"
+ initial={{ opacity: 0, y: 10 }}
+ animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
- className="glass p-8 rounded-2xl flex flex-col items-center justify-center text-center"
- style={{ minHeight: '300px' }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ layout
>
-
- Select a job to view and edit details
-
- )}
+
+
+
+ {job.book && {job.book}
}
+
+ {job.chapter && Ch. {job.chapter}}
+ {job.page && p. {job.page}}
+
+ {job.author && {job.author}
}
+ {new Date(job.submitted_at).toLocaleDateString()}
+
+ ))}
-
+ {totalPages > 1 && (
+
+
+ Page {page + 1} of {totalPages}
+
+
+ )}
+
)
}
diff --git a/frontend/src/components/ResultPanel.jsx b/frontend/src/components/ResultPanel.jsx
index 7742ce3..79bc656 100644
--- a/frontend/src/components/ResultPanel.jsx
+++ b/frontend/src/components/ResultPanel.jsx
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
-import { Copy, Download, Sparkles, Loader2, CheckCircle2, ChevronDown, Database } from 'lucide-react'
+import { Copy, Download, Sparkles, Loader2, CheckCircle2, ChevronDown } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import DOMPurify from 'dompurify'
-export default function ResultPanel({ result, loading, imagePreview, onCopy, onDownload, onCommitJob, commitLoading, commitResult, editedOcrText, onOcrTextChange }) {
+export default function ResultPanel({ result, loading, imagePreview, onCopy, onDownload }) {
const canvasRef = useRef(null)
const imgRef = useRef(null)
const [showAdvanced, setShowAdvanced] = useState(false)
@@ -205,78 +205,46 @@ export default function ResultPanel({ result, loading, imagePreview, onCopy, onD
exit={{ opacity: 0, y: -20 }}
className="space-y-4"
>
- {/* plain_ocr commit mode: image + editable textarea side by side */}
- {onCommitJob ? (
-
- {imagePreview && typeof imagePreview === 'string' ? (
-
-
Source Image
-

-
- ) : (
-
- )}
-
-
- OCR Text (edit before committing)
-
-
+ {/* Preview with boxes (grounding modes) */}
+ {imagePreview && result.boxes && result.boxes.length > 0 && (
+
+

{
+ console.log('🖼️ Image loaded, triggering draw')
+ setImageLoaded(true)
+ }}
+ />
+
- ) : (
- <>
- {/* Preview with boxes (grounding modes) */}
- {imagePreview && result.boxes && result.boxes.length > 0 && (
-
-

{
- console.log('🖼️ Image loaded, triggering draw')
- setImageLoaded(true)
- }}
- />
-
-
- )}
-
- {/* Rendered text result */}
-
- {isHTML ? (
-
- ) : isMarkdown ? (
-
- {result.text}
-
- ) : (
-
- {result.text}
-
- )}
-
- >
)}
+ {/* Rendered text result */}
+
+ {isHTML ? (
+
+ ) : isMarkdown ? (
+
+ {result.text}
+
+ ) : (
+
+ {result.text}
+
+ )}
+
+
{/* Raw Response Viewer */}
{result.raw_text && (
@@ -343,44 +311,6 @@ export default function ResultPanel({ result, loading, imagePreview, onCopy, onD
- {/* Commit Job button (plain_ocr only) */}
- {onCommitJob && (
-
-
- {commitLoading ? (
- <> Committing Job...>
- ) : commitResult?.success ? (
- <> Job Committed>
- ) : (
- <> Commit Job>
- )}
-
-
- {commitResult?.success && (
-
-
- Job saved — ID: {commitResult.job?.id}
-
-
- )}
- {commitResult && !commitResult.success && (
-
- )}
-
- )}
-
{/* Success indicator */}