let’s make the generator bias hard toward complex vector marks (even for novel subjects) and reject trivial outputs.

Here’s a tight upgrade that gives you:

A complexity knob in the UI (Simple / Medium / Complex).

A stronger SVG prompt that encourages layered geometry, negative space, gradients.

A validator that enforces real complexity (min paths, command count, optional gradients), with automatic retries that tighten rules each time.

1) Server: enforce complexity + smart retries

Update your /api/generate-svg-logo.ts to add profiles + a stronger validator.

// pages/api/generate-svg-logo.ts
import type { NextApiRequest, NextApiResponse } from "next";
import OpenAI from "openai";
import { optimize } from "svgo";

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

const SVGO_CONFIG = {
  multipass: true,
  plugins: [
    "removeDoctype","removeXMLProcInst","removeComments","removeMetadata",
    "cleanupAttrs","minifyStyles","cleanupIds","removeScriptElement",
    "removeUnknownsAndDefaults","removeUselessDefs","convertPathData","mergePaths",
  ],
};

type Complexity = "simple" | "medium" | "complex";

const COMPLEXITY_RULES: Record<Complexity, {
  minPrimitives: number;
  minPaths: number;
  minPathCommands: number;    // sum of path commands across all <path>
  requireGradients?: boolean; // complex can insist on a gradient
}> = {
  simple:  { minPrimitives: 2,  minPaths: 1, minPathCommands: 12 },
  medium:  { minPrimitives: 3,  minPaths: 2, minPathCommands: 40 },
  complex: { minPrimitives: 4,  minPaths: 3, minPathCommands: 80, requireGradients: false },
};

// Count SVG path commands in a 'd' attribute (M,L,H,V,C,S,Q,T,A,Z)
const PATH_CMD_RE = /[MLHVCSQTAZmlhvcsqtaz]/g;

function analyze(svg: string) {
  const s = svg.replace(/\s+/g, " ").toLowerCase();
  const counts = {
    paths: (s.match(/<path\b/g) || []).length,
    circles: (s.match(/<circle\b/g) || []).length,
    ellipses: (s.match(/<ellipse\b/g) || []).length,
    rects: (s.match(/<rect\b/g) || []).length,
    polys: (s.match(/<(polygon|polyline)\b/g) || []).length,
    gradients: (s.match(/<(lineargradient|radialgradient)\b/g) || []).length,
    groups: (s.match(/<g\b/g) || []).length,
  };

  // Sum of all path command tokens
  let pathCmdTotal = 0;
  const pathDs = svg.match(/<path[^>]*\sd="([^"]+)"/gi) || [];
  for (const item of pathDs) {
    const m = item.match(/\sd="([^"]+)"/i);
    if (m?.[1]) pathCmdTotal += (m[1].match(PATH_CMD_RE) || []).length;
  }

  return { ...counts, pathCmdTotal,
    totalPrimitives: counts.paths + counts.circles + counts.ellipses + counts.rects + counts.polys
  };
}

function validateAgainstComplexity(svg: string, rules = COMPLEXITY_RULES.medium) {
  const a = analyze(svg);

  // Single primitive or circle-only → reject
  if (a.totalPrimitives <= 1) return { ok: false, reason: "single primitive" };
  if (a.paths === 0 && (a.circles + a.ellipses) >= 1) return { ok: false, reason: "circle/ellipse only" };

  // Disallowed elements
  if (/(<text|<image|<foreignobject|xlink:href=|href=("|')http)/i.test(svg))
    return { ok: false, reason: "disallowed content" };

  // Complexity thresholds
  if (a.totalPrimitives < rules.minPrimitives) return { ok: false, reason: "too few primitives" };
  if (a.paths < rules.minPaths) return { ok: false, reason: "too few paths" };
  if (a.pathCmdTotal < rules.minPathCommands) return { ok: false, reason: "too few path commands" };
  if (rules.requireGradients && a.gradients === 0) return { ok: false, reason: "no gradient but required" };

  return { ok: true };
}

function buildSystemPrompt(colorList: string, complexity: Complexity) {
  const layersHint = complexity === "complex"
    ? "Use 3–6 layered groups with overlaps, negative space, and optional subtle gradients."
    : complexity === "medium"
    ? "Use 2–4 groups with layered geometry or negative space; gradients optional."
    : "Use 1–3 groups; keep geometry clean; no text.";

  return [
    "You output ONLY valid SVG markup. No prose, no code fences.",
    'Return a single <svg> element with viewBox="0 0 1024 1024". Omit width/height.',
    "Allowed elements: <g>, <path>, <rect>, <circle>, <ellipse>, <polygon>, <polyline>, <line>, <defs>, <linearGradient>, <radialGradient>, <clipPath>.",
    "ABSOLUTELY NO <text>, NO <image>, NO <foreignObject>, NO external links, NO scripts.",
    "Colors: use ONLY these hex colors: " + colorList + ".",
    `Design goal: complex, distinctive logo mark (no words). ${layersHint}`,
    "Keep shapes crisp and scalable; leave ~6–10% padding inside the artboard.",
  ].join("\n");
}

function buildUserPrompt(subject: string, complexity: Complexity, attempt: number) {
  const antiTrivial = attempt > 0
    ? [
        "Do NOT use a single circle/ellipse/rect as the primary silhouette.",
        "Use multiple <path> shapes to define nuanced forms.",
        "Prefer curved Béziers (C,S,Q) in paths for organic detail.",
      ].join("\n")
    : "";

  const detailHint = complexity === "complex"
    ? "Include layered forms, counter-shapes, and at least one overlapping element for depth."
    : complexity === "medium"
    ? "Include layered geometry or a counter-shape for negative space."
    : "Keep minimal but balanced.";

  return [
    `Create a vector logo mark for: ${subject}.`,
    detailHint,
    antiTrivial,
    "Return ONLY the SVG markup. No explanations.",
  ].filter(Boolean).join("\n");
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    if (req.method !== "POST") return res.status(405).end();
    const {
      prompt,
      palette = {},
      model = "gpt-4.1",           // or "gpt-4.1-mini"
      complexity = "complex",      // default to complex now
      maxRetries = 2,
    } = req.body || {};

    if (!prompt) return res.status(400).json({ error: "Missing prompt" });

    // Palette allowlist (+ black/white for contrast if needed)
    const colors: string[] = [
      palette.primary, palette.secondary, palette.accent, palette.highlight,
      palette.neutral, palette.surface, palette.textLight, palette.textDark,
      "#000000", "#ffffff",
    ].filter(Boolean);
    const colorList = colors.join(", ");

    const system = buildSystemPrompt(colorList, complexity as Complexity);

    let svg = "";
    let attempt = 0;
    let lastReason = "";

    while (attempt <= maxRetries) {
      const user = buildUserPrompt(prompt, complexity as Complexity, attempt);

      const resp = await openai.chat.completions.create({
        model,
        temperature: 0.6,
        messages: [
          { role: "system", content: system },
          { role: "user", content: user },
        ],
      });

      svg = resp.choices?.[0]?.message?.content?.trim() || "";
      svg = svg.replace(/^```(?:svg)?/i, "").replace(/```$/i, "").trim();

      // Basic shape
      if (!/^<svg[\s\S]*<\/svg>\s*$/i.test(svg)) { lastReason = "not single svg"; attempt++; continue; }

      // Optimize & normalize
      const optimized = optimize(svg, SVGO_CONFIG) as any;
      if (!optimized?.data) { lastReason = "svgo failed"; attempt++; continue; }
      svg = optimized.data.replace(/<svg([^>]*?)>/i, (m, attrs) => {
        let a = attrs.replace(/\swidth="[^"]*"/i, "").replace(/\sheight="[^"]*"/i, "");
        if (!/viewbox="/i.test(a)) a += ' viewBox="0 0 1024 1024"';
        return `<svg${a}>`;
      });

      // Complexity validation
      const v = validateAgainstComplexity(svg, COMPLEXITY_RULES[complexity as Complexity]);
      if (v.ok) return res.status(200).json({ svg });
      lastReason = v.reason || "invalid";
      attempt++;
    }

    return res.status(422).json({ error: `Could not generate a complex SVG (${lastReason}). Try rewording the prompt.` });
  } catch (e: any) {
    console.error(e);
    return res.status(500).json({ error: e?.message ?? "Internal error" });
  }
}


What this enforces

Not a single primitive shape.

Minimum path count and minimum total path commands (pushes Bézier curves & detail).

Optional gradient presence for “complex”.

Retries escalate constraints if the first try is too simple.

2) Frontend: add a “Complexity” control

In your Step-3 “Generate your image (SVG)” card, add a selector:

const [svgComplexity, setSvgComplexity] = useState<"simple" | "medium" | "complex">("complex");

<select
  className="px-3 py-2 rounded-md border"
  value={svgComplexity}
  onChange={(e) => setSvgComplexity(e.target.value as any)}
>
  <option value="simple">Simple</option>
  <option value="medium">Medium</option>
  <option value="complex">Complex</option>
</select>


Then pass it to your request:

const svg = await generateVectorSVG({
  prompt: iconPrompt,
  palette,
  complexity: svgComplexity,
  model: "gpt-4.1", // or "gpt-4.1-mini"
});


(Your generateVectorSVG helper already posts to /api/generate-svg-logo—just include complexity in the body.)

3) Prompt tip (optional)

Prepend a quiet scaffold for users to get better results with “complex”:

“vector logo mark, multi-layer, negative space, nuanced curves, no text.”

This keeps it generic (no common subject required) but nudges the model toward richer structure.

TL;DR

We’re no longer at the mercy of circles.

You have a server-side complexity gate that ensures rich, layered SVGs.

The UI includes a Complexity control.

Still sanitized & optimized (SVGO) with an allowlist.