feat: add settings page with database configuration, AI provider management, and FF API status tracking.

This commit is contained in:
Michael committed 2026-04-10 15:05:51 +02:00
1 parent d0ca0c3602
commit ade120a9da
3 files changed
+137 -44

No files matched your search

+10 -18
View File
@@ -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<ModelInfo[]>([]);
@@ -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));
}, []);
+66 -26
View File
@@ -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" && (
<>
<Field label="Clé API Google AI Studio" hint="Disponible sur aistudio.google.com/apikey — commence par AIza...">
<div className="relative">
<input
type={showGoogleKey ? "text" : "password"}
placeholder="AIzaSy..."
value={googleAiKey}
onChange={(e) => setGoogleAiKey(e.target.value)}
className="apple-input font-mono pr-10"
/>
<div className="flex gap-2">
<div className="relative flex-1">
<input
type={showGoogleKey ? "text" : "password"}
placeholder="AIzaSy..."
value={googleAiKey}
onChange={(e) => setGoogleAiKey(e.target.value)}
className="apple-input font-mono pr-10"
/>
<button
onClick={() => setShowGoogleKey((v) => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 opacity-50 hover:opacity-100 transition-opacity"
>
{showGoogleKey ? <EyeOff className="w-4 h-4" style={{ color: "var(--text-primary)" }} /> : <Eye className="w-4 h-4" style={{ color: "var(--text-primary)" }} />}
</button>
</div>
<button
onClick={() => setShowGoogleKey((v) => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 opacity-50 hover:opacity-100 transition-opacity"
onClick={() => fetchGoogleModels(googleAiKey)}
disabled={!googleAiKey || googleModelsStatus === "loading"}
className="apple-btn-secondary h-9 px-4 whitespace-nowrap"
>
{showGoogleKey ? <EyeOff className="w-4 h-4" style={{ color: "var(--text-primary)" }} /> : <Eye className="w-4 h-4" style={{ color: "var(--text-primary)" }} />}
{googleModelsStatus === "loading" ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <RefreshCw className="w-3.5 h-3.5" />}
Charger les modèles
</button>
</div>
</Field>
<Field label="Modèle Google" hint="ex: gemini-2.5-pro-preview-05-06, gemini-2.0-flash, gemini-1.5-pro">
<select
value={googleAiModel}
onChange={(e) => setGoogleAiModel(e.target.value)}
className="apple-input"
>
<option value="gemini-2.5-pro-preview-05-06">Gemini 2.5 Pro Preview</option>
<option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
<option value="gemini-2.0-flash-thinking-exp">Gemini 2.0 Flash Thinking</option>
<option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
<option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
</select>
<Field
label={`Modèle Google${googleModels.length > 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 ? (
<select
value={googleAiModel}
onChange={(e) => setGoogleAiModel(e.target.value)}
className="apple-input"
>
{googleModels.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option>
))}
</select>
) : (
<input
type="text"
value={googleAiModel}
onChange={(e) => setGoogleAiModel(e.target.value)}
placeholder="ex: gemini-2.0-flash"
className="apple-input"
/>
)}
</Field>
</>
)}
+61
View File
@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
async function getSavedGoogleKey(): Promise<string | null> {
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 });
}
}