[object Object]

← back to Marketing Command Center

Copy panel: Gemini path (preferred over dead Anthropic) → real AI copy via valid Gemini key

b546fbd7c35898020b7d8ce7178056d3d3e5bbf5 · 2026-06-10 08:58:51 -0700 · Steve

Files touched

Diff

commit b546fbd7c35898020b7d8ce7178056d3d3e5bbf5
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jun 10 08:58:51 2026 -0700

    Copy panel: Gemini path (preferred over dead Anthropic) → real AI copy via valid Gemini key
---
 modules/copy/index.js | 39 +++++++++++++++++++++++++++++++++------
 1 file changed, 33 insertions(+), 6 deletions(-)

diff --git a/modules/copy/index.js b/modules/copy/index.js
index 6e00bec..26918a2 100644
--- a/modules/copy/index.js
+++ b/modules/copy/index.js
@@ -225,6 +225,29 @@ async function llmGenerate(ctx, n) {
   return parseVariants(text, n);
 }
 
+// Gemini path (Google Generative Language API). Preferred when GEMINI_API_KEY is
+// set — same brand system prompt, parsed by the shared parseVariants().
+async function geminiGenerate(ctx, n) {
+  const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
+  const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';
+  const resp = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`, {
+    method: 'POST',
+    headers: { 'content-type': 'application/json' },
+    body: JSON.stringify({
+      systemInstruction: { parts: [{ text: brandSystemPrompt() }] },
+      contents: [{ role: 'user', parts: [{ text: userPromptFor(ctx, n) }] }],
+      generationConfig: { maxOutputTokens: 1500, temperature: 0.9 },
+    }),
+  });
+  if (!resp.ok) {
+    const detail = await resp.text().catch(() => '');
+    throw new Error(`gemini ${resp.status}: ${detail.slice(0, 300)}`);
+  }
+  const data = await resp.json();
+  const text = (data.candidates?.[0]?.content?.parts || []).map(p => p.text || '').join('').trim();
+  return parseVariants(text, n);
+}
+
 // Parse the model's response into clean variants. Tolerant of JSON, fenced
 // JSON, or plain newline/numbered lists.
 function parseVariants(raw, n) {
@@ -276,7 +299,7 @@ module.exports = {
         voice: brand.voice,
         tagline: brand.tagline,
         compliance: brand.compliance,
-        llm: !!process.env.ANTHROPIC_API_KEY,
+        llm: !!(process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.ANTHROPIC_API_KEY),
       });
     });
 
@@ -293,15 +316,19 @@ module.exports = {
       };
       const n = clampN(b.n);
 
-      // LLM path when keyed; gracefully fall back to templates on any failure.
+      // LLM path when keyed — Gemini preferred, Anthropic next; gracefully fall
+      // back to templates on any failure so we never return nothing.
+      if (process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
+        try {
+          const variants = await geminiGenerate(ctx, n);
+          if (variants.length) return res.json({ variants, kind, mock: false, source: 'gemini' });
+        } catch (e) { console.warn(`[copy] gemini failed, trying next: ${e.message}`); }
+      }
       if (process.env.ANTHROPIC_API_KEY) {
         try {
           const variants = await llmGenerate(ctx, n);
           if (variants.length) return res.json({ variants, kind, mock: false, source: 'anthropic' });
-          // Empty parse → fall through to templates rather than return nothing.
-        } catch (e) {
-          console.warn(`[copy] LLM failed, using templates: ${e.message}`);
-        }
+        } catch (e) { console.warn(`[copy] anthropic failed, using templates: ${e.message}`); }
       }
       const variants = localGenerate(ctx, n);
       res.json({ variants, kind, mock: true, source: 'templates' });

← a85f5b2 Channels publishing engine: FB/IG/TikTok/YouTube adapters (r  ·  back to Marketing Command Center  ·  Channels OAuth connect machinery: per-platform connect/callb a57c5b8 →