customize model based on step

This commit is contained in:
Garret Patti
2026-04-12 19:50:18 -04:00
parent 470f34c985
commit 5ac4b3bd8a
6 changed files with 317 additions and 30 deletions

View File

@@ -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() })
}

View 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 })
}
}

View File

@@ -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