customize model based on step
This commit is contained in:
@@ -6,23 +6,23 @@ export async function GET(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { endpoint, model, enabled } = getAiConfig()
|
||||
const { endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled } = getAiConfig()
|
||||
const preferredLanguage = getPreferredLanguage()
|
||||
return NextResponse.json({ endpoint, model, enabled, preferredLanguage })
|
||||
return NextResponse.json({ endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, 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; preferredLanguage?: string }
|
||||
let body: { endpoint?: string; model?: string; modelTagging?: string; modelDescribe?: string; modelExtract?: string; modelTranslate?: string; enabled?: boolean; preferredLanguage?: string }
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { endpoint, model, enabled, preferredLanguage } = body
|
||||
const { endpoint, model, enabled, preferredLanguage, modelTagging, modelDescribe, modelExtract, modelTranslate } = body
|
||||
|
||||
if (typeof endpoint !== 'string') {
|
||||
return NextResponse.json({ error: 'endpoint is required' }, { status: 400 })
|
||||
@@ -34,11 +34,20 @@ export async function PUT(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'enabled must be a boolean' }, { status: 400 })
|
||||
}
|
||||
|
||||
updateAiConfig(endpoint, model, enabled)
|
||||
updateAiConfig(
|
||||
endpoint,
|
||||
model,
|
||||
enabled,
|
||||
typeof modelTagging === 'string' ? modelTagging : undefined,
|
||||
typeof modelDescribe === 'string' ? modelDescribe : undefined,
|
||||
typeof modelExtract === 'string' ? modelExtract : undefined,
|
||||
typeof modelTranslate === 'string' ? modelTranslate : undefined,
|
||||
)
|
||||
|
||||
if (typeof preferredLanguage === 'string' && preferredLanguage.trim()) {
|
||||
setPreferredLanguage(preferredLanguage.trim())
|
||||
}
|
||||
|
||||
return NextResponse.json({ endpoint, model, enabled, preferredLanguage: getPreferredLanguage() })
|
||||
const config = getAiConfig()
|
||||
return NextResponse.json({ ...config, preferredLanguage: getPreferredLanguage() })
|
||||
}
|
||||
|
||||
38
src/app/api/ai-tagging/describe-bulk/route.ts
Normal file
38
src/app/api/ai-tagging/describe-bulk/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
import { describeDirectoryItems } 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 describeDirectoryItems(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/describe-bulk] Error:', error)
|
||||
return NextResponse.json({ error: 'Failed to generate descriptions' }, { status: 502 })
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,16 @@ import { useEffect, useState, useCallback } from 'react'
|
||||
interface AiSettings {
|
||||
endpoint: string
|
||||
model: string
|
||||
modelTagging: string
|
||||
modelDescribe: string
|
||||
modelExtract: string
|
||||
modelTranslate: string
|
||||
enabled: boolean
|
||||
preferredLanguage: string
|
||||
}
|
||||
|
||||
export default function AiTaggingPage() {
|
||||
const [settings, setSettings] = useState<AiSettings>({ endpoint: '', model: '', enabled: false, preferredLanguage: 'English' })
|
||||
const [settings, setSettings] = useState<AiSettings>({ endpoint: '', model: '', modelTagging: '', modelDescribe: '', modelExtract: '', modelTranslate: '', enabled: false, preferredLanguage: 'English' })
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
@@ -134,7 +138,7 @@ export default function AiTaggingPage() {
|
||||
</p>
|
||||
</Field>
|
||||
|
||||
<Field label="Model">
|
||||
<Field label="Default Model">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.model}
|
||||
@@ -150,10 +154,78 @@ export default function AiTaggingPage() {
|
||||
onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')}
|
||||
/>
|
||||
<p className="mt-1 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
Model name to use for vision requests.
|
||||
Default model used for all AI tasks unless overridden below.
|
||||
</p>
|
||||
</Field>
|
||||
|
||||
<Field label="Tagging Model">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.modelTagging}
|
||||
onChange={(e) => setSettings((s) => ({ ...s, modelTagging: e.target.value }))}
|
||||
placeholder="Leave blank to use default"
|
||||
className="w-full rounded-lg px-3 py-2 text-sm font-mono 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)')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Description Model">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.modelDescribe}
|
||||
onChange={(e) => setSettings((s) => ({ ...s, modelDescribe: e.target.value }))}
|
||||
placeholder="Leave blank to use default"
|
||||
className="w-full rounded-lg px-3 py-2 text-sm font-mono 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)')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Text Extraction Model">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.modelExtract}
|
||||
onChange={(e) => setSettings((s) => ({ ...s, modelExtract: e.target.value }))}
|
||||
placeholder="Leave blank to use default"
|
||||
className="w-full rounded-lg px-3 py-2 text-sm font-mono 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)')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Translation Model">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.modelTranslate}
|
||||
onChange={(e) => setSettings((s) => ({ ...s, modelTranslate: e.target.value }))}
|
||||
placeholder="Leave blank to use default"
|
||||
className="w-full rounded-lg px-3 py-2 text-sm font-mono 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)')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Automatic Tagging">
|
||||
<label className="flex items-center gap-3 cursor-pointer select-none">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user