@easybits.cloud/html-tailwind-generator 0.2.12 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -155,6 +155,16 @@ function extractJsonObjects(text) {
155
155
  }
156
156
  return [objects, remaining];
157
157
  }
158
+ var LOADING_PLACEHOLDER = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="800" height="500" viewBox="0 0 800 500"><rect fill="%231f2937" width="800" height="500" rx="12"/><style>@keyframes p{0%{opacity:.3}50%{opacity:1}to{opacity:.3}}.d{animation:p 1.5s ease-in-out infinite}</style><circle class="d" cx="370" cy="240" r="8" fill="%239ca3af"/><circle class="d" cx="400" cy="240" r="8" fill="%239ca3af" style="animation-delay:.2s"/><circle class="d" cx="430" cy="240" r="8" fill="%239ca3af" style="animation-delay:.4s"/><text x="400" y="280" text-anchor="middle" fill="%239ca3af" font-family="system-ui" font-size="14">Generando imagen...</text></svg>`)}`;
159
+ function addLoadingPlaceholders(html) {
160
+ return html.replace(
161
+ /(<img\s[^>]*)data-image-query="([^"]+)"([^>]*?)(?:\s*\/?>)/gi,
162
+ (_match, before, query, after) => {
163
+ if (before.includes("src=") || after.includes("src=")) return _match;
164
+ return `${before}src="${LOADING_PLACEHOLDER}" data-image-query="${query}" alt="${query}"${after}>`;
165
+ }
166
+ );
167
+ }
158
168
  async function generateLanding(options) {
159
169
  const {
160
170
  anthropicApiKey,
@@ -210,6 +220,7 @@ Additional instructions: ${extraInstructions}` : "";
210
220
  html: obj.html,
211
221
  label: obj.label
212
222
  };
223
+ section.html = addLoadingPlaceholders(section.html);
213
224
  allSections.push(section);
214
225
  onSection?.(section);
215
226
  const slots = findImageSlots(section.html);
@@ -268,6 +279,7 @@ Additional instructions: ${extraInstructions}` : "";
268
279
  html: obj.html,
269
280
  label: obj.label
270
281
  };
282
+ section.html = addLoadingPlaceholders(section.html);
271
283
  allSections.push(section);
272
284
  onSection?.(section);
273
285
  const slots = findImageSlots(section.html);
@@ -338,4 +350,4 @@ export {
338
350
  extractJsonObjects,
339
351
  generateLanding
340
352
  };
341
- //# sourceMappingURL=chunk-XGFWZQ2Y.js.map
353
+ //# sourceMappingURL=chunk-3YGNTEU5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generate.ts"],"sourcesContent":["import { streamText } from \"ai\";\nimport { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { nanoid } from \"nanoid\";\nimport { findImageSlots, type EnrichImagesOptions } from \"./images/enrichImages\";\nimport { searchImage } from \"./images/pexels\";\nimport { generateImage } from \"./images/dalleImages\";\nimport type { Section3 } from \"./types\";\n\nasync function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {\n // Prefer Anthropic for text generation when both keys are available\n const anthropicKey = opts.anthropicApiKey || process.env.ANTHROPIC_API_KEY;\n if (anthropicKey) {\n const anthropic = createAnthropic({ apiKey: anthropicKey });\n return anthropic(opts.modelId || opts.defaultAnthropic);\n }\n // Fallback to OpenAI for text only if no Anthropic key\n const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const { createOpenAI } = await import(\"@ai-sdk/openai\");\n const openai = createOpenAI({ apiKey: openaiKey });\n return openai(opts.modelId || opts.defaultOpenai);\n }\n // Last resort: createAnthropic() without key (uses env var)\n return createAnthropic()(opts.modelId || opts.defaultAnthropic);\n}\n\nexport const SYSTEM_PROMPT = `You are a world-class web designer who creates AWARD-WINNING landing pages. Your designs win Awwwards, FWA, and CSS Design Awards. You think in terms of visual hierarchy, whitespace, and emotional impact.\n\nRULES:\n- Each section is a complete <section> tag with Tailwind CSS classes\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import, no @tailwind directives)\n- NO JavaScript, only HTML+Tailwind\n- Each section must be independent and self-contained\n- Responsive: mobile-first with sm/md/lg/xl breakpoints\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real-looking content (not Lorem ipsum) — make it specific to the prompt\n\nRESPONSIVE — MANDATORY:\n- EVERY grid: grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 (NEVER grid-cols-3 alone)\n- EVERY flex row: flex flex-col md:flex-row (NEVER flex flex-row alone)\n- Text sizes: text-3xl md:text-5xl lg:text-7xl (NEVER text-7xl alone)\n- Images: w-full h-auto object-cover max-w-full\n- Padding: px-4 md:px-8 lg:px-16 (NEVER px-16 alone)\n- Hide decorative on mobile if breaks layout: hidden md:block\n\nIMAGES — CRITICAL:\n- EVERY image MUST use: <img data-image-query=\"english search query\" alt=\"description\" class=\"w-full h-auto object-cover rounded-xl\"/>\n- NEVER use <img> without data-image-query\n- NEVER include a src attribute — the system auto-replaces data-image-query with a real image URL\n- Queries must be generic stock-photo friendly (e.g. \"modern office\" not \"Juan's cybercafe\")\n- For avatar-like elements, use colored divs with initials instead of img tags (e.g. <div class=\"w-10 h-10 rounded-full bg-primary flex items-center justify-center text-on-primary font-bold\">JD</div>)\n\nCOLOR SYSTEM — CRITICAL (READ CAREFULLY):\n- Use semantic color classes: bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary, bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted, bg-secondary, text-secondary, bg-accent, text-accent\n- NEVER use hardcoded Tailwind color classes: NO bg-gray-*, bg-black, bg-white, bg-indigo-*, bg-blue-*, bg-purple-*, text-gray-*, text-black, text-white, etc.\n- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.\n- ALL backgrounds MUST use: bg-primary, bg-primary-dark, bg-surface, bg-surface-alt\n- ALL text MUST use: text-on-surface, text-on-surface-muted, text-on-primary, text-primary, text-accent\n- CONTRAST RULE: on bg-primary or bg-primary-dark → use text-on-primary. On bg-surface or bg-surface-alt → use text-on-surface or text-on-surface-muted. NEVER put text-on-surface on bg-primary or vice versa. text-accent is decorative — use sparingly on bg-surface/bg-surface-alt only.\n- ANTI-PATTERN: NEVER put bg-primary on BOTH the section AND elements inside it. If section is bg-primary, inner cards/elements should be bg-surface. If section is bg-surface, cards can use bg-surface-alt or bg-primary.\n- For gradients: from-primary to-primary-dark, from-surface to-surface-alt\n- For hover: hover:bg-primary-dark, hover:bg-primary-light\n\nDESIGN PHILOSOPHY — what separates good from GREAT:\n- WHITESPACE is your best friend. Generous padding (py-24, py-32, px-8). Let elements breathe.\n- CONTRAST: mix dark sections with light ones. Alternate bg-primary and bg-surface sections.\n- TYPOGRAPHY: use extreme size differences for hierarchy (text-7xl headline next to text-sm label)\n- DEPTH: overlapping elements, negative margins (-mt-12), z-index layering, shadows\n- ASYMMETRY: avoid centering everything. Use grid-cols-5 with col-span-3 + col-span-2. Offset elements.\n- TEXTURE: use subtle patterns, gradients, border treatments, rounded-3xl mixed with sharp edges\n- Each section should have a COMPLETELY DIFFERENT layout from the others\n\nSECTION LAYOUT — CRITICAL:\n- Each <section> must be full-width (bg goes edge-to-edge). NO max-w on the section itself.\n- Constrain content inside with a wrapper div: <section class=\"bg-primary py-24\"><div class=\"max-w-7xl mx-auto px-4 md:px-8\">...content...</div></section>\n- EVERY section follows this pattern. The <section> handles bg color + vertical padding. The inner <div> handles horizontal padding + max-width.\n\nTESTIMONIALS SECTION:\n- Cards MUST use bg-surface or bg-surface-alt with text-on-surface\n- If section bg is bg-primary or bg-primary-dark, cards MUST be bg-surface (light cards on dark bg)\n- Quote text: text-on-surface, italic\n- Avatar: colored div with initials (bg-accent text-on-primary or bg-primary-light text-on-primary)\n- Name: text-on-surface font-semibold. Role/company: text-on-surface-muted\n- NEVER use same dark bg for both section AND cards\n\nHERO SECTION — your masterpiece:\n- Use a 2-column grid (lg:grid-cols-2) that fills the full height, NOT content floating in empty space\n- Left column: headline + description + CTAs, vertically centered with flex flex-col justify-center\n- Right column: large hero image (data-image-query) filling the column, or a bento-grid of image + stat cards\n- Bold oversized headline (text-4xl md:text-6xl lg:text-7xl font-black leading-tight)\n- Tag/label above headline (uppercase, tracking-wider, text-xs text-accent)\n- Short description paragraph (text-lg text-on-surface-muted, max-w-lg)\n- 2 CTAs: primary (large, px-8 py-4, with → arrow) + secondary (ghost/outlined)\n- Optional: social proof bar below CTAs (avatar stack + \"2,847+ users\" text)\n- Min height: min-h-[90vh] with items-center on the grid so content is vertically centered\n- CRITICAL: the grid must stretch to fill the section height. Use min-h-[90vh] on the grid container itself, not just the section\n- NEVER leave large empty areas — if using min-h-[90vh], content must be centered/distributed within it\n\nTAILWIND v3 NOTES:\n- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)\n- Borders: border + border-gray-200 for visible borders`;\n\nexport const PROMPT_SUFFIX = `\n\nOUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Short Label\", \"html\": \"<section>...</section>\"}\n\nGenerate 7-9 sections. Always start with Hero and end with Footer.\nIMPORTANT: Make each section VISUALLY UNIQUE — different layouts, different background colors, different grid structures.\nThink like a premium design agency creating a $50K landing page.\nNO generic Bootstrap layouts. Use creative grids, bento layouts, overlapping elements, asymmetric columns.`;\n\n/**\n * Extract complete JSON objects from accumulated text using brace-depth tracking.\n */\nexport function extractJsonObjects(text: string): [any[], string] {\n const objects: any[] = [];\n let remaining = text;\n\n while (remaining.length > 0) {\n remaining = remaining.trimStart();\n if (!remaining.startsWith(\"{\")) {\n const nextBrace = remaining.indexOf(\"{\");\n if (nextBrace === -1) break;\n remaining = remaining.slice(nextBrace);\n continue;\n }\n\n let depth = 0;\n let inString = false;\n let escape = false;\n let end = -1;\n\n for (let i = 0; i < remaining.length; i++) {\n const ch = remaining[i];\n if (escape) { escape = false; continue; }\n if (ch === \"\\\\\") { escape = true; continue; }\n if (ch === '\"') { inString = !inString; continue; }\n if (inString) continue;\n if (ch === \"{\") depth++;\n if (ch === \"}\") { depth--; if (depth === 0) { end = i; break; } }\n }\n\n if (end === -1) break;\n\n const candidate = remaining.slice(0, end + 1);\n remaining = remaining.slice(end + 1);\n\n try {\n objects.push(JSON.parse(candidate));\n } catch {\n // malformed, skip\n }\n }\n\n return [objects, remaining];\n}\n\n/** Inline SVG data URI: animated \"generating\" placeholder for images */\nconst LOADING_PLACEHOLDER = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"800\" height=\"500\" viewBox=\"0 0 800 500\"><rect fill=\"%231f2937\" width=\"800\" height=\"500\" rx=\"12\"/><style>@keyframes p{0%{opacity:.3}50%{opacity:1}to{opacity:.3}}.d{animation:p 1.5s ease-in-out infinite}</style><circle class=\"d\" cx=\"370\" cy=\"240\" r=\"8\" fill=\"%239ca3af\"/><circle class=\"d\" cx=\"400\" cy=\"240\" r=\"8\" fill=\"%239ca3af\" style=\"animation-delay:.2s\"/><circle class=\"d\" cx=\"430\" cy=\"240\" r=\"8\" fill=\"%239ca3af\" style=\"animation-delay:.4s\"/><text x=\"400\" y=\"280\" text-anchor=\"middle\" fill=\"%239ca3af\" font-family=\"system-ui\" font-size=\"14\">Generando imagen...</text></svg>`)}`;\n\n/** Replace data-image-query attrs with animated loading placeholders */\nfunction addLoadingPlaceholders(html: string): string {\n return html.replace(\n /(<img\\s[^>]*)data-image-query=\"([^\"]+)\"([^>]*?)(?:\\s*\\/?>)/gi,\n (_match, before, query, after) => {\n // Don't add src if already has one\n if (before.includes('src=') || after.includes('src=')) return _match;\n return `${before}src=\"${LOADING_PLACEHOLDER}\" data-image-query=\"${query}\" alt=\"${query}\"${after}>`;\n }\n );\n}\n\nexport interface GenerateOptions {\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */\n anthropicApiKey?: string;\n /** OpenAI API key. If provided, uses GPT-4o instead of Claude */\n openaiApiKey?: string;\n /** Landing page description prompt */\n prompt: string;\n /** Reference image (base64 data URI) for vision-based generation */\n referenceImage?: string;\n /** Extra instructions appended to the prompt */\n extraInstructions?: string;\n /** Custom system prompt (overrides default SYSTEM_PROMPT) */\n systemPrompt?: string;\n /** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */\n model?: string;\n /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */\n pexelsApiKey?: string;\n /** Called with temp DALL-E URL + query, returns permanent URL. Use to persist to S3/etc. */\n persistImage?: (tempUrl: string, query: string) => Promise<string>;\n /** Called when a new section is parsed from the stream */\n onSection?: (section: Section3) => void;\n /** Called when a section's images are enriched */\n onImageUpdate?: (sectionId: string, html: string) => void;\n /** Called when generation is complete */\n onDone?: (sections: Section3[]) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n}\n\n/**\n * Generate a landing page with streaming AI + image enrichment.\n * Returns all generated sections when complete.\n */\nexport async function generateLanding(options: GenerateOptions): Promise<Section3[]> {\n const {\n anthropicApiKey,\n openaiApiKey: _openaiApiKey,\n prompt,\n referenceImage,\n extraInstructions,\n systemPrompt = SYSTEM_PROMPT,\n model: modelId,\n pexelsApiKey,\n persistImage,\n onSection,\n onImageUpdate,\n onDone,\n onError,\n } = options;\n\n const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;\n const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: \"gpt-4o\", defaultAnthropic: \"claude-sonnet-4-6\" });\n\n // Build prompt content (supports multimodal with reference image)\n const extra = extraInstructions ? `\\nAdditional instructions: ${extraInstructions}` : \"\";\n const content: any[] = [];\n if (referenceImage) {\n content.push({ type: \"image\", image: referenceImage });\n content.push({\n type: \"text\",\n text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n } else {\n content.push({\n type: \"text\",\n text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n }\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: [{ role: \"user\", content }],\n });\n\n const allSections: Section3[] = [];\n const imagePromises: Promise<void>[] = [];\n let sectionOrder = 0;\n let buffer = \"\";\n\n try {\n for await (const chunk of result.textStream) {\n buffer += chunk;\n\n const [objects, remaining] = extractJsonObjects(buffer);\n buffer = remaining;\n\n for (const obj of objects) {\n if (!obj.html || !obj.label) continue;\n\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n\n // Add loading placeholders so images don't show as broken while DALL-E generates\n section.html = addLoadingPlaceholders(section.html);\n allSections.push(section);\n onSection?.(section);\n\n // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)\n const slots = findImageSlots(section.html);\n if (slots.length > 0) {\n const sectionRef = section;\n const slotsSnapshot = slots.map((s) => ({ ...s }));\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n slotsSnapshot.map(async (slot) => {\n let url: string | null = null;\n // 1. DALL-E if openaiApiKey provided\n if (openaiApiKey) {\n try {\n const tempUrl = await generateImage(slot.query, openaiApiKey);\n url = persistImage ? await persistImage(tempUrl, slot.query) : tempUrl;\n } catch (e) {\n console.warn(`[dalle] failed for \"${slot.query}\":`, e);\n }\n }\n // 2. Pexels fallback\n if (!url) {\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n }\n // 3. Placeholder fallback\n url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;\n return { slot, url };\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n const { slot, url } = r.value;\n const replacement = slot.replaceStr.replace(\"{url}\", url);\n html = html.replaceAll(slot.searchStr, replacement);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\n }\n }\n\n // Parse remaining buffer\n if (buffer.trim()) {\n let cleaned = buffer.trim();\n if (cleaned.startsWith(\"```\")) {\n cleaned = cleaned\n .replace(/^```(?:json)?\\s*/, \"\")\n .replace(/\\s*```$/, \"\");\n }\n const [lastObjects] = extractJsonObjects(cleaned);\n for (const obj of lastObjects) {\n if (!obj.html || !obj.label) continue;\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n // Add loading placeholders so images don't show as broken while DALL-E generates\n section.html = addLoadingPlaceholders(section.html);\n allSections.push(section);\n onSection?.(section);\n\n // Enrich images for remaining-buffer sections too\n const slots = findImageSlots(section.html);\n if (slots.length > 0) {\n const sectionRef = section;\n const slotsSnapshot = slots.map((s) => ({ ...s }));\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n slotsSnapshot.map(async (slot) => {\n let url: string | null = null;\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;\n return { slot, url };\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n const { slot, url } = r.value;\n const replacement = slot.replaceStr.replace(\"{url}\", url);\n html = html.replaceAll(slot.searchStr, replacement);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\n }\n }\n\n // Wait for image enrichment\n await Promise.allSettled(imagePromises);\n\n // Final fallback: any <img> still without src gets a placeholder\n for (const section of allSections) {\n const before = section.html;\n section.html = section.html.replace(\n /<img\\s(?![^>]*\\bsrc=)([^>]*?)>/gi,\n (_match, attrs) => {\n const altMatch = attrs.match(/alt=\"([^\"]*?)\"/);\n const query = altMatch?.[1] || \"image\";\n return `<img src=\"https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}\" ${attrs}>`;\n }\n );\n // Also replace any remaining data-image-query that wasn't enriched\n section.html = section.html.replace(\n /data-image-query=\"([^\"]+)\"/g,\n (_match, query) => {\n return `src=\"https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}\" data-enriched=\"placeholder\"`;\n }\n );\n if (section.html !== before) {\n onImageUpdate?.(section.id, section.html);\n }\n }\n\n onDone?.(allSections);\n return allSections;\n } catch (err: any) {\n const error = err instanceof Error ? err : new Error(err?.message || \"Generation failed\");\n onError?.(error);\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/generate.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  SYSTEM_PROMPT,
4
4
  extractJsonObjects,
5
5
  generateLanding
6
- } from "./chunk-XGFWZQ2Y.js";
6
+ } from "./chunk-3YGNTEU5.js";
7
7
  import "./chunk-X5GL44KS.js";
8
8
  export {
9
9
  PROMPT_SUFFIX,
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  SYSTEM_PROMPT,
12
12
  extractJsonObjects,
13
13
  generateLanding
14
- } from "./chunk-XGFWZQ2Y.js";
14
+ } from "./chunk-3YGNTEU5.js";
15
15
  import {
16
16
  REFINE_SYSTEM,
17
17
  refineLanding
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/html-tailwind-generator",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "AI-powered landing page generator with Tailwind CSS — canvas editor, streaming generation, and one-click deploy",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/generate.ts"],"sourcesContent":["import { streamText } from \"ai\";\nimport { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { nanoid } from \"nanoid\";\nimport { findImageSlots, type EnrichImagesOptions } from \"./images/enrichImages\";\nimport { searchImage } from \"./images/pexels\";\nimport { generateImage } from \"./images/dalleImages\";\nimport type { Section3 } from \"./types\";\n\nasync function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {\n // Prefer Anthropic for text generation when both keys are available\n const anthropicKey = opts.anthropicApiKey || process.env.ANTHROPIC_API_KEY;\n if (anthropicKey) {\n const anthropic = createAnthropic({ apiKey: anthropicKey });\n return anthropic(opts.modelId || opts.defaultAnthropic);\n }\n // Fallback to OpenAI for text only if no Anthropic key\n const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const { createOpenAI } = await import(\"@ai-sdk/openai\");\n const openai = createOpenAI({ apiKey: openaiKey });\n return openai(opts.modelId || opts.defaultOpenai);\n }\n // Last resort: createAnthropic() without key (uses env var)\n return createAnthropic()(opts.modelId || opts.defaultAnthropic);\n}\n\nexport const SYSTEM_PROMPT = `You are a world-class web designer who creates AWARD-WINNING landing pages. Your designs win Awwwards, FWA, and CSS Design Awards. You think in terms of visual hierarchy, whitespace, and emotional impact.\n\nRULES:\n- Each section is a complete <section> tag with Tailwind CSS classes\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import, no @tailwind directives)\n- NO JavaScript, only HTML+Tailwind\n- Each section must be independent and self-contained\n- Responsive: mobile-first with sm/md/lg/xl breakpoints\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real-looking content (not Lorem ipsum) — make it specific to the prompt\n\nRESPONSIVE — MANDATORY:\n- EVERY grid: grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 (NEVER grid-cols-3 alone)\n- EVERY flex row: flex flex-col md:flex-row (NEVER flex flex-row alone)\n- Text sizes: text-3xl md:text-5xl lg:text-7xl (NEVER text-7xl alone)\n- Images: w-full h-auto object-cover max-w-full\n- Padding: px-4 md:px-8 lg:px-16 (NEVER px-16 alone)\n- Hide decorative on mobile if breaks layout: hidden md:block\n\nIMAGES — CRITICAL:\n- EVERY image MUST use: <img data-image-query=\"english search query\" alt=\"description\" class=\"w-full h-auto object-cover rounded-xl\"/>\n- NEVER use <img> without data-image-query\n- NEVER include a src attribute — the system auto-replaces data-image-query with a real image URL\n- Queries must be generic stock-photo friendly (e.g. \"modern office\" not \"Juan's cybercafe\")\n- For avatar-like elements, use colored divs with initials instead of img tags (e.g. <div class=\"w-10 h-10 rounded-full bg-primary flex items-center justify-center text-on-primary font-bold\">JD</div>)\n\nCOLOR SYSTEM — CRITICAL (READ CAREFULLY):\n- Use semantic color classes: bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary, bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted, bg-secondary, text-secondary, bg-accent, text-accent\n- NEVER use hardcoded Tailwind color classes: NO bg-gray-*, bg-black, bg-white, bg-indigo-*, bg-blue-*, bg-purple-*, text-gray-*, text-black, text-white, etc.\n- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.\n- ALL backgrounds MUST use: bg-primary, bg-primary-dark, bg-surface, bg-surface-alt\n- ALL text MUST use: text-on-surface, text-on-surface-muted, text-on-primary, text-primary, text-accent\n- CONTRAST RULE: on bg-primary or bg-primary-dark → use text-on-primary. On bg-surface or bg-surface-alt → use text-on-surface or text-on-surface-muted. NEVER put text-on-surface on bg-primary or vice versa. text-accent is decorative — use sparingly on bg-surface/bg-surface-alt only.\n- ANTI-PATTERN: NEVER put bg-primary on BOTH the section AND elements inside it. If section is bg-primary, inner cards/elements should be bg-surface. If section is bg-surface, cards can use bg-surface-alt or bg-primary.\n- For gradients: from-primary to-primary-dark, from-surface to-surface-alt\n- For hover: hover:bg-primary-dark, hover:bg-primary-light\n\nDESIGN PHILOSOPHY — what separates good from GREAT:\n- WHITESPACE is your best friend. Generous padding (py-24, py-32, px-8). Let elements breathe.\n- CONTRAST: mix dark sections with light ones. Alternate bg-primary and bg-surface sections.\n- TYPOGRAPHY: use extreme size differences for hierarchy (text-7xl headline next to text-sm label)\n- DEPTH: overlapping elements, negative margins (-mt-12), z-index layering, shadows\n- ASYMMETRY: avoid centering everything. Use grid-cols-5 with col-span-3 + col-span-2. Offset elements.\n- TEXTURE: use subtle patterns, gradients, border treatments, rounded-3xl mixed with sharp edges\n- Each section should have a COMPLETELY DIFFERENT layout from the others\n\nSECTION LAYOUT — CRITICAL:\n- Each <section> must be full-width (bg goes edge-to-edge). NO max-w on the section itself.\n- Constrain content inside with a wrapper div: <section class=\"bg-primary py-24\"><div class=\"max-w-7xl mx-auto px-4 md:px-8\">...content...</div></section>\n- EVERY section follows this pattern. The <section> handles bg color + vertical padding. The inner <div> handles horizontal padding + max-width.\n\nTESTIMONIALS SECTION:\n- Cards MUST use bg-surface or bg-surface-alt with text-on-surface\n- If section bg is bg-primary or bg-primary-dark, cards MUST be bg-surface (light cards on dark bg)\n- Quote text: text-on-surface, italic\n- Avatar: colored div with initials (bg-accent text-on-primary or bg-primary-light text-on-primary)\n- Name: text-on-surface font-semibold. Role/company: text-on-surface-muted\n- NEVER use same dark bg for both section AND cards\n\nHERO SECTION — your masterpiece:\n- Use a 2-column grid (lg:grid-cols-2) that fills the full height, NOT content floating in empty space\n- Left column: headline + description + CTAs, vertically centered with flex flex-col justify-center\n- Right column: large hero image (data-image-query) filling the column, or a bento-grid of image + stat cards\n- Bold oversized headline (text-4xl md:text-6xl lg:text-7xl font-black leading-tight)\n- Tag/label above headline (uppercase, tracking-wider, text-xs text-accent)\n- Short description paragraph (text-lg text-on-surface-muted, max-w-lg)\n- 2 CTAs: primary (large, px-8 py-4, with → arrow) + secondary (ghost/outlined)\n- Optional: social proof bar below CTAs (avatar stack + \"2,847+ users\" text)\n- Min height: min-h-[90vh] with items-center on the grid so content is vertically centered\n- CRITICAL: the grid must stretch to fill the section height. Use min-h-[90vh] on the grid container itself, not just the section\n- NEVER leave large empty areas — if using min-h-[90vh], content must be centered/distributed within it\n\nTAILWIND v3 NOTES:\n- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)\n- Borders: border + border-gray-200 for visible borders`;\n\nexport const PROMPT_SUFFIX = `\n\nOUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Short Label\", \"html\": \"<section>...</section>\"}\n\nGenerate 7-9 sections. Always start with Hero and end with Footer.\nIMPORTANT: Make each section VISUALLY UNIQUE — different layouts, different background colors, different grid structures.\nThink like a premium design agency creating a $50K landing page.\nNO generic Bootstrap layouts. Use creative grids, bento layouts, overlapping elements, asymmetric columns.`;\n\n/**\n * Extract complete JSON objects from accumulated text using brace-depth tracking.\n */\nexport function extractJsonObjects(text: string): [any[], string] {\n const objects: any[] = [];\n let remaining = text;\n\n while (remaining.length > 0) {\n remaining = remaining.trimStart();\n if (!remaining.startsWith(\"{\")) {\n const nextBrace = remaining.indexOf(\"{\");\n if (nextBrace === -1) break;\n remaining = remaining.slice(nextBrace);\n continue;\n }\n\n let depth = 0;\n let inString = false;\n let escape = false;\n let end = -1;\n\n for (let i = 0; i < remaining.length; i++) {\n const ch = remaining[i];\n if (escape) { escape = false; continue; }\n if (ch === \"\\\\\") { escape = true; continue; }\n if (ch === '\"') { inString = !inString; continue; }\n if (inString) continue;\n if (ch === \"{\") depth++;\n if (ch === \"}\") { depth--; if (depth === 0) { end = i; break; } }\n }\n\n if (end === -1) break;\n\n const candidate = remaining.slice(0, end + 1);\n remaining = remaining.slice(end + 1);\n\n try {\n objects.push(JSON.parse(candidate));\n } catch {\n // malformed, skip\n }\n }\n\n return [objects, remaining];\n}\n\nexport interface GenerateOptions {\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */\n anthropicApiKey?: string;\n /** OpenAI API key. If provided, uses GPT-4o instead of Claude */\n openaiApiKey?: string;\n /** Landing page description prompt */\n prompt: string;\n /** Reference image (base64 data URI) for vision-based generation */\n referenceImage?: string;\n /** Extra instructions appended to the prompt */\n extraInstructions?: string;\n /** Custom system prompt (overrides default SYSTEM_PROMPT) */\n systemPrompt?: string;\n /** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */\n model?: string;\n /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */\n pexelsApiKey?: string;\n /** Called with temp DALL-E URL + query, returns permanent URL. Use to persist to S3/etc. */\n persistImage?: (tempUrl: string, query: string) => Promise<string>;\n /** Called when a new section is parsed from the stream */\n onSection?: (section: Section3) => void;\n /** Called when a section's images are enriched */\n onImageUpdate?: (sectionId: string, html: string) => void;\n /** Called when generation is complete */\n onDone?: (sections: Section3[]) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n}\n\n/**\n * Generate a landing page with streaming AI + image enrichment.\n * Returns all generated sections when complete.\n */\nexport async function generateLanding(options: GenerateOptions): Promise<Section3[]> {\n const {\n anthropicApiKey,\n openaiApiKey: _openaiApiKey,\n prompt,\n referenceImage,\n extraInstructions,\n systemPrompt = SYSTEM_PROMPT,\n model: modelId,\n pexelsApiKey,\n persistImage,\n onSection,\n onImageUpdate,\n onDone,\n onError,\n } = options;\n\n const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;\n const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: \"gpt-4o\", defaultAnthropic: \"claude-sonnet-4-6\" });\n\n // Build prompt content (supports multimodal with reference image)\n const extra = extraInstructions ? `\\nAdditional instructions: ${extraInstructions}` : \"\";\n const content: any[] = [];\n if (referenceImage) {\n content.push({ type: \"image\", image: referenceImage });\n content.push({\n type: \"text\",\n text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n } else {\n content.push({\n type: \"text\",\n text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n }\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: [{ role: \"user\", content }],\n });\n\n const allSections: Section3[] = [];\n const imagePromises: Promise<void>[] = [];\n let sectionOrder = 0;\n let buffer = \"\";\n\n try {\n for await (const chunk of result.textStream) {\n buffer += chunk;\n\n const [objects, remaining] = extractJsonObjects(buffer);\n buffer = remaining;\n\n for (const obj of objects) {\n if (!obj.html || !obj.label) continue;\n\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n\n allSections.push(section);\n onSection?.(section);\n\n // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)\n const slots = findImageSlots(section.html);\n if (slots.length > 0) {\n const sectionRef = section;\n const slotsSnapshot = slots.map((s) => ({ ...s }));\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n slotsSnapshot.map(async (slot) => {\n let url: string | null = null;\n // 1. DALL-E if openaiApiKey provided\n if (openaiApiKey) {\n try {\n const tempUrl = await generateImage(slot.query, openaiApiKey);\n url = persistImage ? await persistImage(tempUrl, slot.query) : tempUrl;\n } catch (e) {\n console.warn(`[dalle] failed for \"${slot.query}\":`, e);\n }\n }\n // 2. Pexels fallback\n if (!url) {\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n }\n // 3. Placeholder fallback\n url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;\n return { slot, url };\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n const { slot, url } = r.value;\n const replacement = slot.replaceStr.replace(\"{url}\", url);\n html = html.replaceAll(slot.searchStr, replacement);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\n }\n }\n\n // Parse remaining buffer\n if (buffer.trim()) {\n let cleaned = buffer.trim();\n if (cleaned.startsWith(\"```\")) {\n cleaned = cleaned\n .replace(/^```(?:json)?\\s*/, \"\")\n .replace(/\\s*```$/, \"\");\n }\n const [lastObjects] = extractJsonObjects(cleaned);\n for (const obj of lastObjects) {\n if (!obj.html || !obj.label) continue;\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n allSections.push(section);\n onSection?.(section);\n\n // Enrich images for remaining-buffer sections too\n const slots = findImageSlots(section.html);\n if (slots.length > 0) {\n const sectionRef = section;\n const slotsSnapshot = slots.map((s) => ({ ...s }));\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n slotsSnapshot.map(async (slot) => {\n let url: string | null = null;\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;\n return { slot, url };\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n const { slot, url } = r.value;\n const replacement = slot.replaceStr.replace(\"{url}\", url);\n html = html.replaceAll(slot.searchStr, replacement);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\n }\n }\n\n // Wait for image enrichment\n await Promise.allSettled(imagePromises);\n\n // Final fallback: any <img> still without src gets a placeholder\n for (const section of allSections) {\n const before = section.html;\n section.html = section.html.replace(\n /<img\\s(?![^>]*\\bsrc=)([^>]*?)>/gi,\n (_match, attrs) => {\n const altMatch = attrs.match(/alt=\"([^\"]*?)\"/);\n const query = altMatch?.[1] || \"image\";\n return `<img src=\"https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}\" ${attrs}>`;\n }\n );\n // Also replace any remaining data-image-query that wasn't enriched\n section.html = section.html.replace(\n /data-image-query=\"([^\"]+)\"/g,\n (_match, query) => {\n return `src=\"https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}\" data-enriched=\"placeholder\"`;\n }\n );\n if (section.html !== before) {\n onImageUpdate?.(section.id, section.html);\n }\n }\n\n onDone?.(allSections);\n return allSections;\n } catch (err: any) {\n const error = err instanceof Error ? err : new Error(err?.message || \"Generation failed\");\n onError?.(error);\n throw error;\n }\n}\n"],"mappings":"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","names":[]}