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 */}
+ + {suggestions.authors.map(a => + + {suggestions.chapters.map(c =>
{[ - { 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 }) => (
- + + + {(suggestions.authors || []).map(a => + + {(suggestions.chapters || []).map(c => + + {(suggestions.reviewers || []).map(r =>
- 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() { + + {suggestions.authors.map(a =>
- 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

+ + {authors.map(a => + + {chapters.map(c => +
Chapter { + fetch(`${API_BASE}/jobs/suggestions`) + .then(r => r.ok ? r.json() : null) + .then(data => { if (data) setSuggestions(data) }) + .catch(() => {}) + }, []) + + return suggestions +}