From 502ecba9e9357ed4e60259080c763be0e4803059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Mon, 23 Mar 2026 15:02:35 +0100 Subject: [PATCH] feat: Google Gemini image generation for project covers Schema: - SystemSettings: geminiApiKey, geminiModel, imageProvider fields - imageProvider: "dalle" (default) or "gemini" Gemini Client (packages/api/src/gemini-client.ts): - Direct HTTP call to Gemini REST API with responseModalities: [TEXT, IMAGE] - Returns base64 data URL - Error parsing with user-friendly messages Router (project.ts): - generateCover: routes to DALL-E or Gemini based on imageProvider setting - New isImageGenConfigured query returning { configured, provider } Admin UI (SystemSettingsClient.tsx): - "Image Generation" section with provider radio buttons (DALL-E / Gemini) - Conditional fields: DALL-E config or Gemini API key + model - Separate save button for image settings Security: - geminiApiKey sanitized in audit logs (SENSITIVE_FIELDS) - API key stored server-side only, never sent to client Co-Authored-By: claude-flow --- .../components/admin/SystemSettingsClient.tsx | 183 +++++++++++++++--- .../components/projects/CoverArtSection.tsx | 4 +- packages/api/src/gemini-client.ts | 94 +++++++++ packages/api/src/router/project.ts | 99 +++++++--- packages/api/src/router/settings.ts | 19 ++ packages/db/prisma/schema.prisma | 4 + 6 files changed, 339 insertions(+), 64 deletions(-) create mode 100644 packages/api/src/gemini-client.ts diff --git a/apps/web/src/components/admin/SystemSettingsClient.tsx b/apps/web/src/components/admin/SystemSettingsClient.tsx index 59d0baa..f1e5f83 100644 --- a/apps/web/src/components/admin/SystemSettingsClient.tsx +++ b/apps/web/src/components/admin/SystemSettingsClient.tsx @@ -96,6 +96,13 @@ export function SystemSettingsClient() { const [dalleEndpoint, setDalleEndpoint] = useState(""); const [dalleApiKey, setDalleApiKey] = useState(""); + // Gemini / Image generation settings + type ImageProvider = "dalle" | "gemini"; + const [imageProvider, setImageProvider] = useState("dalle"); + const [geminiApiKey, setGeminiApiKey] = useState(""); + const [geminiModel, setGeminiModel] = useState(""); + const [imageSaved, setImageSaved] = useState(false); + // SMTP settings const [smtpHost, setSmtpHost] = useState(""); const [smtpPort, setSmtpPort] = useState(587); @@ -144,6 +151,9 @@ export function SystemSettingsClient() { // DALL-E setDalleDeployment(settings.azureDalleDeployment ?? ""); setDalleEndpoint(settings.azureDalleEndpoint ?? ""); + // Image provider / Gemini + setImageProvider((settings.imageProvider ?? "dalle") as ImageProvider); + setGeminiModel(settings.geminiModel ?? ""); // SMTP setSmtpHost(settings.smtpHost ?? ""); setSmtpPort(settings.smtpPort ?? 587); @@ -240,6 +250,13 @@ export function SystemSettingsClient() { }, }); + const saveImageMutation = trpc.settings.updateSystemSettings.useMutation({ + onSuccess: () => { + setImageSaved(true); + setTimeout(() => setImageSaved(false), 3000); + }, + }); + function handleSaveSmtp() { saveSmtpMutation.mutate({ smtpHost: smtpHost || undefined, @@ -259,6 +276,19 @@ export function SystemSettingsClient() { saveTimelineMutation.mutate({ timelineUndoMaxSteps: undoMaxSteps }); } + function handleSaveImage() { + saveImageMutation.mutate({ + imageProvider, + // DALL-E fields + azureDalleDeployment: dalleDeployment || undefined, + azureDalleEndpoint: provider === "azure" && dalleEndpoint ? dalleEndpoint : undefined, + ...(dalleApiKey ? { azureDalleApiKey: dalleApiKey } : {}), + // Gemini fields + ...(geminiApiKey ? { geminiApiKey } : {}), + geminiModel: geminiModel || undefined, + }); + } + function handleSaveAnonymization() { saveAnonymizationMutation.mutate({ anonymizationEnabled, @@ -295,9 +325,6 @@ export function SystemSettingsClient() { aiTemperature: temperature, aiSummaryPrompt: summaryPrompt || undefined, ...(apiKey ? { azureOpenAiApiKey: apiKey } : {}), - azureDalleDeployment: dalleDeployment, - azureDalleEndpoint: provider === "azure" && dalleEndpoint ? dalleEndpoint : undefined, - ...(dalleApiKey ? { azureDalleApiKey: dalleApiKey } : {}), }); } @@ -1018,67 +1045,159 @@ export function SystemSettingsClient() { - {/* ── DALL-E Image Generation ────────────────────────────────── */} + {/* ── Image Generation ────────────────────────────────── */}

- DALL-E Image Generation + Image Generation

- Used to generate AI cover art for projects. Leave blank to disable AI cover generation. + Used to generate AI cover art for projects. Configure at least one provider below.

-
-
-
diff --git a/apps/web/src/components/projects/CoverArtSection.tsx b/apps/web/src/components/projects/CoverArtSection.tsx index a5a4142..4917bb9 100644 --- a/apps/web/src/components/projects/CoverArtSection.tsx +++ b/apps/web/src/components/projects/CoverArtSection.tsx @@ -25,7 +25,7 @@ export function CoverArtSection({ projectId, coverImageUrl, coverFocusY = 50, pr const fileInputRef = useRef(null); const utils = trpc.useUtils(); - const { data: dalleStatus } = trpc.project.isDalleConfigured.useQuery(); + const { data: imageGenStatus } = trpc.project.isImageGenConfigured.useQuery(); const generateMutation = trpc.project.generateCover.useMutation(); const uploadMutation = trpc.project.uploadCover.useMutation(); const removeMutation = trpc.project.removeCover.useMutation(); @@ -207,7 +207,7 @@ export function CoverArtSection({ projectId, coverImageUrl, coverFocusY = 50, pr )} {/* Generate with AI */} - {dalleStatus?.configured && ( + {imageGenStatus?.configured && (