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:
@@ -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 ──────────────────────────────────────────── */}
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user