diff --git a/src/app/api/ai-settings/library/[id]/route.ts b/src/app/api/ai-settings/library/[id]/route.ts new file mode 100644 index 0000000..b6afdc4 --- /dev/null +++ b/src/app/api/ai-settings/library/[id]/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAdmin } from '@/lib/auth' +import { getLibraryAiOverrides, setLibraryAiOverrides } from '@/lib/app-settings' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const auth = await requireAdmin(request) + if (auth instanceof NextResponse) return auth + + const { id } = await params + return NextResponse.json(getLibraryAiOverrides(id)) +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const auth = await requireAdmin(request) + if (auth instanceof NextResponse) return auth + + const { id } = await params + + let body: Record + try { + body = await request.json() + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) + } + + setLibraryAiOverrides(id, { + modelTagging: typeof body.modelTagging === 'string' ? body.modelTagging : undefined, + modelDescribe: typeof body.modelDescribe === 'string' ? body.modelDescribe : undefined, + modelExtract: typeof body.modelExtract === 'string' ? body.modelExtract : undefined, + modelTranslate: typeof body.modelTranslate === 'string' ? body.modelTranslate : undefined, + promptDescribe: typeof body.promptDescribe === 'string' ? body.promptDescribe : undefined, + promptTagger: typeof body.promptTagger === 'string' ? body.promptTagger : undefined, + promptExtract: typeof body.promptExtract === 'string' ? body.promptExtract : undefined, + promptTranslate: typeof body.promptTranslate === 'string' ? body.promptTranslate : undefined, + }) + + return NextResponse.json(getLibraryAiOverrides(id)) +} diff --git a/src/app/api/ai-settings/route.ts b/src/app/api/ai-settings/route.ts index 7eba49d..414ddbb 100644 --- a/src/app/api/ai-settings/route.ts +++ b/src/app/api/ai-settings/route.ts @@ -6,23 +6,40 @@ export async function GET(request: NextRequest) { const auth = await requireAdmin(request) if (auth instanceof NextResponse) return auth - const { endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled } = getAiConfig() + const config = getAiConfig() const preferredLanguage = getPreferredLanguage() - return NextResponse.json({ endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled, preferredLanguage }) + return NextResponse.json({ ...config, preferredLanguage }) } export async function PUT(request: NextRequest) { const auth = await requireAdmin(request) if (auth instanceof NextResponse) return auth - let body: { endpoint?: string; model?: string; modelTagging?: string; modelDescribe?: string; modelExtract?: string; modelTranslate?: string; enabled?: boolean; preferredLanguage?: string } + let body: { + endpoint?: string + model?: string + modelTagging?: string + modelDescribe?: string + modelExtract?: string + modelTranslate?: string + enabled?: boolean + preferredLanguage?: string + promptDescribe?: string + promptTagger?: string + promptExtract?: string + promptTranslate?: string + } try { body = await request.json() } catch { return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) } - const { endpoint, model, enabled, preferredLanguage, modelTagging, modelDescribe, modelExtract, modelTranslate } = body + const { + endpoint, model, enabled, preferredLanguage, + modelTagging, modelDescribe, modelExtract, modelTranslate, + promptDescribe, promptTagger, promptExtract, promptTranslate, + } = body if (typeof endpoint !== 'string') { return NextResponse.json({ error: 'endpoint is required' }, { status: 400 }) @@ -42,6 +59,10 @@ export async function PUT(request: NextRequest) { typeof modelDescribe === 'string' ? modelDescribe : undefined, typeof modelExtract === 'string' ? modelExtract : undefined, typeof modelTranslate === 'string' ? modelTranslate : undefined, + typeof promptDescribe === 'string' ? promptDescribe : undefined, + typeof promptTagger === 'string' ? promptTagger : undefined, + typeof promptExtract === 'string' ? promptExtract : undefined, + typeof promptTranslate === 'string' ? promptTranslate : undefined, ) if (typeof preferredLanguage === 'string' && preferredLanguage.trim()) { diff --git a/src/app/manage/ai-tagging/page.tsx b/src/app/manage/ai-tagging/page.tsx index 6839360..5807cd7 100644 --- a/src/app/manage/ai-tagging/page.tsx +++ b/src/app/manage/ai-tagging/page.tsx @@ -11,10 +11,34 @@ interface AiSettings { modelTranslate: string enabled: boolean preferredLanguage: string + promptDescribe: string + promptTagger: string + promptExtract: string + promptTranslate: string +} + +interface Library { + id: string + name: string +} + +interface LibraryOverride { + modelTagging: string + modelDescribe: string + modelExtract: string + modelTranslate: string + promptDescribe: string + promptTagger: string + promptExtract: string + promptTranslate: string } export default function AiTaggingPage() { - const [settings, setSettings] = useState({ endpoint: '', model: '', modelTagging: '', modelDescribe: '', modelExtract: '', modelTranslate: '', enabled: false, preferredLanguage: 'English' }) + const [settings, setSettings] = useState({ + endpoint: '', model: '', modelTagging: '', modelDescribe: '', modelExtract: '', modelTranslate: '', + enabled: false, preferredLanguage: 'English', + promptDescribe: '', promptTagger: '', promptExtract: '', promptTranslate: '', + }) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [saveError, setSaveError] = useState(null) @@ -24,12 +48,26 @@ export default function AiTaggingPage() { const [retagging, setRetagging] = useState(false) const [retagResult, setRetagResult] = useState(null) + const [libraries, setLibraries] = useState([]) + const [libraryOverrides, setLibraryOverrides] = useState>({}) + const [expandedLibrary, setExpandedLibrary] = useState(null) + const [librarySaving, setLibrarySaving] = useState>({}) + const [librarySaveResult, setLibrarySaveResult] = useState>({}) + const fetchSettings = useCallback(async () => { try { - const res = await fetch('/api/ai-settings') - if (!res.ok) return - const data: AiSettings = await res.json() - setSettings(data) + const [settingsRes, librariesRes] = await Promise.all([ + fetch('/api/ai-settings'), + fetch('/api/libraries'), + ]) + if (settingsRes.ok) { + const data: AiSettings = await settingsRes.json() + setSettings(data) + } + if (librariesRes.ok) { + const data: Library[] = await librariesRes.json() + setLibraries(data) + } } catch { // ignore } finally { @@ -41,6 +79,29 @@ export default function AiTaggingPage() { fetchSettings() }, [fetchSettings]) + const fetchLibraryOverrides = useCallback(async (libraryId: string) => { + try { + const res = await fetch(`/api/ai-settings/library/${libraryId}`) + if (res.ok) { + const data: LibraryOverride = await res.json() + setLibraryOverrides((prev) => ({ ...prev, [libraryId]: data })) + } + } catch { + // ignore + } + }, []) + + const handleToggleLibrary = (libraryId: string) => { + if (expandedLibrary === libraryId) { + setExpandedLibrary(null) + } else { + setExpandedLibrary(libraryId) + if (!libraryOverrides[libraryId]) { + fetchLibraryOverrides(libraryId) + } + } + } + const handleSave = async (e: React.FormEvent) => { e.preventDefault() setSaveError(null) @@ -104,6 +165,39 @@ export default function AiTaggingPage() { } } + const handleSaveLibraryOverride = async (libraryId: string) => { + const overrides = libraryOverrides[libraryId] + if (!overrides) return + setLibrarySaving((prev) => ({ ...prev, [libraryId]: true })) + setLibrarySaveResult((prev) => ({ ...prev, [libraryId]: { ok: false, message: '' } })) + try { + const res = await fetch(`/api/ai-settings/library/${libraryId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(overrides), + }) + const data = await res.json() + if (res.ok) { + setLibraryOverrides((prev) => ({ ...prev, [libraryId]: data })) + setLibrarySaveResult((prev) => ({ ...prev, [libraryId]: { ok: true, message: 'Saved.' } })) + setTimeout(() => setLibrarySaveResult((prev) => ({ ...prev, [libraryId]: { ok: true, message: '' } })), 3000) + } else { + setLibrarySaveResult((prev) => ({ ...prev, [libraryId]: { ok: false, message: data.error ?? 'Failed to save.' } })) + } + } catch { + setLibrarySaveResult((prev) => ({ ...prev, [libraryId]: { ok: false, message: 'Network error.' } })) + } finally { + setLibrarySaving((prev) => ({ ...prev, [libraryId]: false })) + } + } + + const updateLibraryOverride = (libraryId: string, field: keyof LibraryOverride, value: string) => { + setLibraryOverrides((prev) => ({ + ...prev, + [libraryId]: { ...(prev[libraryId] ?? emptyOverride()), [field]: value }, + })) + } + return (

@@ -334,6 +428,255 @@ export default function AiTaggingPage() { )} +
+ {loading ? ( + + ) : ( +
+ +