From 5ac4b3bd8a48ab3500adf2c1fd0a9cf46efb69ee Mon Sep 17 00:00:00 2001 From: Garret Patti <42485635+garretpatti@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:50:18 -0400 Subject: [PATCH] customize model based on step --- src/app/api/ai-settings/route.ts | 21 +++- src/app/api/ai-tagging/describe-bulk/route.ts | 38 +++++++ src/app/manage/ai-tagging/page.tsx | 78 ++++++++++++- src/components/mixed/MixedView.tsx | 106 ++++++++++++++++-- src/lib/ai-tagger.ts | 80 +++++++++++-- src/lib/app-settings.ts | 24 +++- 6 files changed, 317 insertions(+), 30 deletions(-) create mode 100644 src/app/api/ai-tagging/describe-bulk/route.ts diff --git a/src/app/api/ai-settings/route.ts b/src/app/api/ai-settings/route.ts index 09e9435..7eba49d 100644 --- a/src/app/api/ai-settings/route.ts +++ b/src/app/api/ai-settings/route.ts @@ -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() }) } diff --git a/src/app/api/ai-tagging/describe-bulk/route.ts b/src/app/api/ai-tagging/describe-bulk/route.ts new file mode 100644 index 0000000..fb9d05e --- /dev/null +++ b/src/app/api/ai-tagging/describe-bulk/route.ts @@ -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 }) + } +} diff --git a/src/app/manage/ai-tagging/page.tsx b/src/app/manage/ai-tagging/page.tsx index 4cba8a4..6839360 100644 --- a/src/app/manage/ai-tagging/page.tsx +++ b/src/app/manage/ai-tagging/page.tsx @@ -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({ endpoint: '', model: '', enabled: false, preferredLanguage: 'English' }) + const [settings, setSettings] = useState({ endpoint: '', model: '', modelTagging: '', modelDescribe: '', modelExtract: '', modelTranslate: '', enabled: false, preferredLanguage: 'English' }) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [saveError, setSaveError] = useState(null) @@ -134,7 +138,7 @@ export default function AiTaggingPage() {

- + ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')} />

- Model name to use for vision requests. + Default model used for all AI tasks unless overridden below.

+ + 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)')} + /> + + + + 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)')} + /> + + + + 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)')} + /> + + + + 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)')} + /> + +