Add book title to autocomplete suggestions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -756,13 +756,14 @@ async def list_jobs(
|
|||||||
|
|
||||||
@app.get("/api/jobs/suggestions")
|
@app.get("/api/jobs/suggestions")
|
||||||
async def job_suggestions():
|
async def job_suggestions():
|
||||||
"""Return distinct values for author, chapter, and reviewer_name to power autocomplete."""
|
"""Return distinct values for author, book, chapter, and reviewer_name to power autocomplete."""
|
||||||
try:
|
try:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
array_remove(array_agg(DISTINCT author ORDER BY author), NULL) AS authors,
|
array_remove(array_agg(DISTINCT author ORDER BY author), NULL) AS authors,
|
||||||
|
array_remove(array_agg(DISTINCT book ORDER BY book), NULL) AS books,
|
||||||
array_remove(array_agg(DISTINCT chapter ORDER BY chapter), NULL) AS chapters,
|
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
|
array_remove(array_agg(DISTINCT reviewer_name ORDER BY reviewer_name), NULL) AS reviewers
|
||||||
FROM ocr_jobs
|
FROM ocr_jobs
|
||||||
@@ -774,6 +775,7 @@ async def job_suggestions():
|
|||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"authors": row["authors"] or [],
|
"authors": row["authors"] or [],
|
||||||
|
"books": row["books"] or [],
|
||||||
"chapters": row["chapters"] or [],
|
"chapters": row["chapters"] or [],
|
||||||
"reviewers": row["reviewers"] or [],
|
"reviewers": row["reviewers"] or [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -274,13 +274,16 @@ function App() {
|
|||||||
<datalist id="rv-authors">
|
<datalist id="rv-authors">
|
||||||
{suggestions.authors.map(a => <option key={a} value={a} />)}
|
{suggestions.authors.map(a => <option key={a} value={a} />)}
|
||||||
</datalist>
|
</datalist>
|
||||||
|
<datalist id="rv-books">
|
||||||
|
{(suggestions.books || []).map(b => <option key={b} value={b} />)}
|
||||||
|
</datalist>
|
||||||
<datalist id="rv-chapters">
|
<datalist id="rv-chapters">
|
||||||
{suggestions.chapters.map(c => <option key={c} value={c} />)}
|
{suggestions.chapters.map(c => <option key={c} value={c} />)}
|
||||||
</datalist>
|
</datalist>
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ key: 'author', label: 'Author', placeholder: 'Author name', list: 'rv-authors' },
|
{ key: 'author', label: 'Author', placeholder: 'Author name', list: 'rv-authors' },
|
||||||
{ key: 'book', label: 'Book', placeholder: 'Book title', list: undefined },
|
{ key: 'book', label: 'Book', placeholder: 'Book title', list: 'rv-books' },
|
||||||
{ key: 'chapter', label: 'Chapter', placeholder: 'Chapter', list: 'rv-chapters' },
|
{ key: 'chapter', label: 'Chapter', placeholder: 'Chapter', list: 'rv-chapters' },
|
||||||
{ key: 'page', label: 'Page', placeholder: 'Page number', list: undefined },
|
{ key: 'page', label: 'Page', placeholder: 'Page number', list: undefined },
|
||||||
].map(({ key, label, placeholder, list }) => (
|
].map(({ key, label, placeholder, list }) => (
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ function JobDetail({ jobId, onClose, onReviewed, suggestions = {} }) {
|
|||||||
<datalist id="jd-authors">
|
<datalist id="jd-authors">
|
||||||
{(suggestions.authors || []).map(a => <option key={a} value={a} />)}
|
{(suggestions.authors || []).map(a => <option key={a} value={a} />)}
|
||||||
</datalist>
|
</datalist>
|
||||||
|
<datalist id="jd-books">
|
||||||
|
{(suggestions.books || []).map(b => <option key={b} value={b} />)}
|
||||||
|
</datalist>
|
||||||
<datalist id="jd-chapters">
|
<datalist id="jd-chapters">
|
||||||
{(suggestions.chapters || []).map(c => <option key={c} value={c} />)}
|
{(suggestions.chapters || []).map(c => <option key={c} value={c} />)}
|
||||||
</datalist>
|
</datalist>
|
||||||
@@ -194,7 +197,7 @@ function JobDetail({ jobId, onClose, onReviewed, suggestions = {} }) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-gray-400 mb-1 block">Book</label>
|
<label className="text-xs text-gray-400 mb-1 block">Book</label>
|
||||||
<input type="text" value={editBook} onChange={e => setEditBook(e.target.value)} placeholder="Book title" className={INPUT_CLASS} />
|
<input type="text" list="jd-books" value={editBook} onChange={e => setEditBook(e.target.value)} placeholder="Book title" className={INPUT_CLASS} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-gray-400 mb-1 block">Chapter</label>
|
<label className="text-xs text-gray-400 mb-1 block">Chapter</label>
|
||||||
@@ -355,6 +358,9 @@ export default function JobsPanel() {
|
|||||||
<datalist id="jp-authors">
|
<datalist id="jp-authors">
|
||||||
{suggestions.authors.map(a => <option key={a} value={a} />)}
|
{suggestions.authors.map(a => <option key={a} value={a} />)}
|
||||||
</datalist>
|
</datalist>
|
||||||
|
<datalist id="jp-books">
|
||||||
|
{(suggestions.books || []).map(b => <option key={b} value={b} />)}
|
||||||
|
</datalist>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className={INPUT_CLASS}>
|
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className={INPUT_CLASS}>
|
||||||
<option value="">All statuses</option>
|
<option value="">All statuses</option>
|
||||||
@@ -362,7 +368,7 @@ export default function JobsPanel() {
|
|||||||
<option value="reviewed">Reviewed</option>
|
<option value="reviewed">Reviewed</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" list="jp-authors" value={filterAuthor} onChange={e => setFilterAuthor(e.target.value)} placeholder="Author..." className={INPUT_CLASS} />
|
<input type="text" list="jp-authors" value={filterAuthor} onChange={e => setFilterAuthor(e.target.value)} placeholder="Author..." className={INPUT_CLASS} />
|
||||||
<input type="text" value={filterBook} onChange={e => setFilterBook(e.target.value)} placeholder="Book..." className={INPUT_CLASS} />
|
<input type="text" list="jp-books" value={filterBook} onChange={e => setFilterBook(e.target.value)} placeholder="Book..." className={INPUT_CLASS} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BookOpen } from 'lucide-react'
|
|||||||
|
|
||||||
export default function MetadataForm({ metadata, onChange, suggestions = {} }) {
|
export default function MetadataForm({ metadata, onChange, suggestions = {} }) {
|
||||||
const { author, book, chapter, page } = metadata
|
const { author, book, chapter, page } = metadata
|
||||||
const { authors = [], chapters = [] } = suggestions
|
const { authors = [], books = [], chapters = [] } = suggestions
|
||||||
|
|
||||||
const field = (key) => (e) => onChange({ ...metadata, [key]: e.target.value })
|
const field = (key) => (e) => onChange({ ...metadata, [key]: e.target.value })
|
||||||
|
|
||||||
@@ -20,6 +20,9 @@ export default function MetadataForm({ metadata, onChange, suggestions = {} }) {
|
|||||||
<datalist id="mf-authors">
|
<datalist id="mf-authors">
|
||||||
{authors.map(a => <option key={a} value={a} />)}
|
{authors.map(a => <option key={a} value={a} />)}
|
||||||
</datalist>
|
</datalist>
|
||||||
|
<datalist id="mf-books">
|
||||||
|
{books.map(b => <option key={b} value={b} />)}
|
||||||
|
</datalist>
|
||||||
<datalist id="mf-chapters">
|
<datalist id="mf-chapters">
|
||||||
{chapters.map(c => <option key={c} value={c} />)}
|
{chapters.map(c => <option key={c} value={c} />)}
|
||||||
</datalist>
|
</datalist>
|
||||||
@@ -40,6 +43,7 @@ export default function MetadataForm({ metadata, onChange, suggestions = {} }) {
|
|||||||
<label className="text-xs text-gray-400 mb-1 block">Book</label>
|
<label className="text-xs text-gray-400 mb-1 block">Book</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
list="mf-books"
|
||||||
value={book}
|
value={book}
|
||||||
onChange={field('book')}
|
onChange={field('book')}
|
||||||
placeholder="Book title"
|
placeholder="Book title"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'
|
|||||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
|
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
|
||||||
|
|
||||||
export function useSuggestions() {
|
export function useSuggestions() {
|
||||||
const [suggestions, setSuggestions] = useState({ authors: [], chapters: [], reviewers: [] })
|
const [suggestions, setSuggestions] = useState({ authors: [], books: [], chapters: [], reviewers: [] })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${API_BASE}/jobs/suggestions`)
|
fetch(`${API_BASE}/jobs/suggestions`)
|
||||||
|
|||||||
Reference in New Issue
Block a user