diff --git a/backend/main.py b/backend/main.py
index 48b4566..36f55a5 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -754,6 +754,31 @@ async def list_jobs(
return JSONResponse({"total": total, "limit": limit, "offset": offset, "jobs": rows})
+@app.get("/api/jobs/suggestions")
+async def job_suggestions():
+ """Return distinct values for author, chapter, and reviewer_name to power autocomplete."""
+ try:
+ with get_db() as conn:
+ with conn.cursor() as cur:
+ cur.execute("""
+ SELECT
+ array_remove(array_agg(DISTINCT author ORDER BY author), NULL) AS authors,
+ array_remove(array_agg(DISTINCT chapter ORDER BY chapter), NULL) AS chapters,
+ array_remove(array_agg(DISTINCT reviewer_name ORDER BY reviewer_name), NULL) AS reviewers
+ FROM ocr_jobs
+ """)
+ row = cur.fetchone()
+ except Exception as exc:
+ print(f"suggestions DB error: {exc}")
+ raise HTTPException(status_code=500, detail="Database error.")
+
+ return JSONResponse({
+ "authors": row["authors"] or [],
+ "chapters": row["chapters"] or [],
+ "reviewers": row["reviewers"] or [],
+ })
+
+
@app.get("/api/jobs/{job_id}")
async def get_job(job_id: str):
"""Retrieve full job record including OCR text."""
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index bf50028..229f3f9 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,4 +1,5 @@
import { useState, useCallback } from 'react'
+import { useSuggestions } from './hooks/useSuggestions'
import { motion, AnimatePresence } from 'framer-motion'
import {
Sparkles, Zap, Loader2, Settings, Image as ImageIcon, FileText,
@@ -39,6 +40,8 @@ function App() {
base_size: 1024, image_size: 640, crop_mode: true, test_compress: false,
})
+ const suggestions = useSuggestions()
+
const [metadata, setMetadata] = useState({ author: '', book: '', chapter: '', page: '' })
const [editedOcrText, setEditedOcrText] = useState('')
const [commitLoading, setCommitLoading] = useState(false)
@@ -268,17 +271,24 @@ function App() {
{/* Metadata row */}
+
+
{[
- { key: 'author', label: 'Author', placeholder: 'Author name' },
- { key: 'book', label: 'Book', placeholder: 'Book title' },
- { key: 'chapter', label: 'Chapter', placeholder: 'Chapter' },
- { key: 'page', label: 'Page', placeholder: 'Page number' },
- ].map(({ key, label, placeholder }) => (
+ { key: 'author', label: 'Author', placeholder: 'Author name', list: 'rv-authors' },
+ { key: 'book', label: 'Book', placeholder: 'Book title', list: undefined },
+ { key: 'chapter', label: 'Chapter', placeholder: 'Chapter', list: 'rv-chapters' },
+ { key: 'page', label: 'Page', placeholder: 'Page number', list: undefined },
+ ].map(({ key, label, placeholder, list }) => (
-
+
+
+
+
- setEditAuthor(e.target.value)} placeholder="Author" className={INPUT_CLASS} />
+ setEditAuthor(e.target.value)} placeholder="Author" className={INPUT_CLASS} />
@@ -188,7 +198,7 @@ function JobDetail({ jobId, onClose, onReviewed }) {
- setEditChapter(e.target.value)} placeholder="Chapter" className={INPUT_CLASS} />
+ setEditChapter(e.target.value)} placeholder="Chapter" className={INPUT_CLASS} />
@@ -196,7 +206,7 @@ function JobDetail({ jobId, onClose, onReviewed }) {
- setReviewerName(e.target.value)} placeholder="Your name" className={INPUT_CLASS} />
+ setReviewerName(e.target.value)} placeholder="Your name" className={INPUT_CLASS} />
setSelectedJobId(null)}
onReviewed={handleReviewed}
+ suggestions={suggestions}
/>
)
@@ -340,13 +352,16 @@ export default function JobsPanel() {
+
- setFilterAuthor(e.target.value)} placeholder="Author..." className={INPUT_CLASS} />
+ setFilterAuthor(e.target.value)} placeholder="Author..." className={INPUT_CLASS} />
setFilterBook(e.target.value)} placeholder="Book..." className={INPUT_CLASS} />
diff --git a/frontend/src/components/MetadataForm.jsx b/frontend/src/components/MetadataForm.jsx
index 6242646..b028890 100644
--- a/frontend/src/components/MetadataForm.jsx
+++ b/frontend/src/components/MetadataForm.jsx
@@ -1,7 +1,8 @@
import { BookOpen } from 'lucide-react'
-export default function MetadataForm({ metadata, onChange }) {
+export default function MetadataForm({ metadata, onChange, suggestions = {} }) {
const { author, book, chapter, page } = metadata
+ const { authors = [], chapters = [] } = suggestions
const field = (key) => (e) => onChange({ ...metadata, [key]: e.target.value })
@@ -16,11 +17,19 @@ export default function MetadataForm({ metadata, onChange }) {
Job Metadata
+
+
+