feat: Gemini image generation test button in admin settings

API: new testGeminiConnection adminProcedure
- Generates a simple test image via Gemini API
- Returns { ok, model } on success, { ok: false, error } on failure
- Audit logged: "Gemini test succeeded/failed"

UI: "Test Gemini" button next to "Save Image Settings"
- Only visible when Gemini provider is selected
- Shows green success or red error result below the buttons
- Displays the model name on success

Model: gemini-2.0-flash-preview-image-generation (correct name)

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-23 15:11:28 +01:00
parent 502ecba9e9
commit 3ceba38ac8
2 changed files with 70 additions and 1 deletions
@@ -257,6 +257,12 @@ export function SystemSettingsClient() {
}, },
}); });
const [geminiTestResult, setGeminiTestResult] = useState<{ ok: boolean; model?: string; error?: string } | null>(null);
const testGeminiMut = trpc.settings.testGeminiConnection.useMutation({
onSuccess: (data) => setGeminiTestResult(data as any),
onError: (err) => setGeminiTestResult({ ok: false, error: err.message }),
});
function handleSaveSmtp() { function handleSaveSmtp() {
saveSmtpMutation.mutate({ saveSmtpMutation.mutate({
smtpHost: smtpHost || undefined, smtpHost: smtpHost || undefined,
@@ -1187,7 +1193,7 @@ export function SystemSettingsClient() {
</div> </div>
)} )}
<div className="flex items-center gap-3 pt-1"> <div className="flex flex-wrap items-center gap-3 pt-1">
<button <button
type="button" type="button"
className={PRIMARY_BUTTON_CLASS} className={PRIMARY_BUTTON_CLASS}
@@ -1196,10 +1202,31 @@ export function SystemSettingsClient() {
> >
{saveImageMutation.isPending ? "Saving..." : "Save Image Settings"} {saveImageMutation.isPending ? "Saving..." : "Save Image Settings"}
</button> </button>
{imageProvider === "gemini" && (
<button
type="button"
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-sm font-medium disabled:opacity-50"
disabled={testGeminiMut.isPending}
onClick={() => testGeminiMut.mutate()}
>
{testGeminiMut.isPending ? "Testing..." : "Test Gemini"}
</button>
)}
{imageSaved && ( {imageSaved && (
<span className="text-sm font-medium text-green-600 dark:text-green-400">Saved</span> <span className="text-sm font-medium text-green-600 dark:text-green-400">Saved</span>
)} )}
</div> </div>
{geminiTestResult && (
<div className={`mt-3 rounded-lg px-3 py-2 text-sm ${
geminiTestResult.ok
? "bg-green-50 text-green-700 border border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800"
: "bg-red-50 text-red-700 border border-red-200 dark:bg-red-900/20 dark:text-red-300 dark:border-red-800"
}`}>
{geminiTestResult.ok
? `Gemini image generation works! Model: ${(geminiTestResult as any).model}`
: `Test failed: ${(geminiTestResult as any).error}`}
</div>
)}
</div> </div>
{/* ── SMTP / Email ──────────────────────────────────────────── */} {/* ── SMTP / Email ──────────────────────────────────────────── */}
+42
View File
@@ -319,6 +319,48 @@ export const settingsRouter = createTRPCRouter({
return result; return result;
}), }),
testGeminiConnection: adminProcedure.mutation(async ({ ctx }) => {
const settings = await ctx.db.systemSettings.findUnique({
where: { id: "singleton" },
select: { geminiApiKey: true, geminiModel: true },
});
if (!settings?.geminiApiKey) {
return { ok: false, error: "Gemini API key is not configured." };
}
try {
const { generateGeminiImage } = await import("../gemini-client.js");
const model = settings.geminiModel ?? "gemini-2.0-flash-preview-image-generation";
// Generate a tiny test image with a simple prompt
const dataUrl = await generateGeminiImage(
settings.geminiApiKey,
"A simple blue circle on white background, minimal, 256x256",
model,
);
const hasImage = dataUrl.startsWith("data:image/");
void createAuditEntry({
db: ctx.db,
entityType: "SystemSettings",
entityId: "singleton",
entityName: "Gemini Connection Test",
action: "UPDATE",
userId: ctx.dbUser?.id,
after: { testResult: hasImage ? "success" : "failed" },
source: "ui",
summary: hasImage ? "Gemini image generation test succeeded" : "Gemini test returned no image",
});
return { ok: hasImage, model, preview: hasImage ? dataUrl.slice(0, 100) + "..." : undefined };
} catch (err) {
const { parseGeminiError } = await import("../gemini-client.js");
return { ok: false, error: parseGeminiError(err) };
}
}),
getAiConfigured: protectedProcedure.query(async ({ ctx }) => { getAiConfigured: protectedProcedure.query(async ({ ctx }) => {
const settings = await ctx.db.systemSettings.findUnique({ const settings = await ctx.db.systemSettings.findUnique({
where: { id: "singleton" }, where: { id: "singleton" },