Yasss—quality jump confirmed. 🔥
Ok, next step… let’s ship the three UX upgrades cleanly:

↻ Reroll a single variant

⭐ Favorite/Lock per variant

🎨 Auto Monochrome + Reverse files in the ZIP

I’m giving you full files so you can paste-replace and run.

1) Server route — support stylePack and variantType (single reroll)

server/routes/ai-logos.ts (full file)

import express from "express";
import OpenAI from "openai";

const router = express.Router();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

function stripCodeFences(s: string) {
  return s
    .replace(/^```json\s*/i, "")
    .replace(/^```\s*/i, "")
    .replace(/```$/i, "")
    .trim();
}

function getOutputText(resp: any): string {
  // Responses API common helpers
  if (resp?.output_text) return resp.output_text;
  const t = resp?.output?.[0]?.content?.[0]?.text;
  if (typeof t === "string") return t;
  // Fallback: stringify
  return JSON.stringify(resp);
}

function normalizeToVariants(data: any) {
  if (Array.isArray(data?.variants)) return data.variants;
  if (Array.isArray(data)) return data;
  if (data?.filename && data?.svg) return [data];
  return [];
}

router.post("/generate", async (req, res) => {
  try {
    const {
      businessName,
      personality,
      palette,
      keywords,
      stylePack,    // "balanced" | "monogram" | "badge"
      variantType,  // optional: "geometric" | "monogram" | "badge" (for single reroll)
    } = req.body as {
      businessName: string;
      personality: "Modern" | "Natural" | "Luxury" | "Friendly";
      palette?: { primary: string; secondary: string; accent: string };
      keywords?: string[];
      stylePack?: "balanced" | "monogram" | "badge";
      variantType?: "geometric" | "monogram" | "badge";
    };

    const style = personality ?? "Modern";
    const p = palette ?? { primary: "#4274b9", secondary: "#6fc282", accent: "#FF8800" };
    const words = (keywords && keywords.length) ? keywords.join(", ") : "brand, identity";

    // Style bias block
    let bias = `Style bias: BALANCED.
Return one geometric, one monogram, one badge:
"logo-geometric.svg", "logo-monogram.svg", "logo-badge.svg".`;

    if (stylePack === "monogram") {
      bias = `Style bias: MONOGRAM-FOCUSED.
All three variants must be monogram-centric using the first letter of "${businessName}".
Output filenames exactly:
"logo-monogram-a.svg", "logo-monogram-b.svg", "logo-monogram-c.svg".`;
    } else if (stylePack === "badge") {
      bias = `Style bias: BADGE-FOCUSED.
All three variants must be compact emblems (circle/shield/hex). Consider outer ring, inner mark, small separators/dots.
Output filenames exactly:
"logo-badge-a.svg", "logo-badge-b.svg", "logo-badge-c.svg".`;
    }

    // Single-variant reroll mode
    let single = "";
    if (variantType) {
      const map = {
        geometric: "logo-geometric.svg",
        monogram: "logo-monogram.svg",
        badge: "logo-badge.svg",
      } as const;
      const fname = map[variantType];
      single = `
REROLL MODE: Return only ONE variant focused on "${variantType}".
Output shape:
{ "variants": [ { "filename": "${fname}", "svg": "<svg...>" } ] }`;
    }

    const prompt = `
Return ONLY JSON (no code fences). Shape exactly:
{
  "variants": [
    { "filename": "logo-geometric.svg", "svg": "<svg...>" },
    { "filename": "logo-monogram.svg",  "svg": "<svg...>" },
    { "filename": "logo-badge.svg",     "svg": "<svg...>" }
  ]
}

${single || ""}

Design brief:
- Brand name: "${businessName}"
- Personality: ${style}
- Keywords: ${words}
- Preferred colors: primary ${p.primary}, secondary ${p.secondary}, accent ${p.accent}
- Vibe: simple, modern, balanced; strong negative space.

${stylePack ? bias : `Style bias: BALANCED. One geometric, one monogram, one badge.`}

Constraints (MUST):
- Self-contained SVG. Include width, height, and viewBox on the root.
- No <script>, event handlers, <image>, or <foreignObject>.
- Use geometric primitives (path, rect, circle, polygon, line) and text only if needed.
- Use an 8px grid logic; align key points to multiples of 8.
- Even stroke weights; prefer round linecap/linejoin where used.
- WCAG AA contrast for any text against background.
- Legible at 32×32; crisp at 1024×1024.
- Legal: avoid resemblance to known trademarks.
`;

    const response = await openai.responses.create({
      model: process.env.OPENAI_MODEL || "gpt-4.1-mini",
      input: prompt,
      temperature: 0.2,
      max_output_tokens: 1500,
      seed: 7,
    });

    let text = stripCodeFences(getOutputText(response));
    let data: any;
    try {
      data = JSON.parse(text);
    } catch {
      // Try to salvage common formatting mistakes
      text = text.trim();
      data = JSON.parse(text);
    }
    const variants = normalizeToVariants(data);
    res.json({ variants });
  } catch (err: any) {
    console.error("AI Logo error:", err?.response?.data || err?.message || err);
    res.status(500).json({ error: "Logo generation failed." });
  }
});

export default router;


If your server entry is different, keep app.use("/api/logos", aiLogos) as before.
You can set OPENAI_MODEL=gpt-4.1-mini (or your GPT-5) in Replit Secrets.