add ai descriptions and extracted text
This commit is contained in:
@@ -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() })
|
||||
}
|
||||
|
||||
39
src/app/api/ai-tagging/describe/route.ts
Normal file
39
src/app/api/ai-tagging/describe/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
38
src/app/api/ai-tagging/extract-text-bulk/route.ts
Normal file
38
src/app/api/ai-tagging/extract-text-bulk/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
39
src/app/api/ai-tagging/extract-text/route.ts
Normal file
39
src/app/api/ai-tagging/extract-text/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
19
src/app/api/ai-tagging/fields/route.ts
Normal file
19
src/app/api/ai-tagging/fields/route.ts
Normal file
@@ -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)
|
||||
}
|
||||
36
src/app/api/ai-tagging/translate/route.ts
Normal file
36
src/app/api/ai-tagging/translate/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ interface AiSettings {
|
||||
endpoint: string
|
||||
model: string
|
||||
enabled: boolean
|
||||
preferredLanguage: string
|
||||
}
|
||||
|
||||
export default function AiTaggingPage() {
|
||||
const [settings, setSettings] = useState<AiSettings>({ endpoint: '', model: '', enabled: false })
|
||||
const [settings, setSettings] = useState<AiSettings>({ endpoint: '', model: '', enabled: false, preferredLanguage: 'English' })
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
@@ -178,6 +179,26 @@ export default function AiTaggingPage() {
|
||||
</p>
|
||||
</Field>
|
||||
|
||||
<Field label="Preferred Language">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.preferredLanguage}
|
||||
onChange={(e) => setSettings((s) => ({ ...s, preferredLanguage: e.target.value }))}
|
||||
placeholder="English"
|
||||
className="w-full rounded-lg px-3 py-2 text-sm outline-none focus:ring-2"
|
||||
style={{
|
||||
backgroundColor: 'var(--background)',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--text-primary)',
|
||||
}}
|
||||
onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)')}
|
||||
onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')}
|
||||
/>
|
||||
<p className="mt-1 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
Language used for translating extracted text. Text not in this language will be automatically translated.
|
||||
</p>
|
||||
</Field>
|
||||
|
||||
{saveError && (
|
||||
<p
|
||||
className="text-sm rounded-lg px-3 py-2"
|
||||
|
||||
Reference in New Issue
Block a user