UI polish: live job polling, panel layout, pending button states

- Poll /api/ai-tagging/fields every 2s after any 202 (queued) response in
  ImageLightbox and DoomScrollView so extraction, translation, and description
  results appear automatically without a page refresh
- DoomScrollView extract button now turns accent-coloured while a job is
  queued instead of flashing red; red is reserved for genuine errors
- Kebab menu "Translate" option is now gated on entry.hasExtractedText
  (populated via a batch DB query in the browse API) so it only appears
  when there is text to translate
- Tag panel redesigned: toolbar collapses to just the filename when open;
  panel header holds hide (›), AI Tagger (), and Close (✕) buttons;
  sections ordered Description → Text Extraction → Tags; description
  state and generate handler moved from TagSelector into ImageLightbox
- VideoPlayerModal receives the same toolbar/panel restructure
- TagSelector gains hideDescription prop so the parent can own description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Garret Patti
2026-04-13 20:37:20 -04:00
parent d754f85717
commit 96cfb8aae7
7 changed files with 552 additions and 220 deletions

View File

@@ -1,4 +1,5 @@
import fs from 'fs'
import path from 'path'
import { NextRequest, NextResponse } from 'next/server'
import { getLibrary, resolveLibraryRoot, resolveAndJail } from '@/lib/libraries'
import { scanDirectory, scanDirectoryRecursive } from '@/lib/files'
@@ -31,6 +32,21 @@ export async function GET(request: NextRequest) {
const listing = recursive
? scanDirectoryRecursive(root, libraryId, subpath)
: scanDirectory(root, libraryId, subpath)
// Annotate image entries with whether they have extracted text
const db = getDb()
const rows = db
.prepare('SELECT item_key FROM media_items WHERE library_id = ? AND extracted_text IS NOT NULL')
.all(libraryId) as { item_key: string }[]
const withText = new Set(rows.map((r) => r.item_key))
listing.entries = listing.entries.map((e) => {
if (e.type !== 'file' || e.mediaType !== 'image') return e
const relPath = subpath ? path.join(subpath, e.name) : e.name
const itemKey = `${libraryId}:mixed_file:${encodeURIComponent(relPath)}`
return { ...e, hasExtractedText: withText.has(itemKey) }
})
return NextResponse.json(listing)
}