@easybits.cloud/html-tailwind-generator 0.2.27 → 0.2.29

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.
@@ -0,0 +1,225 @@
1
+ import {
2
+ findImageSlots,
3
+ generateImage,
4
+ searchImage
5
+ } from "./chunk-FM4IJA64.js";
6
+
7
+ // src/streamCore.ts
8
+ import { streamText } from "ai";
9
+ import { createAnthropic } from "@ai-sdk/anthropic";
10
+ import { nanoid } from "nanoid";
11
+ async function resolveModel(opts) {
12
+ const anthropicKey = opts.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
13
+ if (anthropicKey) {
14
+ const anthropic = createAnthropic({ apiKey: anthropicKey });
15
+ return anthropic(opts.modelId || opts.defaultAnthropic);
16
+ }
17
+ const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;
18
+ if (openaiKey) {
19
+ const { createOpenAI } = await import("@ai-sdk/openai");
20
+ const openai = createOpenAI({ apiKey: openaiKey });
21
+ return openai(opts.modelId || opts.defaultOpenai);
22
+ }
23
+ return createAnthropic()(opts.modelId || opts.defaultAnthropic);
24
+ }
25
+ function dataUrlToImagePart(dataUrl) {
26
+ const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
27
+ if (!match) return null;
28
+ return {
29
+ image: new Uint8Array(Buffer.from(match[2], "base64")),
30
+ mimeType: match[1]
31
+ };
32
+ }
33
+ function extractJsonObjects(text) {
34
+ const objects = [];
35
+ let remaining = text;
36
+ while (remaining.length > 0) {
37
+ remaining = remaining.trimStart();
38
+ if (!remaining.startsWith("{")) {
39
+ const nextBrace = remaining.indexOf("{");
40
+ if (nextBrace === -1) break;
41
+ remaining = remaining.slice(nextBrace);
42
+ continue;
43
+ }
44
+ let depth = 0;
45
+ let inString = false;
46
+ let escape = false;
47
+ let end = -1;
48
+ for (let i = 0; i < remaining.length; i++) {
49
+ const ch = remaining[i];
50
+ if (escape) {
51
+ escape = false;
52
+ continue;
53
+ }
54
+ if (ch === "\\") {
55
+ escape = true;
56
+ continue;
57
+ }
58
+ if (ch === '"') {
59
+ inString = !inString;
60
+ continue;
61
+ }
62
+ if (inString) continue;
63
+ if (ch === "{") depth++;
64
+ if (ch === "}") {
65
+ depth--;
66
+ if (depth === 0) {
67
+ end = i;
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ if (end === -1) break;
73
+ const candidate = remaining.slice(0, end + 1);
74
+ remaining = remaining.slice(end + 1);
75
+ try {
76
+ objects.push(JSON.parse(candidate));
77
+ } catch {
78
+ }
79
+ }
80
+ return [objects, remaining];
81
+ }
82
+ 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"><defs><linearGradient id="sh" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="%23e5e7eb"/><stop offset="50%" stop-color="%23f9fafb"/><stop offset="100%" stop-color="%23e5e7eb"/></linearGradient></defs><rect fill="%23f3f4f6" width="800" height="500" rx="12"/><rect fill="url(%23sh)" width="800" height="500" rx="12"><animate attributeName="x" from="-800" to="800" dur="1.5s" repeatCount="indefinite"/></rect><circle cx="370" cy="230" r="8" fill="%239ca3af" opacity=".5"><animate attributeName="opacity" values=".3;1;.3" dur="1.5s" repeatCount="indefinite"/></circle><circle cx="400" cy="230" r="8" fill="%239ca3af" opacity=".5"><animate attributeName="opacity" values=".3;1;.3" dur="1.5s" begin=".2s" repeatCount="indefinite"/></circle><circle cx="430" cy="230" r="8" fill="%239ca3af" opacity=".5"><animate attributeName="opacity" values=".3;1;.3" dur="1.5s" begin=".4s" repeatCount="indefinite"/></circle><text x="400" y="270" text-anchor="middle" fill="%239ca3af" font-family="system-ui" font-size="14">Generando imagen...</text></svg>`)}`;
83
+ function addLoadingPlaceholders(html) {
84
+ return html.replace(
85
+ /(<img\s[^>]*)data-image-query="([^"]+)"([^>]*?)(?:\s*\/?>)/gi,
86
+ (_match, before, query, after) => {
87
+ if (before.includes("src=") || after.includes("src=")) return _match;
88
+ return `${before}src="${LOADING_PLACEHOLDER}" data-image-query="${query}" alt="${query}"${after}>`;
89
+ }
90
+ );
91
+ }
92
+ async function streamGenerate(options) {
93
+ const {
94
+ anthropicApiKey,
95
+ openaiApiKey: _openaiApiKey,
96
+ model: modelId,
97
+ systemPrompt,
98
+ userContent,
99
+ pexelsApiKey,
100
+ persistImage,
101
+ onSection,
102
+ onImageUpdate,
103
+ onDone,
104
+ onError
105
+ } = options;
106
+ const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;
107
+ const model = await resolveModel({
108
+ openaiApiKey,
109
+ anthropicApiKey,
110
+ modelId,
111
+ defaultOpenai: "gpt-4o",
112
+ defaultAnthropic: "claude-sonnet-4-6"
113
+ });
114
+ const result = streamText({
115
+ model,
116
+ system: systemPrompt,
117
+ messages: [{ role: "user", content: userContent }]
118
+ });
119
+ const allSections = [];
120
+ const imagePromises = [];
121
+ let sectionOrder = 0;
122
+ let buffer = "";
123
+ function enrichSection(sectionRef) {
124
+ const slots = findImageSlots(sectionRef.html);
125
+ if (slots.length === 0) return;
126
+ const slotsSnapshot = slots.map((s) => ({ ...s }));
127
+ imagePromises.push(
128
+ (async () => {
129
+ const results = await Promise.allSettled(
130
+ slotsSnapshot.map(async (slot) => {
131
+ let url = null;
132
+ if (openaiApiKey) {
133
+ try {
134
+ const tempUrl = await generateImage(slot.query, openaiApiKey);
135
+ url = persistImage ? await persistImage(tempUrl, slot.query) : tempUrl;
136
+ } catch (e) {
137
+ console.warn(`[dalle] failed for "${slot.query}":`, e);
138
+ }
139
+ }
140
+ if (!url) {
141
+ const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
142
+ url = img?.url || null;
143
+ }
144
+ url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
145
+ return { slot, url };
146
+ })
147
+ );
148
+ let html = sectionRef.html;
149
+ for (const r of results) {
150
+ if (r.status === "fulfilled" && r.value) {
151
+ const { slot, url } = r.value;
152
+ const replacement = slot.replaceStr.replace("{url}", url);
153
+ html = html.replaceAll(slot.searchStr, replacement);
154
+ }
155
+ }
156
+ if (html !== sectionRef.html) {
157
+ sectionRef.html = html;
158
+ onImageUpdate?.(sectionRef.id, html);
159
+ }
160
+ })()
161
+ );
162
+ }
163
+ function processObject(obj) {
164
+ if (!obj.html || !obj.label) return;
165
+ const section = {
166
+ id: nanoid(8),
167
+ order: sectionOrder++,
168
+ html: addLoadingPlaceholders(obj.html),
169
+ label: obj.label
170
+ };
171
+ allSections.push(section);
172
+ onSection?.(section);
173
+ enrichSection(section);
174
+ }
175
+ try {
176
+ for await (const chunk of result.textStream) {
177
+ buffer += chunk;
178
+ const [objects, remaining] = extractJsonObjects(buffer);
179
+ buffer = remaining;
180
+ for (const obj of objects) processObject(obj);
181
+ }
182
+ if (buffer.trim()) {
183
+ let cleaned = buffer.trim();
184
+ if (cleaned.startsWith("```")) {
185
+ cleaned = cleaned.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
186
+ }
187
+ const [lastObjects] = extractJsonObjects(cleaned);
188
+ for (const obj of lastObjects) processObject(obj);
189
+ }
190
+ await Promise.allSettled(imagePromises);
191
+ for (const section of allSections) {
192
+ const before = section.html;
193
+ section.html = section.html.replace(
194
+ /<img\s(?![^>]*\bsrc=)([^>]*?)>/gi,
195
+ (_match, attrs) => {
196
+ const altMatch = attrs.match(/alt="([^"]*?)"/);
197
+ const query = altMatch?.[1] || "image";
198
+ return `<img src="https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}" ${attrs}>`;
199
+ }
200
+ );
201
+ section.html = section.html.replace(
202
+ /data-image-query="([^"]+)"/g,
203
+ (_match, query) => {
204
+ return `src="https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}" data-enriched="placeholder"`;
205
+ }
206
+ );
207
+ if (section.html !== before) {
208
+ onImageUpdate?.(section.id, section.html);
209
+ }
210
+ }
211
+ onDone?.(allSections);
212
+ return allSections;
213
+ } catch (err) {
214
+ const error = err instanceof Error ? err : new Error(err?.message || "Generation failed");
215
+ onError?.(error);
216
+ throw error;
217
+ }
218
+ }
219
+
220
+ export {
221
+ dataUrlToImagePart,
222
+ extractJsonObjects,
223
+ streamGenerate
224
+ };
225
+ //# sourceMappingURL=chunk-PNEUKC6I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/streamCore.ts"],"sourcesContent":["import { streamText } from \"ai\";\nimport { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { nanoid } from \"nanoid\";\nimport { findImageSlots } from \"./images/enrichImages\";\nimport { searchImage } from \"./images/pexels\";\nimport { generateImage } from \"./images/dalleImages\";\nimport type { Section3 } from \"./types\";\n\n/**\n * Resolve AI model from available keys.\n * Prefers Anthropic, falls back to OpenAI.\n */\nexport async function resolveModel(opts: {\n openaiApiKey?: string;\n anthropicApiKey?: string;\n modelId?: string;\n defaultOpenai: string;\n defaultAnthropic: string;\n}) {\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 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 return createAnthropic()(opts.modelId || opts.defaultAnthropic);\n}\n\n/**\n * Convert data URL to Uint8Array for AI SDK vision.\n */\nexport function dataUrlToImagePart(dataUrl: string): { image: Uint8Array; mimeType: string } | null {\n const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);\n if (!match) return null;\n return {\n image: new Uint8Array(Buffer.from(match[2], \"base64\")),\n mimeType: match[1],\n };\n}\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 placeholder for loading 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\"><defs><linearGradient id=\"sh\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0%\" stop-color=\"%23e5e7eb\"/><stop offset=\"50%\" stop-color=\"%23f9fafb\"/><stop offset=\"100%\" stop-color=\"%23e5e7eb\"/></linearGradient></defs><rect fill=\"%23f3f4f6\" width=\"800\" height=\"500\" rx=\"12\"/><rect fill=\"url(%23sh)\" width=\"800\" height=\"500\" rx=\"12\"><animate attributeName=\"x\" from=\"-800\" to=\"800\" dur=\"1.5s\" repeatCount=\"indefinite\"/></rect><circle cx=\"370\" cy=\"230\" r=\"8\" fill=\"%239ca3af\" opacity=\".5\"><animate attributeName=\"opacity\" values=\".3;1;.3\" dur=\"1.5s\" repeatCount=\"indefinite\"/></circle><circle cx=\"400\" cy=\"230\" r=\"8\" fill=\"%239ca3af\" opacity=\".5\"><animate attributeName=\"opacity\" values=\".3;1;.3\" dur=\"1.5s\" begin=\".2s\" repeatCount=\"indefinite\"/></circle><circle cx=\"430\" cy=\"230\" r=\"8\" fill=\"%239ca3af\" opacity=\".5\"><animate attributeName=\"opacity\" values=\".3;1;.3\" dur=\"1.5s\" begin=\".4s\" repeatCount=\"indefinite\"/></circle><text x=\"400\" y=\"270\" 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 */\nexport function addLoadingPlaceholders(html: string): string {\n return html.replace(\n /(<img\\s[^>]*)data-image-query=\"([^\"]+)\"([^>]*?)(?:\\s*\\/?>)/gi,\n (_match, before, query, after) => {\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 StreamGenerateOptions {\n /** Anthropic API key */\n anthropicApiKey?: string;\n /** OpenAI API key */\n openaiApiKey?: string;\n /** Model ID override */\n model?: string;\n /** System prompt */\n systemPrompt: string;\n /** User message content (text or multimodal parts) */\n userContent: any[];\n /** Pexels API key for image enrichment */\n pexelsApiKey?: string;\n /** Persist DALL-E images to permanent storage */\n persistImage?: (tempUrl: string, query: string) => Promise<string>;\n /** Called when a new section is parsed */\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 * Core streaming generation: stream AI text → parse NDJSON → emit sections → enrich images.\n * Used by both generateLanding and generateDocument.\n */\nexport async function streamGenerate(options: StreamGenerateOptions): Promise<Section3[]> {\n const {\n anthropicApiKey,\n openaiApiKey: _openaiApiKey,\n model: modelId,\n systemPrompt,\n userContent,\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({\n openaiApiKey,\n anthropicApiKey,\n modelId,\n defaultOpenai: \"gpt-4o\",\n defaultAnthropic: \"claude-sonnet-4-6\",\n });\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: [{ role: \"user\", content: userContent }],\n });\n\n const allSections: Section3[] = [];\n const imagePromises: Promise<void>[] = [];\n let sectionOrder = 0;\n let buffer = \"\";\n\n function enrichSection(sectionRef: Section3) {\n const slots = findImageSlots(sectionRef.html);\n if (slots.length === 0) return;\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 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 if (!url) {\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n }\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 function processObject(obj: any) {\n if (!obj.html || !obj.label) return;\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: addLoadingPlaceholders(obj.html),\n label: obj.label,\n };\n allSections.push(section);\n onSection?.(section);\n enrichSection(section);\n }\n\n try {\n for await (const chunk of result.textStream) {\n buffer += chunk;\n const [objects, remaining] = extractJsonObjects(buffer);\n buffer = remaining;\n for (const obj of objects) processObject(obj);\n }\n\n // Parse remaining buffer\n if (buffer.trim()) {\n let cleaned = buffer.trim();\n if (cleaned.startsWith(\"```\")) {\n cleaned = cleaned.replace(/^```(?:json)?\\s*/, \"\").replace(/\\s*```$/, \"\");\n }\n const [lastObjects] = extractJsonObjects(cleaned);\n for (const obj of lastObjects) processObject(obj);\n }\n\n // Wait for image enrichment\n await Promise.allSettled(imagePromises);\n\n // Final fallback for images without src\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 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":[]}
@@ -0,0 +1,107 @@
1
+ import {
2
+ dataUrlToImagePart,
3
+ streamGenerate
4
+ } from "./chunk-PNEUKC6I.js";
5
+
6
+ // src/generateDocument.ts
7
+ var DOCUMENT_SYSTEM_PROMPT = `You are a professional document designer who creates stunning letter-sized (8.5" \xD7 11") document pages using HTML + Tailwind CSS.
8
+
9
+ RULES:
10
+ - Each page is a <section> element sized for letter paper
11
+ - Page structure: <section class="w-[8.5in] min-h-[11in] relative overflow-hidden">
12
+ - The section itself has NO padding \u2014 backgrounds, gradients, and decorative elements go edge-to-edge
13
+ - For text content, use an inner wrapper: <div class="px-[0.75in] py-[0.5in]">...content...</div>
14
+ - Cover pages and decorative sections can use full-bleed backgrounds (bg-primary, gradients, images that fill the entire page)
15
+ - Content MUST NOT overflow page boundaries \u2014 be conservative with spacing
16
+ - Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import)
17
+ - NO JavaScript, only HTML+Tailwind
18
+ - All text content in Spanish unless the prompt specifies otherwise
19
+ - Use real content from the source material, not Lorem ipsum
20
+ - NOT responsive \u2014 fixed letter size, no breakpoints needed
21
+ - Sections can have ANY background \u2014 full-bleed color, gradients, or white. Not limited to white paper.
22
+
23
+ DESIGN:
24
+ - Professional, colorful designs: geometric decorations, gradients, accent colors
25
+ - Typography: use font weights (font-light to font-black), good hierarchy
26
+ - Tables: Tailwind table classes, alternating row colors, clean borders
27
+ - Decorative elements: colored sidebars, header bands, icon accents, SVG shapes
28
+ - First page MUST be a cover/title page with impactful design
29
+ - Use page-appropriate content density \u2014 don't cram too much on one page
30
+ - For numerical data: styled tables, colored bars, progress elements
31
+
32
+ COLOR SYSTEM \u2014 use semantic classes for decorative elements:
33
+ - bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary
34
+ - bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted
35
+ - bg-secondary, text-secondary, bg-accent, text-accent
36
+ - Use semantic colors freely for full-page backgrounds, headers, sidebars, decorative bars, table headers, accents
37
+ - Cover pages should use bold full-bleed backgrounds (bg-primary, gradients)
38
+ - CONTRAST RULE: on bg-primary \u2192 text-on-primary. On white/bg-surface \u2192 text-on-surface or text-primary
39
+ - Gradients: from-primary to-primary-dark
40
+
41
+ IMAGES:
42
+ - EVERY image MUST use: <img data-image-query="english search query" alt="description" class="w-full h-auto object-cover rounded-xl"/>
43
+ - NEVER include a src attribute \u2014 the system auto-replaces data-image-query with a real image
44
+ - For avatar-like elements, use colored divs with initials instead of img tags
45
+
46
+ TAILWIND v3 NOTES:
47
+ - Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
48
+ - Borders: border + border-gray-200 for visible borders`;
49
+ var DOCUMENT_PROMPT_SUFFIX = `
50
+
51
+ OUTPUT FORMAT: NDJSON \u2014 one JSON object per line, NO wrapper array, NO markdown fences.
52
+ Each line: {"label": "Page Title", "html": "<section class='w-[8.5in] min-h-[11in] relative overflow-hidden'>...</section>"}
53
+
54
+ Generate 3-8 pages depending on content length. First page = cover/title page.
55
+ Each page must fit within letter size (8.5" \xD7 11"). Be conservative with spacing.
56
+ Make each page visually distinct \u2014 different layouts, different accent placements.`;
57
+ async function generateDocument(options) {
58
+ const {
59
+ prompt,
60
+ logoUrl,
61
+ referenceImage,
62
+ extraInstructions,
63
+ ...rest
64
+ } = options;
65
+ const extra = extraInstructions ? `
66
+ Additional instructions: ${extraInstructions}` : "";
67
+ const logoInstruction = logoUrl ? `
68
+ LOGO: Include this logo on the cover page and as a small header on other pages:
69
+ <img src="${logoUrl}" alt="Logo" class="h-12 object-contain" />
70
+ Use this exact <img> tag \u2014 do NOT invent a different logo.` : "";
71
+ const content = [];
72
+ if (logoUrl?.startsWith("data:")) {
73
+ const converted = dataUrlToImagePart(logoUrl);
74
+ if (converted) {
75
+ content.push({ type: "image", ...converted });
76
+ }
77
+ }
78
+ if (referenceImage) {
79
+ const converted = dataUrlToImagePart(referenceImage);
80
+ if (converted) {
81
+ content.push({ type: "image", ...converted });
82
+ } else {
83
+ content.push({ type: "image", image: referenceImage });
84
+ }
85
+ content.push({
86
+ type: "text",
87
+ text: `Create a professional document inspired by this reference image for: ${prompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`
88
+ });
89
+ } else {
90
+ content.push({
91
+ type: "text",
92
+ text: `Create a professional document for: ${prompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`
93
+ });
94
+ }
95
+ return streamGenerate({
96
+ ...rest,
97
+ systemPrompt: DOCUMENT_SYSTEM_PROMPT,
98
+ userContent: content
99
+ });
100
+ }
101
+
102
+ export {
103
+ DOCUMENT_SYSTEM_PROMPT,
104
+ DOCUMENT_PROMPT_SUFFIX,
105
+ generateDocument
106
+ };
107
+ //# sourceMappingURL=chunk-QMT4BVDH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generateDocument.ts"],"sourcesContent":["import { streamGenerate, dataUrlToImagePart } from \"./streamCore\";\nimport type { Section3 } from \"./types\";\n\nexport const DOCUMENT_SYSTEM_PROMPT = `You are a professional document designer who creates stunning letter-sized (8.5\" × 11\") document pages using HTML + Tailwind CSS.\n\nRULES:\n- Each page is a <section> element sized for letter paper\n- Page structure: <section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden\">\n- The section itself has NO padding — backgrounds, gradients, and decorative elements go edge-to-edge\n- For text content, use an inner wrapper: <div class=\"px-[0.75in] py-[0.5in]\">...content...</div>\n- Cover pages and decorative sections can use full-bleed backgrounds (bg-primary, gradients, images that fill the entire page)\n- Content MUST NOT overflow page boundaries — be conservative with spacing\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import)\n- NO JavaScript, only HTML+Tailwind\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real content from the source material, not Lorem ipsum\n- NOT responsive — fixed letter size, no breakpoints needed\n- Sections can have ANY background — full-bleed color, gradients, or white. Not limited to white paper.\n\nDESIGN:\n- Professional, colorful designs: geometric decorations, gradients, accent colors\n- Typography: use font weights (font-light to font-black), good hierarchy\n- Tables: Tailwind table classes, alternating row colors, clean borders\n- Decorative elements: colored sidebars, header bands, icon accents, SVG shapes\n- First page MUST be a cover/title page with impactful design\n- Use page-appropriate content density — don't cram too much on one page\n- For numerical data: styled tables, colored bars, progress elements\n\nCOLOR SYSTEM — use semantic classes for decorative elements:\n- bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary\n- bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted\n- bg-secondary, text-secondary, bg-accent, text-accent\n- Use semantic colors freely for full-page backgrounds, headers, sidebars, decorative bars, table headers, accents\n- Cover pages should use bold full-bleed backgrounds (bg-primary, gradients)\n- CONTRAST RULE: on bg-primary → text-on-primary. On white/bg-surface → text-on-surface or text-primary\n- Gradients: from-primary to-primary-dark\n\nIMAGES:\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 include a src attribute — the system auto-replaces data-image-query with a real image\n- For avatar-like elements, use colored divs with initials instead of img tags\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 DOCUMENT_PROMPT_SUFFIX = `\n\nOUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Page Title\", \"html\": \"<section class='w-[8.5in] min-h-[11in] relative overflow-hidden'>...</section>\"}\n\nGenerate 3-8 pages depending on content length. First page = cover/title page.\nEach page must fit within letter size (8.5\" × 11\"). Be conservative with spacing.\nMake each page visually distinct — different layouts, different accent placements.`;\n\nexport interface GenerateDocumentOptions {\n anthropicApiKey?: string;\n openaiApiKey?: string;\n prompt: string;\n logoUrl?: string;\n referenceImage?: string;\n extraInstructions?: string;\n model?: string;\n pexelsApiKey?: string;\n persistImage?: (tempUrl: string, query: string) => Promise<string>;\n onSection?: (section: Section3) => void;\n onImageUpdate?: (sectionId: string, html: string) => void;\n onDone?: (sections: Section3[]) => void;\n onError?: (error: Error) => void;\n}\n\n/**\n * Generate a multi-page document with streaming AI + image enrichment.\n */\nexport async function generateDocument(options: GenerateDocumentOptions): Promise<Section3[]> {\n const {\n prompt,\n logoUrl,\n referenceImage,\n extraInstructions,\n ...rest\n } = options;\n\n const extra = extraInstructions ? `\\nAdditional instructions: ${extraInstructions}` : \"\";\n const logoInstruction = logoUrl\n ? `\\nLOGO: Include this logo on the cover page and as a small header on other pages:\\n<img src=\"${logoUrl}\" alt=\"Logo\" class=\"h-12 object-contain\" />\\nUse this exact <img> tag — do NOT invent a different logo.`\n : \"\";\n\n const content: any[] = [];\n\n // If logo is a data URL, send it as vision input so AI can see it\n if (logoUrl?.startsWith(\"data:\")) {\n const converted = dataUrlToImagePart(logoUrl);\n if (converted) {\n content.push({ type: \"image\", ...converted });\n }\n }\n\n if (referenceImage) {\n const converted = dataUrlToImagePart(referenceImage);\n if (converted) {\n content.push({ type: \"image\", ...converted });\n } else {\n content.push({ type: \"image\", image: referenceImage });\n }\n content.push({\n type: \"text\",\n text: `Create a professional document inspired by this reference image for: ${prompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`,\n });\n } else {\n content.push({\n type: \"text\",\n text: `Create a professional document for: ${prompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`,\n });\n }\n\n return streamGenerate({\n ...rest,\n systemPrompt: DOCUMENT_SYSTEM_PROMPT,\n userContent: content,\n });\n}\n"],"mappings":";;;;;;AAGO,IAAM,yBAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2C/B,IAAM,yBAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BtC,eAAsB,iBAAiB,SAAuD;AAC5F,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,QAAQ,oBAAoB;AAAA,2BAA8B,iBAAiB,KAAK;AACtF,QAAM,kBAAkB,UACpB;AAAA;AAAA,YAAgG,OAAO;AAAA,mEACvG;AAEJ,QAAM,UAAiB,CAAC;AAGxB,MAAI,SAAS,WAAW,OAAO,GAAG;AAChC,UAAM,YAAY,mBAAmB,OAAO;AAC5C,QAAI,WAAW;AACb,cAAQ,KAAK,EAAE,MAAM,SAAS,GAAG,UAAU,CAAC;AAAA,IAC9C;AAAA,EACF;AAEA,MAAI,gBAAgB;AAClB,UAAM,YAAY,mBAAmB,cAAc;AACnD,QAAI,WAAW;AACb,cAAQ,KAAK,EAAE,MAAM,SAAS,GAAG,UAAU,CAAC;AAAA,IAC9C,OAAO;AACL,cAAQ,KAAK,EAAE,MAAM,SAAS,OAAO,eAAe,CAAC;AAAA,IACvD;AACA,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN,MAAM,wEAAwE,MAAM,GAAG,eAAe,GAAG,KAAK,GAAG,sBAAsB;AAAA,IACzI,CAAC;AAAA,EACH,OAAO;AACL,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN,MAAM,uCAAuC,MAAM,GAAG,eAAe,GAAG,KAAK,GAAG,sBAAsB;AAAA,IACxG,CAAC;AAAA,EACH;AAEA,SAAO,eAAe;AAAA,IACpB,GAAG;AAAA,IACH,cAAc;AAAA,IACd,aAAa;AAAA,EACf,CAAC;AACH;","names":[]}
@@ -0,0 +1,131 @@
1
+ import {
2
+ dataUrlToImagePart,
3
+ streamGenerate
4
+ } from "./chunk-PNEUKC6I.js";
5
+
6
+ // src/generate.ts
7
+ var 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.
8
+
9
+ RULES:
10
+ - Each section is a complete <section> tag with Tailwind CSS classes
11
+ - Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import, no @tailwind directives)
12
+ - NO JavaScript, only HTML+Tailwind
13
+ - Each section must be independent and self-contained
14
+ - Responsive: mobile-first with sm/md/lg/xl breakpoints
15
+ - All text content in Spanish unless the prompt specifies otherwise
16
+ - Use real-looking content (not Lorem ipsum) \u2014 make it specific to the prompt
17
+
18
+ RESPONSIVE \u2014 MANDATORY:
19
+ - EVERY grid: grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 (NEVER grid-cols-3 alone)
20
+ - EVERY flex row: flex flex-col md:flex-row (NEVER flex flex-row alone)
21
+ - Text sizes: text-3xl md:text-5xl lg:text-7xl (NEVER text-7xl alone)
22
+ - Images: w-full h-auto object-cover max-w-full
23
+ - Padding: px-4 md:px-8 lg:px-16 (NEVER px-16 alone)
24
+ - Hide decorative on mobile if breaks layout: hidden md:block
25
+
26
+ IMAGES \u2014 CRITICAL:
27
+ - EVERY image MUST use: <img data-image-query="english search query" alt="description" class="w-full h-auto object-cover rounded-xl"/>
28
+ - NEVER use <img> without data-image-query
29
+ - NEVER include a src attribute \u2014 the system auto-replaces data-image-query with a real image URL
30
+ - Queries must be generic stock-photo friendly (e.g. "modern office" not "Juan's cybercafe")
31
+ - 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>)
32
+
33
+ COLOR SYSTEM \u2014 CRITICAL (READ CAREFULLY):
34
+ - 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
35
+ - 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.
36
+ - The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.
37
+ - ALL backgrounds MUST use: bg-primary, bg-primary-dark, bg-surface, bg-surface-alt
38
+ - ALL text MUST use: text-on-surface, text-on-surface-muted, text-on-primary, text-accent. Use text-primary ONLY on bg-surface/bg-surface-alt (it's the same hue as bg-primary \u2014 invisible on primary backgrounds).
39
+ - CONTRAST RULE: on bg-primary or bg-primary-dark \u2192 use ONLY text-on-primary. On bg-surface or bg-surface-alt \u2192 use text-on-surface, text-on-surface-muted, or text-primary. NEVER use text-primary on bg-primary \u2014 they are the SAME COLOR. NEVER put text-on-surface on bg-primary or text-on-primary on bg-surface. text-accent is decorative \u2014 use sparingly on bg-surface/bg-surface-alt only.
40
+ - 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.
41
+ - For gradients: from-primary to-primary-dark, from-surface to-surface-alt
42
+ - For hover: hover:bg-primary-dark, hover:bg-primary-light
43
+
44
+ DESIGN PHILOSOPHY \u2014 what separates good from GREAT:
45
+ - WHITESPACE is your best friend. Generous padding (py-24, py-32, px-8). Let elements breathe.
46
+ - CONTRAST: mix dark sections with light ones. Alternate bg-primary and bg-surface sections.
47
+ - TYPOGRAPHY: use extreme size differences for hierarchy (text-7xl headline next to text-sm label)
48
+ - DEPTH: overlapping elements, negative margins (-mt-12), z-index layering, shadows
49
+ - ASYMMETRY: avoid centering everything. Use grid-cols-5 with col-span-3 + col-span-2. Offset elements.
50
+ - TEXTURE: use subtle patterns, gradients, border treatments, rounded-3xl mixed with sharp edges
51
+ - Each section should have a COMPLETELY DIFFERENT layout from the others
52
+
53
+ SECTION LAYOUT \u2014 CRITICAL:
54
+ - Each <section> must be full-width (bg goes edge-to-edge). NO max-w on the section itself.
55
+ - 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>
56
+ - EVERY section follows this pattern. The <section> handles bg color + vertical padding. The inner <div> handles horizontal padding + max-width.
57
+
58
+ TESTIMONIALS SECTION:
59
+ - Cards MUST use bg-surface or bg-surface-alt with text-on-surface
60
+ - If section bg is bg-primary or bg-primary-dark, cards MUST be bg-surface (light cards on dark bg)
61
+ - Quote text: text-on-surface, italic
62
+ - Avatar: colored div with initials (bg-accent text-on-primary or bg-primary-light text-on-primary)
63
+ - Name: text-on-surface font-semibold. Role/company: text-on-surface-muted
64
+ - NEVER use same dark bg for both section AND cards
65
+
66
+ HERO SECTION \u2014 your masterpiece:
67
+ - Use a 2-column grid (lg:grid-cols-2) that fills the full height, NOT content floating in empty space
68
+ - Left column: headline + description + CTAs, vertically centered with flex flex-col justify-center
69
+ - Right column: large hero image (data-image-query) filling the column, or a bento-grid of image + stat cards
70
+ - Bold oversized headline (text-4xl md:text-6xl lg:text-7xl font-black leading-tight)
71
+ - Tag/label above headline (uppercase, tracking-wider, text-xs text-accent)
72
+ - Short description paragraph (text-lg text-on-surface-muted, max-w-lg)
73
+ - 2 CTAs: primary (large, px-8 py-4, with \u2192 arrow) + secondary (ghost/outlined)
74
+ - Optional: social proof bar below CTAs (avatar stack + "2,847+ users" text)
75
+ - Min height: min-h-[90vh] with items-center on the grid so content is vertically centered
76
+ - CRITICAL: the grid must stretch to fill the section height. Use min-h-[90vh] on the grid container itself, not just the section
77
+ - NEVER leave large empty areas \u2014 if using min-h-[90vh], content must be centered/distributed within it
78
+
79
+ TAILWIND v3 NOTES:
80
+ - Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
81
+ - Borders: border + border-gray-200 for visible borders`;
82
+ var PROMPT_SUFFIX = `
83
+
84
+ OUTPUT FORMAT: NDJSON \u2014 one JSON object per line, NO wrapper array, NO markdown fences.
85
+ Each line: {"label": "Short Label", "html": "<section>...</section>"}
86
+
87
+ Generate 7-9 sections. Always start with Hero and end with Footer.
88
+ IMPORTANT: Make each section VISUALLY UNIQUE \u2014 different layouts, different background colors, different grid structures.
89
+ Think like a premium design agency creating a $50K landing page.
90
+ NO generic Bootstrap layouts. Use creative grids, bento layouts, overlapping elements, asymmetric columns.`;
91
+ async function generateLanding(options) {
92
+ const {
93
+ prompt,
94
+ referenceImage,
95
+ extraInstructions,
96
+ systemPrompt = SYSTEM_PROMPT,
97
+ ...rest
98
+ } = options;
99
+ const extra = extraInstructions ? `
100
+ Additional instructions: ${extraInstructions}` : "";
101
+ const content = [];
102
+ if (referenceImage) {
103
+ const converted = dataUrlToImagePart(referenceImage);
104
+ if (converted) {
105
+ content.push({ type: "image", ...converted });
106
+ } else {
107
+ content.push({ type: "image", image: referenceImage });
108
+ }
109
+ content.push({
110
+ type: "text",
111
+ text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`
112
+ });
113
+ } else {
114
+ content.push({
115
+ type: "text",
116
+ text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`
117
+ });
118
+ }
119
+ return streamGenerate({
120
+ ...rest,
121
+ systemPrompt,
122
+ userContent: content
123
+ });
124
+ }
125
+
126
+ export {
127
+ SYSTEM_PROMPT,
128
+ PROMPT_SUFFIX,
129
+ generateLanding
130
+ };
131
+ //# sourceMappingURL=chunk-XUQ5UX5B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generate.ts"],"sourcesContent":["import { streamGenerate, dataUrlToImagePart, extractJsonObjects } from \"./streamCore\";\nimport type { Section3 } from \"./types\";\n\nexport { extractJsonObjects };\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-accent. Use text-primary ONLY on bg-surface/bg-surface-alt (it's the same hue as bg-primary — invisible on primary backgrounds).\n- CONTRAST RULE: on bg-primary or bg-primary-dark → use ONLY text-on-primary. On bg-surface or bg-surface-alt → use text-on-surface, text-on-surface-muted, or text-primary. NEVER use text-primary on bg-primary — they are the SAME COLOR. NEVER put text-on-surface on bg-primary or text-on-primary on bg-surface. 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\nexport interface GenerateOptions {\n anthropicApiKey?: string;\n openaiApiKey?: string;\n prompt: string;\n referenceImage?: string;\n extraInstructions?: string;\n systemPrompt?: string;\n model?: string;\n pexelsApiKey?: string;\n persistImage?: (tempUrl: string, query: string) => Promise<string>;\n onSection?: (section: Section3) => void;\n onImageUpdate?: (sectionId: string, html: string) => void;\n onDone?: (sections: Section3[]) => void;\n onError?: (error: Error) => void;\n}\n\n/**\n * Generate a landing page with streaming AI + image enrichment.\n */\nexport async function generateLanding(options: GenerateOptions): Promise<Section3[]> {\n const {\n prompt,\n referenceImage,\n extraInstructions,\n systemPrompt = SYSTEM_PROMPT,\n ...rest\n } = options;\n\n const extra = extraInstructions ? `\\nAdditional instructions: ${extraInstructions}` : \"\";\n const content: any[] = [];\n\n if (referenceImage) {\n const converted = dataUrlToImagePart(referenceImage);\n if (converted) {\n content.push({ type: \"image\", ...converted });\n } else {\n content.push({ type: \"image\", image: referenceImage });\n }\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 return streamGenerate({\n ...rest,\n systemPrompt,\n userContent: content,\n });\n}\n"],"mappings":";;;;;;AAKO,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4EtB,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6B7B,eAAsB,gBAAgB,SAA+C;AACnF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,QAAQ,oBAAoB;AAAA,2BAA8B,iBAAiB,KAAK;AACtF,QAAM,UAAiB,CAAC;AAExB,MAAI,gBAAgB;AAClB,UAAM,YAAY,mBAAmB,cAAc;AACnD,QAAI,WAAW;AACb,cAAQ,KAAK,EAAE,MAAM,SAAS,GAAG,UAAU,CAAC;AAAA,IAC9C,OAAO;AACL,cAAQ,KAAK,EAAE,MAAM,SAAS,OAAO,eAAe,CAAC;AAAA,IACvD;AACA,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN,MAAM,iEAAiE,MAAM,GAAG,KAAK,GAAG,aAAa;AAAA,IACvG,CAAC;AAAA,EACH,OAAO;AACL,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN,MAAM,gCAAgC,MAAM,GAAG,KAAK,GAAG,aAAa;AAAA,IACtE,CAAC;AAAA,EACH;AAEA,SAAO,eAAe;AAAA,IACpB,GAAG;AAAA,IACH;AAAA,IACA,aAAa;AAAA,EACf,CAAC;AACH;","names":[]}
@@ -1,42 +1,29 @@
1
1
  import { S as Section3 } from './types-Flpl4wDs.js';
2
2
 
3
- declare 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) \u2014 make it specific to the prompt\n\nRESPONSIVE \u2014 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 \u2014 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 \u2014 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 \u2014 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-accent. Use text-primary ONLY on bg-surface/bg-surface-alt (it's the same hue as bg-primary \u2014 invisible on primary backgrounds).\n- CONTRAST RULE: on bg-primary or bg-primary-dark \u2192 use ONLY text-on-primary. On bg-surface or bg-surface-alt \u2192 use text-on-surface, text-on-surface-muted, or text-primary. NEVER use text-primary on bg-primary \u2014 they are the SAME COLOR. NEVER put text-on-surface on bg-primary or text-on-primary on bg-surface. text-accent is decorative \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2192 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 \u2014 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";
4
- declare const PROMPT_SUFFIX = "\n\nOUTPUT FORMAT: NDJSON \u2014 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 \u2014 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.";
5
3
  /**
6
4
  * Extract complete JSON objects from accumulated text using brace-depth tracking.
7
5
  */
8
6
  declare function extractJsonObjects(text: string): [any[], string];
7
+
8
+ declare 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) \u2014 make it specific to the prompt\n\nRESPONSIVE \u2014 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 \u2014 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 \u2014 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 \u2014 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-accent. Use text-primary ONLY on bg-surface/bg-surface-alt (it's the same hue as bg-primary \u2014 invisible on primary backgrounds).\n- CONTRAST RULE: on bg-primary or bg-primary-dark \u2192 use ONLY text-on-primary. On bg-surface or bg-surface-alt \u2192 use text-on-surface, text-on-surface-muted, or text-primary. NEVER use text-primary on bg-primary \u2014 they are the SAME COLOR. NEVER put text-on-surface on bg-primary or text-on-primary on bg-surface. text-accent is decorative \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2192 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 \u2014 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";
9
+ declare const PROMPT_SUFFIX = "\n\nOUTPUT FORMAT: NDJSON \u2014 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 \u2014 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.";
9
10
  interface GenerateOptions {
10
- /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
11
11
  anthropicApiKey?: string;
12
- /** OpenAI API key. If provided, uses GPT-4o instead of Claude */
13
12
  openaiApiKey?: string;
14
- /** Landing page description prompt */
15
13
  prompt: string;
16
- /** Reference image (base64 data URI) for vision-based generation */
17
14
  referenceImage?: string;
18
- /** Extra instructions appended to the prompt */
19
15
  extraInstructions?: string;
20
- /** Custom system prompt (overrides default SYSTEM_PROMPT) */
21
16
  systemPrompt?: string;
22
- /** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */
23
17
  model?: string;
24
- /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
25
18
  pexelsApiKey?: string;
26
- /** Called with temp DALL-E URL + query, returns permanent URL. Use to persist to S3/etc. */
27
19
  persistImage?: (tempUrl: string, query: string) => Promise<string>;
28
- /** Called when a new section is parsed from the stream */
29
20
  onSection?: (section: Section3) => void;
30
- /** Called when a section's images are enriched */
31
21
  onImageUpdate?: (sectionId: string, html: string) => void;
32
- /** Called when generation is complete */
33
22
  onDone?: (sections: Section3[]) => void;
34
- /** Called on error */
35
23
  onError?: (error: Error) => void;
36
24
  }
37
25
  /**
38
26
  * Generate a landing page with streaming AI + image enrichment.
39
- * Returns all generated sections when complete.
40
27
  */
41
28
  declare function generateLanding(options: GenerateOptions): Promise<Section3[]>;
42
29
 
package/dist/generate.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  PROMPT_SUFFIX,
3
3
  SYSTEM_PROMPT,
4
- extractJsonObjects,
5
4
  generateLanding
6
- } from "./chunk-XRH5ICHB.js";
5
+ } from "./chunk-XUQ5UX5B.js";
6
+ import {
7
+ extractJsonObjects
8
+ } from "./chunk-PNEUKC6I.js";
7
9
  import "./chunk-FM4IJA64.js";
8
10
  export {
9
11
  PROMPT_SUFFIX,
@@ -0,0 +1,25 @@
1
+ import { S as Section3 } from './types-Flpl4wDs.js';
2
+
3
+ declare const DOCUMENT_SYSTEM_PROMPT = "You are a professional document designer who creates stunning letter-sized (8.5\" \u00D7 11\") document pages using HTML + Tailwind CSS.\n\nRULES:\n- Each page is a <section> element sized for letter paper\n- Page structure: <section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden\">\n- The section itself has NO padding \u2014 backgrounds, gradients, and decorative elements go edge-to-edge\n- For text content, use an inner wrapper: <div class=\"px-[0.75in] py-[0.5in]\">...content...</div>\n- Cover pages and decorative sections can use full-bleed backgrounds (bg-primary, gradients, images that fill the entire page)\n- Content MUST NOT overflow page boundaries \u2014 be conservative with spacing\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import)\n- NO JavaScript, only HTML+Tailwind\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real content from the source material, not Lorem ipsum\n- NOT responsive \u2014 fixed letter size, no breakpoints needed\n- Sections can have ANY background \u2014 full-bleed color, gradients, or white. Not limited to white paper.\n\nDESIGN:\n- Professional, colorful designs: geometric decorations, gradients, accent colors\n- Typography: use font weights (font-light to font-black), good hierarchy\n- Tables: Tailwind table classes, alternating row colors, clean borders\n- Decorative elements: colored sidebars, header bands, icon accents, SVG shapes\n- First page MUST be a cover/title page with impactful design\n- Use page-appropriate content density \u2014 don't cram too much on one page\n- For numerical data: styled tables, colored bars, progress elements\n\nCOLOR SYSTEM \u2014 use semantic classes for decorative elements:\n- bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary\n- bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted\n- bg-secondary, text-secondary, bg-accent, text-accent\n- Use semantic colors freely for full-page backgrounds, headers, sidebars, decorative bars, table headers, accents\n- Cover pages should use bold full-bleed backgrounds (bg-primary, gradients)\n- CONTRAST RULE: on bg-primary \u2192 text-on-primary. On white/bg-surface \u2192 text-on-surface or text-primary\n- Gradients: from-primary to-primary-dark\n\nIMAGES:\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 include a src attribute \u2014 the system auto-replaces data-image-query with a real image\n- For avatar-like elements, use colored divs with initials instead of img tags\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";
4
+ declare const DOCUMENT_PROMPT_SUFFIX = "\n\nOUTPUT FORMAT: NDJSON \u2014 one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Page Title\", \"html\": \"<section class='w-[8.5in] min-h-[11in] relative overflow-hidden'>...</section>\"}\n\nGenerate 3-8 pages depending on content length. First page = cover/title page.\nEach page must fit within letter size (8.5\" \u00D7 11\"). Be conservative with spacing.\nMake each page visually distinct \u2014 different layouts, different accent placements.";
5
+ interface GenerateDocumentOptions {
6
+ anthropicApiKey?: string;
7
+ openaiApiKey?: string;
8
+ prompt: string;
9
+ logoUrl?: string;
10
+ referenceImage?: string;
11
+ extraInstructions?: string;
12
+ model?: string;
13
+ pexelsApiKey?: string;
14
+ persistImage?: (tempUrl: string, query: string) => Promise<string>;
15
+ onSection?: (section: Section3) => void;
16
+ onImageUpdate?: (sectionId: string, html: string) => void;
17
+ onDone?: (sections: Section3[]) => void;
18
+ onError?: (error: Error) => void;
19
+ }
20
+ /**
21
+ * Generate a multi-page document with streaming AI + image enrichment.
22
+ */
23
+ declare function generateDocument(options: GenerateDocumentOptions): Promise<Section3[]>;
24
+
25
+ export { DOCUMENT_PROMPT_SUFFIX, DOCUMENT_SYSTEM_PROMPT, type GenerateDocumentOptions, generateDocument };
@@ -0,0 +1,13 @@
1
+ import {
2
+ DOCUMENT_PROMPT_SUFFIX,
3
+ DOCUMENT_SYSTEM_PROMPT,
4
+ generateDocument
5
+ } from "./chunk-QMT4BVDH.js";
6
+ import "./chunk-PNEUKC6I.js";
7
+ import "./chunk-FM4IJA64.js";
8
+ export {
9
+ DOCUMENT_PROMPT_SUFFIX,
10
+ DOCUMENT_SYSTEM_PROMPT,
11
+ generateDocument
12
+ };
13
+ //# sourceMappingURL=generateDocument.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { I as IframeMessage } from './types-Flpl4wDs.js';
3
3
  import { C as CustomColors } from './themes-CWFZ6GB-.js';
4
4
  export { L as LANDING_THEMES, a as LandingTheme, b as buildCustomTheme, c as buildCustomThemeCss, d as buildSingleThemeCss, e as buildThemeCss } from './themes-CWFZ6GB-.js';
5
5
  export { GenerateOptions, PROMPT_SUFFIX, SYSTEM_PROMPT, extractJsonObjects, generateLanding } from './generate.js';
6
+ export { DOCUMENT_PROMPT_SUFFIX, DOCUMENT_SYSTEM_PROMPT, GenerateDocumentOptions, generateDocument } from './generateDocument.js';
6
7
  export { REFINE_SYSTEM, RefineOptions, refineLanding } from './refine.js';
7
8
  export { DeployToEasyBitsOptions, DeployToS3Options, deployToEasyBits, deployToS3 } from './deploy.js';
8
9
  export { EnrichImagesOptions, PexelsResult, enrichImages, findImageSlots, generateImage, searchImage } from './images.js';
package/dist/index.js CHANGED
@@ -9,9 +9,16 @@ import "./chunk-RTGCZUNJ.js";
9
9
  import {
10
10
  PROMPT_SUFFIX,
11
11
  SYSTEM_PROMPT,
12
- extractJsonObjects,
13
12
  generateLanding
14
- } from "./chunk-XRH5ICHB.js";
13
+ } from "./chunk-XUQ5UX5B.js";
14
+ import {
15
+ DOCUMENT_PROMPT_SUFFIX,
16
+ DOCUMENT_SYSTEM_PROMPT,
17
+ generateDocument
18
+ } from "./chunk-QMT4BVDH.js";
19
+ import {
20
+ extractJsonObjects
21
+ } from "./chunk-PNEUKC6I.js";
15
22
  import {
16
23
  REFINE_SYSTEM,
17
24
  refineLanding
@@ -39,6 +46,8 @@ import {
39
46
  export {
40
47
  Canvas,
41
48
  CodeEditor,
49
+ DOCUMENT_PROMPT_SUFFIX,
50
+ DOCUMENT_SYSTEM_PROMPT,
42
51
  FloatingToolbar,
43
52
  LANDING_THEMES,
44
53
  PROMPT_SUFFIX,
@@ -57,6 +66,7 @@ export {
57
66
  enrichImages,
58
67
  extractJsonObjects,
59
68
  findImageSlots,
69
+ generateDocument,
60
70
  generateImage,
61
71
  generateLanding,
62
72
  getIframeScript,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/html-tailwind-generator",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
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",
@@ -23,6 +23,10 @@
23
23
  "types": "./dist/generate.d.ts",
24
24
  "import": "./dist/generate.js"
25
25
  },
26
+ "./generateDocument": {
27
+ "types": "./dist/generateDocument.d.ts",
28
+ "import": "./dist/generateDocument.js"
29
+ },
26
30
  "./refine": {
27
31
  "types": "./dist/refine.d.ts",
28
32
  "import": "./dist/refine.js"
@@ -1,358 +0,0 @@
1
- import {
2
- findImageSlots,
3
- generateImage,
4
- searchImage
5
- } from "./chunk-FM4IJA64.js";
6
-
7
- // src/generate.ts
8
- import { streamText } from "ai";
9
- import { createAnthropic } from "@ai-sdk/anthropic";
10
- import { nanoid } from "nanoid";
11
- async function resolveModel(opts) {
12
- const anthropicKey = opts.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
13
- if (anthropicKey) {
14
- const anthropic = createAnthropic({ apiKey: anthropicKey });
15
- return anthropic(opts.modelId || opts.defaultAnthropic);
16
- }
17
- const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;
18
- if (openaiKey) {
19
- const { createOpenAI } = await import("@ai-sdk/openai");
20
- const openai = createOpenAI({ apiKey: openaiKey });
21
- return openai(opts.modelId || opts.defaultOpenai);
22
- }
23
- return createAnthropic()(opts.modelId || opts.defaultAnthropic);
24
- }
25
- var 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.
26
-
27
- RULES:
28
- - Each section is a complete <section> tag with Tailwind CSS classes
29
- - Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import, no @tailwind directives)
30
- - NO JavaScript, only HTML+Tailwind
31
- - Each section must be independent and self-contained
32
- - Responsive: mobile-first with sm/md/lg/xl breakpoints
33
- - All text content in Spanish unless the prompt specifies otherwise
34
- - Use real-looking content (not Lorem ipsum) \u2014 make it specific to the prompt
35
-
36
- RESPONSIVE \u2014 MANDATORY:
37
- - EVERY grid: grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 (NEVER grid-cols-3 alone)
38
- - EVERY flex row: flex flex-col md:flex-row (NEVER flex flex-row alone)
39
- - Text sizes: text-3xl md:text-5xl lg:text-7xl (NEVER text-7xl alone)
40
- - Images: w-full h-auto object-cover max-w-full
41
- - Padding: px-4 md:px-8 lg:px-16 (NEVER px-16 alone)
42
- - Hide decorative on mobile if breaks layout: hidden md:block
43
-
44
- IMAGES \u2014 CRITICAL:
45
- - EVERY image MUST use: <img data-image-query="english search query" alt="description" class="w-full h-auto object-cover rounded-xl"/>
46
- - NEVER use <img> without data-image-query
47
- - NEVER include a src attribute \u2014 the system auto-replaces data-image-query with a real image URL
48
- - Queries must be generic stock-photo friendly (e.g. "modern office" not "Juan's cybercafe")
49
- - 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>)
50
-
51
- COLOR SYSTEM \u2014 CRITICAL (READ CAREFULLY):
52
- - 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
53
- - 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.
54
- - The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.
55
- - ALL backgrounds MUST use: bg-primary, bg-primary-dark, bg-surface, bg-surface-alt
56
- - ALL text MUST use: text-on-surface, text-on-surface-muted, text-on-primary, text-accent. Use text-primary ONLY on bg-surface/bg-surface-alt (it's the same hue as bg-primary \u2014 invisible on primary backgrounds).
57
- - CONTRAST RULE: on bg-primary or bg-primary-dark \u2192 use ONLY text-on-primary. On bg-surface or bg-surface-alt \u2192 use text-on-surface, text-on-surface-muted, or text-primary. NEVER use text-primary on bg-primary \u2014 they are the SAME COLOR. NEVER put text-on-surface on bg-primary or text-on-primary on bg-surface. text-accent is decorative \u2014 use sparingly on bg-surface/bg-surface-alt only.
58
- - 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.
59
- - For gradients: from-primary to-primary-dark, from-surface to-surface-alt
60
- - For hover: hover:bg-primary-dark, hover:bg-primary-light
61
-
62
- DESIGN PHILOSOPHY \u2014 what separates good from GREAT:
63
- - WHITESPACE is your best friend. Generous padding (py-24, py-32, px-8). Let elements breathe.
64
- - CONTRAST: mix dark sections with light ones. Alternate bg-primary and bg-surface sections.
65
- - TYPOGRAPHY: use extreme size differences for hierarchy (text-7xl headline next to text-sm label)
66
- - DEPTH: overlapping elements, negative margins (-mt-12), z-index layering, shadows
67
- - ASYMMETRY: avoid centering everything. Use grid-cols-5 with col-span-3 + col-span-2. Offset elements.
68
- - TEXTURE: use subtle patterns, gradients, border treatments, rounded-3xl mixed with sharp edges
69
- - Each section should have a COMPLETELY DIFFERENT layout from the others
70
-
71
- SECTION LAYOUT \u2014 CRITICAL:
72
- - Each <section> must be full-width (bg goes edge-to-edge). NO max-w on the section itself.
73
- - 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>
74
- - EVERY section follows this pattern. The <section> handles bg color + vertical padding. The inner <div> handles horizontal padding + max-width.
75
-
76
- TESTIMONIALS SECTION:
77
- - Cards MUST use bg-surface or bg-surface-alt with text-on-surface
78
- - If section bg is bg-primary or bg-primary-dark, cards MUST be bg-surface (light cards on dark bg)
79
- - Quote text: text-on-surface, italic
80
- - Avatar: colored div with initials (bg-accent text-on-primary or bg-primary-light text-on-primary)
81
- - Name: text-on-surface font-semibold. Role/company: text-on-surface-muted
82
- - NEVER use same dark bg for both section AND cards
83
-
84
- HERO SECTION \u2014 your masterpiece:
85
- - Use a 2-column grid (lg:grid-cols-2) that fills the full height, NOT content floating in empty space
86
- - Left column: headline + description + CTAs, vertically centered with flex flex-col justify-center
87
- - Right column: large hero image (data-image-query) filling the column, or a bento-grid of image + stat cards
88
- - Bold oversized headline (text-4xl md:text-6xl lg:text-7xl font-black leading-tight)
89
- - Tag/label above headline (uppercase, tracking-wider, text-xs text-accent)
90
- - Short description paragraph (text-lg text-on-surface-muted, max-w-lg)
91
- - 2 CTAs: primary (large, px-8 py-4, with \u2192 arrow) + secondary (ghost/outlined)
92
- - Optional: social proof bar below CTAs (avatar stack + "2,847+ users" text)
93
- - Min height: min-h-[90vh] with items-center on the grid so content is vertically centered
94
- - CRITICAL: the grid must stretch to fill the section height. Use min-h-[90vh] on the grid container itself, not just the section
95
- - NEVER leave large empty areas \u2014 if using min-h-[90vh], content must be centered/distributed within it
96
-
97
- TAILWIND v3 NOTES:
98
- - Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
99
- - Borders: border + border-gray-200 for visible borders`;
100
- var PROMPT_SUFFIX = `
101
-
102
- OUTPUT FORMAT: NDJSON \u2014 one JSON object per line, NO wrapper array, NO markdown fences.
103
- Each line: {"label": "Short Label", "html": "<section>...</section>"}
104
-
105
- Generate 7-9 sections. Always start with Hero and end with Footer.
106
- IMPORTANT: Make each section VISUALLY UNIQUE \u2014 different layouts, different background colors, different grid structures.
107
- Think like a premium design agency creating a $50K landing page.
108
- NO generic Bootstrap layouts. Use creative grids, bento layouts, overlapping elements, asymmetric columns.`;
109
- function extractJsonObjects(text) {
110
- const objects = [];
111
- let remaining = text;
112
- while (remaining.length > 0) {
113
- remaining = remaining.trimStart();
114
- if (!remaining.startsWith("{")) {
115
- const nextBrace = remaining.indexOf("{");
116
- if (nextBrace === -1) break;
117
- remaining = remaining.slice(nextBrace);
118
- continue;
119
- }
120
- let depth = 0;
121
- let inString = false;
122
- let escape = false;
123
- let end = -1;
124
- for (let i = 0; i < remaining.length; i++) {
125
- const ch = remaining[i];
126
- if (escape) {
127
- escape = false;
128
- continue;
129
- }
130
- if (ch === "\\") {
131
- escape = true;
132
- continue;
133
- }
134
- if (ch === '"') {
135
- inString = !inString;
136
- continue;
137
- }
138
- if (inString) continue;
139
- if (ch === "{") depth++;
140
- if (ch === "}") {
141
- depth--;
142
- if (depth === 0) {
143
- end = i;
144
- break;
145
- }
146
- }
147
- }
148
- if (end === -1) break;
149
- const candidate = remaining.slice(0, end + 1);
150
- remaining = remaining.slice(end + 1);
151
- try {
152
- objects.push(JSON.parse(candidate));
153
- } catch {
154
- }
155
- }
156
- return [objects, remaining];
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"><defs><linearGradient id="sh" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="%23e5e7eb"/><stop offset="50%" stop-color="%23f9fafb"/><stop offset="100%" stop-color="%23e5e7eb"/></linearGradient></defs><rect fill="%23f3f4f6" width="800" height="500" rx="12"/><rect fill="url(%23sh)" width="800" height="500" rx="12"><animate attributeName="x" from="-800" to="800" dur="1.5s" repeatCount="indefinite"/></rect><circle cx="370" cy="230" r="8" fill="%239ca3af" opacity=".5"><animate attributeName="opacity" values=".3;1;.3" dur="1.5s" repeatCount="indefinite"/></circle><circle cx="400" cy="230" r="8" fill="%239ca3af" opacity=".5"><animate attributeName="opacity" values=".3;1;.3" dur="1.5s" begin=".2s" repeatCount="indefinite"/></circle><circle cx="430" cy="230" r="8" fill="%239ca3af" opacity=".5"><animate attributeName="opacity" values=".3;1;.3" dur="1.5s" begin=".4s" repeatCount="indefinite"/></circle><text x="400" y="270" 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
- }
168
- async function generateLanding(options) {
169
- const {
170
- anthropicApiKey,
171
- openaiApiKey: _openaiApiKey,
172
- prompt,
173
- referenceImage,
174
- extraInstructions,
175
- systemPrompt = SYSTEM_PROMPT,
176
- model: modelId,
177
- pexelsApiKey,
178
- persistImage,
179
- onSection,
180
- onImageUpdate,
181
- onDone,
182
- onError
183
- } = options;
184
- const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;
185
- const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: "gpt-4o", defaultAnthropic: "claude-sonnet-4-6" });
186
- const extra = extraInstructions ? `
187
- Additional instructions: ${extraInstructions}` : "";
188
- const content = [];
189
- if (referenceImage) {
190
- const base64Match = referenceImage.match(/^data:([^;]+);base64,(.+)$/);
191
- if (base64Match) {
192
- content.push({ type: "image", image: new Uint8Array(Buffer.from(base64Match[2], "base64")), mimeType: base64Match[1] });
193
- } else {
194
- content.push({ type: "image", image: referenceImage });
195
- }
196
- content.push({
197
- type: "text",
198
- text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`
199
- });
200
- } else {
201
- content.push({
202
- type: "text",
203
- text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`
204
- });
205
- }
206
- const result = streamText({
207
- model,
208
- system: systemPrompt,
209
- messages: [{ role: "user", content }]
210
- });
211
- const allSections = [];
212
- const imagePromises = [];
213
- let sectionOrder = 0;
214
- let buffer = "";
215
- try {
216
- for await (const chunk of result.textStream) {
217
- buffer += chunk;
218
- const [objects, remaining] = extractJsonObjects(buffer);
219
- buffer = remaining;
220
- for (const obj of objects) {
221
- if (!obj.html || !obj.label) continue;
222
- const section = {
223
- id: nanoid(8),
224
- order: sectionOrder++,
225
- html: obj.html,
226
- label: obj.label
227
- };
228
- section.html = addLoadingPlaceholders(section.html);
229
- allSections.push(section);
230
- onSection?.(section);
231
- const slots = findImageSlots(section.html);
232
- if (slots.length > 0) {
233
- const sectionRef = section;
234
- const slotsSnapshot = slots.map((s) => ({ ...s }));
235
- imagePromises.push(
236
- (async () => {
237
- const results = await Promise.allSettled(
238
- slotsSnapshot.map(async (slot) => {
239
- let url = null;
240
- if (openaiApiKey) {
241
- try {
242
- const tempUrl = await generateImage(slot.query, openaiApiKey);
243
- url = persistImage ? await persistImage(tempUrl, slot.query) : tempUrl;
244
- } catch (e) {
245
- console.warn(`[dalle] failed for "${slot.query}":`, e);
246
- }
247
- }
248
- if (!url) {
249
- const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
250
- url = img?.url || null;
251
- }
252
- url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
253
- return { slot, url };
254
- })
255
- );
256
- let html = sectionRef.html;
257
- for (const r of results) {
258
- if (r.status === "fulfilled" && r.value) {
259
- const { slot, url } = r.value;
260
- const replacement = slot.replaceStr.replace("{url}", url);
261
- html = html.replaceAll(slot.searchStr, replacement);
262
- }
263
- }
264
- if (html !== sectionRef.html) {
265
- sectionRef.html = html;
266
- onImageUpdate?.(sectionRef.id, html);
267
- }
268
- })()
269
- );
270
- }
271
- }
272
- }
273
- if (buffer.trim()) {
274
- let cleaned = buffer.trim();
275
- if (cleaned.startsWith("```")) {
276
- cleaned = cleaned.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
277
- }
278
- const [lastObjects] = extractJsonObjects(cleaned);
279
- for (const obj of lastObjects) {
280
- if (!obj.html || !obj.label) continue;
281
- const section = {
282
- id: nanoid(8),
283
- order: sectionOrder++,
284
- html: obj.html,
285
- label: obj.label
286
- };
287
- section.html = addLoadingPlaceholders(section.html);
288
- allSections.push(section);
289
- onSection?.(section);
290
- const slots = findImageSlots(section.html);
291
- if (slots.length > 0) {
292
- const sectionRef = section;
293
- const slotsSnapshot = slots.map((s) => ({ ...s }));
294
- imagePromises.push(
295
- (async () => {
296
- const results = await Promise.allSettled(
297
- slotsSnapshot.map(async (slot) => {
298
- let url = null;
299
- const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
300
- url = img?.url || null;
301
- url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
302
- return { slot, url };
303
- })
304
- );
305
- let html = sectionRef.html;
306
- for (const r of results) {
307
- if (r.status === "fulfilled" && r.value) {
308
- const { slot, url } = r.value;
309
- const replacement = slot.replaceStr.replace("{url}", url);
310
- html = html.replaceAll(slot.searchStr, replacement);
311
- }
312
- }
313
- if (html !== sectionRef.html) {
314
- sectionRef.html = html;
315
- onImageUpdate?.(sectionRef.id, html);
316
- }
317
- })()
318
- );
319
- }
320
- }
321
- }
322
- await Promise.allSettled(imagePromises);
323
- for (const section of allSections) {
324
- const before = section.html;
325
- section.html = section.html.replace(
326
- /<img\s(?![^>]*\bsrc=)([^>]*?)>/gi,
327
- (_match, attrs) => {
328
- const altMatch = attrs.match(/alt="([^"]*?)"/);
329
- const query = altMatch?.[1] || "image";
330
- return `<img src="https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}" ${attrs}>`;
331
- }
332
- );
333
- section.html = section.html.replace(
334
- /data-image-query="([^"]+)"/g,
335
- (_match, query) => {
336
- return `src="https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}" data-enriched="placeholder"`;
337
- }
338
- );
339
- if (section.html !== before) {
340
- onImageUpdate?.(section.id, section.html);
341
- }
342
- }
343
- onDone?.(allSections);
344
- return allSections;
345
- } catch (err) {
346
- const error = err instanceof Error ? err : new Error(err?.message || "Generation failed");
347
- onError?.(error);
348
- throw error;
349
- }
350
- }
351
-
352
- export {
353
- SYSTEM_PROMPT,
354
- PROMPT_SUFFIX,
355
- extractJsonObjects,
356
- generateLanding
357
- };
358
- //# sourceMappingURL=chunk-XRH5ICHB.js.map
@@ -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-accent. Use text-primary ONLY on bg-surface/bg-surface-alt (it's the same hue as bg-primary — invisible on primary backgrounds).\n- CONTRAST RULE: on bg-primary or bg-primary-dark → use ONLY text-on-primary. On bg-surface or bg-surface-alt → use text-on-surface, text-on-surface-muted, or text-primary. NEVER use text-primary on bg-primary — they are the SAME COLOR. NEVER put text-on-surface on bg-primary or text-on-primary on bg-surface. 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\"><defs><linearGradient id=\"sh\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0%\" stop-color=\"%23e5e7eb\"/><stop offset=\"50%\" stop-color=\"%23f9fafb\"/><stop offset=\"100%\" stop-color=\"%23e5e7eb\"/></linearGradient></defs><rect fill=\"%23f3f4f6\" width=\"800\" height=\"500\" rx=\"12\"/><rect fill=\"url(%23sh)\" width=\"800\" height=\"500\" rx=\"12\"><animate attributeName=\"x\" from=\"-800\" to=\"800\" dur=\"1.5s\" repeatCount=\"indefinite\"/></rect><circle cx=\"370\" cy=\"230\" r=\"8\" fill=\"%239ca3af\" opacity=\".5\"><animate attributeName=\"opacity\" values=\".3;1;.3\" dur=\"1.5s\" repeatCount=\"indefinite\"/></circle><circle cx=\"400\" cy=\"230\" r=\"8\" fill=\"%239ca3af\" opacity=\".5\"><animate attributeName=\"opacity\" values=\".3;1;.3\" dur=\"1.5s\" begin=\".2s\" repeatCount=\"indefinite\"/></circle><circle cx=\"430\" cy=\"230\" r=\"8\" fill=\"%239ca3af\" opacity=\".5\"><animate attributeName=\"opacity\" values=\".3;1;.3\" dur=\"1.5s\" begin=\".4s\" repeatCount=\"indefinite\"/></circle><text x=\"400\" y=\"270\" 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 // Convert data URLs to Uint8Array (AI SDK doesn't accept data: URLs directly)\n const base64Match = referenceImage.match(/^data:([^;]+);base64,(.+)$/);\n if (base64Match) {\n content.push({ type: \"image\", image: new Uint8Array(Buffer.from(base64Match[2], \"base64\")), mimeType: base64Match[1] });\n } else {\n content.push({ type: \"image\", image: referenceImage });\n }\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":[]}