From ade120a9da19044a58961dbe9a8874dcbbab6f8d Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Fri, 10 Apr 2026 15:05:51 +0200 Subject: [PATCH] feat: add settings page with database configuration, AI provider management, and FF API status tracking. --- src/app/(dashboard)/admin/ai-chat/page.tsx | 28 +++---- src/app/(dashboard)/settings/page.tsx | 92 ++++++++++++++++------ src/app/api/google-ai/models/route.ts | 61 ++++++++++++++ 3 files changed, 137 insertions(+), 44 deletions(-) create mode 100644 src/app/api/google-ai/models/route.ts diff --git a/src/app/(dashboard)/admin/ai-chat/page.tsx b/src/app/(dashboard)/admin/ai-chat/page.tsx index 7cb7256..e233808 100644 --- a/src/app/(dashboard)/admin/ai-chat/page.tsx +++ b/src/app/(dashboard)/admin/ai-chat/page.tsx @@ -145,13 +145,6 @@ function MessageBubble({ msg }: { msg: Message }) { ); } -const GOOGLE_MODELS: ModelInfo[] = [ - { id: "gemini-2.5-pro-preview-05-06", name: "Gemini 2.5 Pro Preview", free: false, promptPrice: 0, completionPrice: 0 }, - { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", free: false, promptPrice: 0, completionPrice: 0 }, - { id: "gemini-2.0-flash-thinking-exp", name: "Gemini 2.0 Flash Thinking", free: false, promptPrice: 0, completionPrice: 0 }, - { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro", free: false, promptPrice: 0, completionPrice: 0 }, - { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash", free: false, promptPrice: 0, completionPrice: 0 }, -]; export default function AdminAiChatPage() { const [models, setModels] = useState([]); @@ -175,25 +168,24 @@ export default function AdminAiChatPage() { setLoadingModels(true); fetch("/api/admin/ai-config") .then(r => r.json()) - .then(d => { + .then(async d => { const provider: "openrouter" | "google" = d.provider ?? "openrouter"; setAiProviderState(provider); if (provider === "google") { - setModels(GOOGLE_MODELS); + const gRes = await fetch("/api/google-ai/models").then(r => r.json()).catch(() => ({ models: [] })); + setModels((gRes.models ?? []).map((m: { id: string; name: string }) => ({ + id: m.id, name: m.name, free: false, promptPrice: 0, completionPrice: 0, + }))); setSelectedModel(d.googleAiModel ?? "gemini-2.0-flash"); } else { - fetch("/api/openrouter/models") - .then(r => r.json()) - .then(od => { if (od.models) setModels(od.models); }) - .catch(() => { }); + const od = await fetch("/api/openrouter/models").then(r => r.json()).catch(() => ({ models: [] })); + if (od.models) setModels(od.models); if (d.openRouterModel) setSelectedModel(d.openRouterModel); } }) - .catch(() => { - fetch("/api/openrouter/models") - .then(r => r.json()) - .then(od => { if (od.models) setModels(od.models); }) - .catch(() => { }); + .catch(async () => { + const od = await fetch("/api/openrouter/models").then(r => r.json()).catch(() => ({ models: [] })); + if (od.models) setModels(od.models); }) .finally(() => setLoadingModels(false)); }, []); diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx index 68ad8d5..280c436 100644 --- a/src/app/(dashboard)/settings/page.tsx +++ b/src/app/(dashboard)/settings/page.tsx @@ -125,7 +125,9 @@ export default function SettingsPage() { const [aiProvider, setAiProvider] = useState<"openrouter" | "google">("openrouter"); const [googleAiKey, setGoogleAiKey] = useState(""); const [showGoogleKey, setShowGoogleKey] = useState(false); - const [googleAiModel, setGoogleAiModel] = useState("gemini-2.5-pro-preview-05-06"); + const [googleAiModel, setGoogleAiModel] = useState(""); + const [googleModels, setGoogleModels] = useState<{ id: string; name: string }[]>([]); + const [googleModelsStatus, setGoogleModelsStatus] = useState<"idle" | "loading" | "ok" | "error">("idle"); const [isMounted, setIsMounted] = useState(false); @@ -139,6 +141,20 @@ export default function SettingsPage() { getDatabaseUrl } = useDbSettingsStore(); + const fetchGoogleModels = useCallback(async (key: string) => { + if (!key.trim()) return; + setGoogleModelsStatus("loading"); + try { + const res = await fetch("/api/google-ai/models", { headers: { "x-google-ai-key": key } }); + if (!res.ok) throw new Error(); + const data = await res.json(); + setGoogleModels(data.models ?? []); + setGoogleModelsStatus("ok"); + } catch { + setGoogleModelsStatus("error"); + } + }, []); + const fetchModels = useCallback(async (key: string) => { if (!key.trim()) return; setModelsStatus("loading"); @@ -178,10 +194,13 @@ export default function SettingsPage() { setSelectedModel(config.openRouterModel); } if (config.aiProvider) setAiProvider(config.aiProvider); - if (config.googleAiKey) setGoogleAiKey(config.googleAiKey); + if (config.googleAiKey) { + setGoogleAiKey(config.googleAiKey); + fetchGoogleModels(config.googleAiKey); + } if (config.googleAiModel) setGoogleAiModel(config.googleAiModel); } - }, [fetchModels, setApiKey, setDatabase, setHost, setPassword, setPort, setSelectedModel, setSsl, setUser]); + }, [fetchModels, fetchGoogleModels, setApiKey, setDatabase, setHost, setPassword, setPort, setSelectedModel, setSsl, setUser]); useEffect(() => { setIsMounted(true); @@ -337,34 +356,55 @@ export default function SettingsPage() { {aiProvider === "google" && ( <> -
- setGoogleAiKey(e.target.value)} - className="apple-input font-mono pr-10" - /> +
+
+ setGoogleAiKey(e.target.value)} + className="apple-input font-mono pr-10" + /> + +
- - + 0 ? ` (${googleModels.length} disponibles)` : ""}`} + hint={googleModelsStatus === "error" ? "⚠ Erreur — vérifiez votre clé puis rechargez" : googleModelsStatus === "idle" ? "Entrez votre clé et cliquez « Charger les modèles »" : undefined} + > + {googleModels.length > 0 ? ( + + ) : ( + setGoogleAiModel(e.target.value)} + placeholder="ex: gemini-2.0-flash" + className="apple-input" + /> + )} )} diff --git a/src/app/api/google-ai/models/route.ts b/src/app/api/google-ai/models/route.ts new file mode 100644 index 0000000..661c412 --- /dev/null +++ b/src/app/api/google-ai/models/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from "next/server"; +import fs from "fs/promises"; +import path from "path"; + +async function getSavedGoogleKey(): Promise { + try { + const configFile = path.join(process.cwd(), "data", ".db-config.json"); + const data = await fs.readFile(configFile, "utf-8"); + const config = JSON.parse(data); + return config.googleAiKey ?? null; + } catch { + return null; + } +} + +export async function GET(req: NextRequest) { + const apiKey = req.headers.get("x-google-ai-key") + || process.env.GOOGLE_AI_KEY + || await getSavedGoogleKey(); + + if (!apiKey) { + return NextResponse.json({ error: "No Google AI key provided" }, { status: 401 }); + } + + try { + const res = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}&pageSize=100`, + { cache: "no-store" } + ); + + if (!res.ok) { + const err = await res.text(); + return NextResponse.json({ error: err }, { status: res.status }); + } + + const data = await res.json(); + + // Keep only generative models that support generateContent + const models = (data.models as Array<{ + name: string; + displayName: string; + supportedGenerationMethods?: string[]; + description?: string; + }>) + .filter(m => + m.supportedGenerationMethods?.includes("generateContent") && + !m.name.includes("embedding") && + !m.name.includes("aqa") + ) + .map(m => ({ + id: m.name.replace("models/", ""), + name: m.displayName, + })) + .sort((a, b) => a.name.localeCompare(b.name, "fr")); + + return NextResponse.json({ models }); + } catch (err) { + const msg = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json({ error: msg }, { status: 500 }); + } +}