diff --git a/backend/main.py b/backend/main.py
index 59b4fe8..b86ba20 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -603,6 +603,10 @@ async def process_pdf(
class ReviewRequest(BaseModel):
reviewed_text: str
reviewer_name: str
+ author: Optional[str] = None
+ book: Optional[str] = None
+ chapter: Optional[str] = None
+ page: Optional[str] = None
def _job_row_to_dict(row) -> Dict[str, Any]:
@@ -811,11 +815,23 @@ async def review_job(job_id: str, body: ReviewRequest):
SET status = 'reviewed',
reviewed_text = %s,
reviewer_name = %s,
- reviewed_at = NOW()
+ reviewed_at = NOW(),
+ author = %s,
+ book = %s,
+ chapter = %s,
+ page = %s
WHERE id = %s
RETURNING *
""",
- (body.reviewed_text, body.reviewer_name, job_id),
+ (
+ body.reviewed_text,
+ body.reviewer_name,
+ body.author or None,
+ body.book or None,
+ body.chapter or None,
+ body.page or None,
+ job_id,
+ ),
)
row = cur.fetchone()
except Exception as exc:
diff --git a/frontend/src/components/JobsPanel.jsx b/frontend/src/components/JobsPanel.jsx
index aa59d08..30999d1 100644
--- a/frontend/src/components/JobsPanel.jsx
+++ b/frontend/src/components/JobsPanel.jsx
@@ -29,37 +29,39 @@ function StatusBadge({ status }) {
)
}
-function MetaRow({ icon: Icon, label, value }) {
- if (!value) return null
- return (
-
-
- {label}:
- {value}
-
- )
-}
-
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 [editChapter, setEditChapter] = useState('')
+ const [editPage, setEditPage] = useState('')
const [reviewerName, setReviewerName] = useState('')
+
const [submitting, setSubmitting] = useState(false)
- const [reviewResult, setReviewResult] = useState(null)
+ const [saveResult, setSaveResult] = useState(null)
useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
- setReviewResult(null)
+ setSaveResult(null)
axios.get(`${API_BASE}/jobs/${jobId}`)
.then(res => {
if (!cancelled) {
- setJob(res.data)
- setEditedText(res.data.reviewed_text ?? res.data.ocr_text ?? '')
+ const d = res.data
+ setJob(d)
+ setEditedText(d.reviewed_text ?? d.ocr_text ?? '')
+ setEditAuthor(d.author || '')
+ setEditBook(d.book || '')
+ setEditChapter(d.chapter || '')
+ setEditPage(d.page || '')
+ setReviewerName(d.reviewer_name || '')
}
})
.catch(err => {
@@ -72,23 +74,27 @@ function JobDetail({ jobId, onClose, onReviewed }) {
return () => { cancelled = true }
}, [jobId])
- const handleMarkReviewed = async () => {
+ const handleSave = async () => {
if (!reviewerName.trim()) {
- setReviewResult({ success: false, error: 'Reviewer name is required.' })
+ setSaveResult({ success: false, error: 'Reviewer name is required.' })
return
}
setSubmitting(true)
- setReviewResult(null)
+ setSaveResult(null)
try {
const res = await axios.put(`${API_BASE}/jobs/${jobId}/review`, {
reviewed_text: editedText,
reviewer_name: reviewerName.trim(),
+ author: editAuthor,
+ book: editBook,
+ chapter: editChapter,
+ page: editPage,
})
setJob(res.data)
- setReviewResult({ success: true })
+ setSaveResult({ success: true })
onReviewed(res.data)
} catch (err) {
- setReviewResult({ success: false, error: err.response?.data?.detail || err.message })
+ setSaveResult({ success: false, error: err.response?.data?.detail || err.message })
} finally {
setSubmitting(false)
}
@@ -98,48 +104,38 @@ function JobDetail({ jobId, onClose, onReviewed }) {
'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 Detail
+
+ {job && }
+
Job Detail
+
-
- {loading && (
-
-
-
- )}
+ {loading && (
+
+
+
+ )}
- {error && (
-
- )}
+ {error && (
+
+ )}
- {job && !loading && (
- <>
- {/* Status + IDs */}
-
-
- {job.id}
-
+ {job && !loading && (
+
- {/* Metadata */}
-
-
-
-
-
-
- {job.mode && }
-
-
- {/* Image */}
+ {/* ── Left column: image + read-only info ── */}
+
Source Image
@@ -152,93 +148,141 @@ function JobDetail({ jobId, onClose, onReviewed }) {
/>
- {/* OCR / Reviewed text */}
-
-
- {job.status === 'reviewed' ? 'Reviewed Text' : 'OCR Text (editable)'}
-
- {job.status === 'reviewed' ? (
-
-
- {job.reviewed_text}
-
-
- ) : (
-
)
}
@@ -280,7 +324,6 @@ export default function JobsPanel() {
}
}, [search, filterStatus, filterAuthor, filterBook])
- // Initial load
useEffect(() => {
fetchJobs(0)
}, []) // eslint-disable-line react-hooks/exhaustive-deps
@@ -301,10 +344,11 @@ export default function JobsPanel() {
'placeholder-gray-600 focus:outline-none focus:border-purple-500/50 transition-colors'
return (
-
- {/* Left: Search + List */}
-
- {/* Search form */}
+ // 1/3 list — 2/3 detail on large screens
+
+
+ {/* ── Left: Search + List ── */}
+
- {/* Results */}
+ {loading && (
+
+
+
+ )}
+
+ {error && (
+
+ )}
+
+ {!loading && !error && jobs.length === 0 && (
+
+
+
No jobs found
+
Commit your first OCR job from the New Job tab
+
+ )}
+
- {loading && (
-
-
-
- )}
-
- {error && (
-
- )}
-
- {!loading && !error && jobs.length === 0 && (
-
-
-
No jobs found
-
Commit your first OCR job from the New Job tab
-
- )}
-
{jobs.map(job => (
- {/* Pagination */}
{totalPages > 1 && (