From 7e284383b4f2033ac5a2ba6d4418ed9416fec9c4 Mon Sep 17 00:00:00 2001
From: Garret Patti <42485635+garretpatti@users.noreply.github.com>
Date: Sun, 12 Apr 2026 18:18:59 -0400
Subject: [PATCH 1/3] add ai descriptions and extracted text
---
src/app/api/ai-settings/route.ts | 16 +-
src/app/api/ai-tagging/describe/route.ts | 39 ++
.../api/ai-tagging/extract-text-bulk/route.ts | 38 ++
src/app/api/ai-tagging/extract-text/route.ts | 39 ++
src/app/api/ai-tagging/fields/route.ts | 19 +
src/app/api/ai-tagging/translate/route.ts | 36 ++
src/app/manage/ai-tagging/page.tsx | 23 +-
src/components/mixed/ImageLightbox.tsx | 144 ++++++++
src/components/mixed/MixedView.tsx | 95 ++++-
src/components/tags/TagSelector.tsx | 73 +++-
src/lib/ai-tagger.ts | 342 +++++++++++++++++-
src/lib/app-settings.ts | 8 +
src/lib/db.ts | 18 +
13 files changed, 879 insertions(+), 11 deletions(-)
create mode 100644 src/app/api/ai-tagging/describe/route.ts
create mode 100644 src/app/api/ai-tagging/extract-text-bulk/route.ts
create mode 100644 src/app/api/ai-tagging/extract-text/route.ts
create mode 100644 src/app/api/ai-tagging/fields/route.ts
create mode 100644 src/app/api/ai-tagging/translate/route.ts
diff --git a/src/app/api/ai-settings/route.ts b/src/app/api/ai-settings/route.ts
index 593f37a..09e9435 100644
--- a/src/app/api/ai-settings/route.ts
+++ b/src/app/api/ai-settings/route.ts
@@ -1,27 +1,28 @@
import { NextRequest, NextResponse } from 'next/server'
import { requireAdmin } from '@/lib/auth'
-import { getAiConfig, updateAiConfig } from '@/lib/app-settings'
+import { getAiConfig, updateAiConfig, getPreferredLanguage, setPreferredLanguage } from '@/lib/app-settings'
export async function GET(request: NextRequest) {
const auth = await requireAdmin(request)
if (auth instanceof NextResponse) return auth
const { endpoint, model, enabled } = getAiConfig()
- return NextResponse.json({ endpoint, model, enabled })
+ const preferredLanguage = getPreferredLanguage()
+ return NextResponse.json({ endpoint, model, enabled, preferredLanguage })
}
export async function PUT(request: NextRequest) {
const auth = await requireAdmin(request)
if (auth instanceof NextResponse) return auth
- let body: { endpoint?: string; model?: string; enabled?: boolean }
+ let body: { endpoint?: string; model?: string; enabled?: boolean; preferredLanguage?: string }
try {
body = await request.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
}
- const { endpoint, model, enabled } = body
+ const { endpoint, model, enabled, preferredLanguage } = body
if (typeof endpoint !== 'string') {
return NextResponse.json({ error: 'endpoint is required' }, { status: 400 })
@@ -34,5 +35,10 @@ export async function PUT(request: NextRequest) {
}
updateAiConfig(endpoint, model, enabled)
- return NextResponse.json({ endpoint, model, enabled })
+
+ if (typeof preferredLanguage === 'string' && preferredLanguage.trim()) {
+ setPreferredLanguage(preferredLanguage.trim())
+ }
+
+ return NextResponse.json({ endpoint, model, enabled, preferredLanguage: getPreferredLanguage() })
}
diff --git a/src/app/api/ai-tagging/describe/route.ts b/src/app/api/ai-tagging/describe/route.ts
new file mode 100644
index 0000000..5121c65
--- /dev/null
+++ b/src/app/api/ai-tagging/describe/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { generateItemDescription } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { itemKey?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { itemKey } = body
+ if (!itemKey || typeof itemKey !== 'string') {
+ return NextResponse.json({ error: 'itemKey is required' }, { status: 400 })
+ }
+
+ const libraryId = itemKey.split(':')[0]
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const description = await generateItemDescription(itemKey)
+ return NextResponse.json({ description })
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ if (error.code === 'NO_IMAGE') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ console.error('[ai-tagging/describe] Error:', error)
+ return NextResponse.json({ error: 'Failed to generate description' }, { status: 502 })
+ }
+}
diff --git a/src/app/api/ai-tagging/extract-text-bulk/route.ts b/src/app/api/ai-tagging/extract-text-bulk/route.ts
new file mode 100644
index 0000000..196ca19
--- /dev/null
+++ b/src/app/api/ai-tagging/extract-text-bulk/route.ts
@@ -0,0 +1,38 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { extractDirectoryText } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { libraryId?: string; path?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { libraryId, path: dirPath } = body
+ if (!libraryId || typeof libraryId !== 'string') {
+ return NextResponse.json({ error: 'libraryId is required' }, { status: 400 })
+ }
+
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const processed = await extractDirectoryText(libraryId, dirPath ?? '')
+ return NextResponse.json({ processed })
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ if (error.code === 'INVALID_TYPE') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ console.error('[ai-tagging/extract-text-bulk] Error:', error)
+ return NextResponse.json({ error: 'Failed to extract text' }, { status: 502 })
+ }
+}
diff --git a/src/app/api/ai-tagging/extract-text/route.ts b/src/app/api/ai-tagging/extract-text/route.ts
new file mode 100644
index 0000000..58de630
--- /dev/null
+++ b/src/app/api/ai-tagging/extract-text/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { extractItemText } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { itemKey?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { itemKey } = body
+ if (!itemKey || typeof itemKey !== 'string') {
+ return NextResponse.json({ error: 'itemKey is required' }, { status: 400 })
+ }
+
+ const libraryId = itemKey.split(':')[0]
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const result = await extractItemText(itemKey)
+ return NextResponse.json(result)
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ if (error.code === 'NO_IMAGE' || error.code === 'INVALID_TYPE') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ console.error('[ai-tagging/extract-text] Error:', error)
+ return NextResponse.json({ error: 'Failed to extract text' }, { status: 502 })
+ }
+}
diff --git a/src/app/api/ai-tagging/fields/route.ts b/src/app/api/ai-tagging/fields/route.ts
new file mode 100644
index 0000000..ee647aa
--- /dev/null
+++ b/src/app/api/ai-tagging/fields/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { getAiFields } from '@/lib/ai-tagger'
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = request.nextUrl
+ const itemKey = searchParams.get('itemKey')
+
+ if (!itemKey) {
+ return NextResponse.json({ error: 'Missing itemKey' }, { status: 400 })
+ }
+
+ const libraryId = itemKey.split(':')[0]
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ const fields = getAiFields(itemKey)
+ return NextResponse.json(fields)
+}
diff --git a/src/app/api/ai-tagging/translate/route.ts b/src/app/api/ai-tagging/translate/route.ts
new file mode 100644
index 0000000..740d9fb
--- /dev/null
+++ b/src/app/api/ai-tagging/translate/route.ts
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { translateItemText } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { itemKey?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { itemKey } = body
+ if (!itemKey || typeof itemKey !== 'string') {
+ return NextResponse.json({ error: 'itemKey is required' }, { status: 400 })
+ }
+
+ const libraryId = itemKey.split(':')[0]
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const translatedText = await translateItemText(itemKey)
+ return NextResponse.json({ translatedText })
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ console.error('[ai-tagging/translate] Error:', error)
+ return NextResponse.json({ error: 'Failed to translate text' }, { status: 502 })
+ }
+}
diff --git a/src/app/manage/ai-tagging/page.tsx b/src/app/manage/ai-tagging/page.tsx
index b8bf1b0..4cba8a4 100644
--- a/src/app/manage/ai-tagging/page.tsx
+++ b/src/app/manage/ai-tagging/page.tsx
@@ -6,10 +6,11 @@ interface AiSettings {
endpoint: string
model: string
enabled: boolean
+ preferredLanguage: string
}
export default function AiTaggingPage() {
- const [settings, setSettings] = useState
+ Language used for translating extracted text. Text not in this language will be automatically translated. +
+(null)
const [tagRefreshKey, setTagRefreshKey] = useState(0)
+ // Text extraction state
+ const [extractedText, setExtractedText] = useState
+ Text Extraction +
+ + + + {extractError && ( +{extractError}
+ )} + + {extractedText && ( ++ Extracted Text +
+
+ {extractedText}
+
+ + Translation +
+
+ {translatedText}
+
+ + {aiDescription} +
+ )} +- Model name to use for vision requests. + Default model used for all AI tasks unless overridden below.