@easybits.cloud/html-tailwind-generator 0.2.26 → 0.2.28
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.
- package/README.md +1 -0
- package/dist/chunk-5Y3R63LZ.js +104 -0
- package/dist/chunk-5Y3R63LZ.js.map +1 -0
- package/dist/chunk-PNEUKC6I.js +225 -0
- package/dist/chunk-PNEUKC6I.js.map +1 -0
- package/dist/chunk-XUQ5UX5B.js +131 -0
- package/dist/chunk-XUQ5UX5B.js.map +1 -0
- package/dist/generate.d.ts +3 -16
- package/dist/generate.js +4 -2
- package/dist/generateDocument.d.ts +25 -0
- package/dist/generateDocument.js +13 -0
- package/dist/generateDocument.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -2
- package/package.json +5 -1
- package/dist/chunk-MZ57RXKT.js +0 -353
- package/dist/chunk-MZ57RXKT.js.map +0 -1
package/README.md
CHANGED
|
@@ -225,6 +225,7 @@ The generator uses a semantic color system with CSS custom properties:
|
|
|
225
225
|
- [ ] **i18n** — Component labels are in Spanish. Add a `locale` prop or i18n system for English and other languages.
|
|
226
226
|
- [ ] **Tests** — Unit tests for `extractJsonObjects`, `findImageSlots`, `buildDeployHtml`, `buildCustomTheme`.
|
|
227
227
|
- [ ] **Storybook** — Visual stories for Canvas, SectionList, FloatingToolbar, CodeEditor.
|
|
228
|
+
- [ ] **Upgrade OpenAI models** — Evaluate GPT-5 / GPT-5-mini as defaults for generation and refinement. Compare quality, latency, and cost vs current GPT-4o/4o-mini. Update `resolveModel()` defaults if better.
|
|
228
229
|
|
|
229
230
|
## Used in Production
|
|
230
231
|
|
|
@@ -0,0 +1,104 @@
|
|
|
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] bg-white relative overflow-hidden p-[0.75in]">
|
|
12
|
+
- Content MUST NOT overflow page boundaries \u2014 be conservative with spacing
|
|
13
|
+
- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import)
|
|
14
|
+
- NO JavaScript, only HTML+Tailwind
|
|
15
|
+
- All text content in Spanish unless the prompt specifies otherwise
|
|
16
|
+
- Use real content from the source material, not Lorem ipsum
|
|
17
|
+
- NOT responsive \u2014 fixed letter size, no breakpoints needed
|
|
18
|
+
- Background is always white paper; use colored accents for decoration
|
|
19
|
+
|
|
20
|
+
DESIGN:
|
|
21
|
+
- Professional, colorful designs: geometric decorations, gradients, accent colors
|
|
22
|
+
- Typography: use font weights (font-light to font-black), good hierarchy
|
|
23
|
+
- Tables: Tailwind table classes, alternating row colors, clean borders
|
|
24
|
+
- Decorative elements: colored sidebars, header bands, icon accents, SVG shapes
|
|
25
|
+
- First page MUST be a cover/title page with impactful design
|
|
26
|
+
- Use page-appropriate content density \u2014 don't cram too much on one page
|
|
27
|
+
- For numerical data: styled tables, colored bars, progress elements
|
|
28
|
+
|
|
29
|
+
COLOR SYSTEM \u2014 use semantic classes for decorative elements:
|
|
30
|
+
- bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary
|
|
31
|
+
- bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted
|
|
32
|
+
- bg-secondary, text-secondary, bg-accent, text-accent
|
|
33
|
+
- Page background is ALWAYS white (bg-white on the section itself)
|
|
34
|
+
- Use semantic colors for headers, sidebars, decorative bars, table headers, accents
|
|
35
|
+
- CONTRAST RULE: on bg-primary \u2192 text-on-primary. On white/bg-surface \u2192 text-on-surface or text-primary
|
|
36
|
+
- Gradients: from-primary to-primary-dark
|
|
37
|
+
|
|
38
|
+
IMAGES:
|
|
39
|
+
- EVERY image MUST use: <img data-image-query="english search query" alt="description" class="w-full h-auto object-cover rounded-xl"/>
|
|
40
|
+
- NEVER include a src attribute \u2014 the system auto-replaces data-image-query with a real image
|
|
41
|
+
- For avatar-like elements, use colored divs with initials instead of img tags
|
|
42
|
+
|
|
43
|
+
TAILWIND v3 NOTES:
|
|
44
|
+
- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
|
|
45
|
+
- Borders: border + border-gray-200 for visible borders`;
|
|
46
|
+
var DOCUMENT_PROMPT_SUFFIX = `
|
|
47
|
+
|
|
48
|
+
OUTPUT FORMAT: NDJSON \u2014 one JSON object per line, NO wrapper array, NO markdown fences.
|
|
49
|
+
Each line: {"label": "Page Title", "html": "<section class='w-[8.5in] min-h-[11in] bg-white relative overflow-hidden p-[0.75in]'>...</section>"}
|
|
50
|
+
|
|
51
|
+
Generate 3-8 pages depending on content length. First page = cover/title page.
|
|
52
|
+
Each page must fit within letter size (8.5" \xD7 11"). Be conservative with spacing.
|
|
53
|
+
Make each page visually distinct \u2014 different layouts, different accent placements.`;
|
|
54
|
+
async function generateDocument(options) {
|
|
55
|
+
const {
|
|
56
|
+
prompt,
|
|
57
|
+
logoUrl,
|
|
58
|
+
referenceImage,
|
|
59
|
+
extraInstructions,
|
|
60
|
+
...rest
|
|
61
|
+
} = options;
|
|
62
|
+
const extra = extraInstructions ? `
|
|
63
|
+
Additional instructions: ${extraInstructions}` : "";
|
|
64
|
+
const logoInstruction = logoUrl ? `
|
|
65
|
+
LOGO: Include this logo on the cover page and as a small header on other pages:
|
|
66
|
+
<img src="${logoUrl}" alt="Logo" class="h-12 object-contain" />
|
|
67
|
+
Use this exact <img> tag \u2014 do NOT invent a different logo.` : "";
|
|
68
|
+
const content = [];
|
|
69
|
+
if (logoUrl?.startsWith("data:")) {
|
|
70
|
+
const converted = dataUrlToImagePart(logoUrl);
|
|
71
|
+
if (converted) {
|
|
72
|
+
content.push({ type: "image", ...converted });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (referenceImage) {
|
|
76
|
+
const converted = dataUrlToImagePart(referenceImage);
|
|
77
|
+
if (converted) {
|
|
78
|
+
content.push({ type: "image", ...converted });
|
|
79
|
+
} else {
|
|
80
|
+
content.push({ type: "image", image: referenceImage });
|
|
81
|
+
}
|
|
82
|
+
content.push({
|
|
83
|
+
type: "text",
|
|
84
|
+
text: `Create a professional document inspired by this reference image for: ${prompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
content.push({
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `Create a professional document for: ${prompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return streamGenerate({
|
|
93
|
+
...rest,
|
|
94
|
+
systemPrompt: DOCUMENT_SYSTEM_PROMPT,
|
|
95
|
+
userContent: content
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
DOCUMENT_SYSTEM_PROMPT,
|
|
101
|
+
DOCUMENT_PROMPT_SUFFIX,
|
|
102
|
+
generateDocument
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=chunk-5Y3R63LZ.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] bg-white relative overflow-hidden p-[0.75in]\">\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- Background is always white paper; use colored accents for decoration\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- Page background is ALWAYS white (bg-white on the section itself)\n- Use semantic colors for headers, sidebars, decorative bars, table headers, accents\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] bg-white relative overflow-hidden p-[0.75in]'>...</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;AAwC/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,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,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":[]}
|
package/dist/generate.d.ts
CHANGED
|
@@ -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-
|
|
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] bg-white relative overflow-hidden p-[0.75in]\">\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- Background is always white paper; use colored accents for decoration\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- Page background is ALWAYS white (bg-white on the section itself)\n- Use semantic colors for headers, sidebars, decorative bars, table headers, accents\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] bg-white relative overflow-hidden p-[0.75in]'>...</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-5Y3R63LZ.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-
|
|
13
|
+
} from "./chunk-XUQ5UX5B.js";
|
|
14
|
+
import {
|
|
15
|
+
DOCUMENT_PROMPT_SUFFIX,
|
|
16
|
+
DOCUMENT_SYSTEM_PROMPT,
|
|
17
|
+
generateDocument
|
|
18
|
+
} from "./chunk-5Y3R63LZ.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.
|
|
3
|
+
"version": "0.2.28",
|
|
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"
|
package/dist/chunk-MZ57RXKT.js
DELETED
|
@@ -1,353 +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
|
-
content.push({ type: "image", image: referenceImage });
|
|
191
|
-
content.push({
|
|
192
|
-
type: "text",
|
|
193
|
-
text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`
|
|
194
|
-
});
|
|
195
|
-
} else {
|
|
196
|
-
content.push({
|
|
197
|
-
type: "text",
|
|
198
|
-
text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
const result = streamText({
|
|
202
|
-
model,
|
|
203
|
-
system: systemPrompt,
|
|
204
|
-
messages: [{ role: "user", content }]
|
|
205
|
-
});
|
|
206
|
-
const allSections = [];
|
|
207
|
-
const imagePromises = [];
|
|
208
|
-
let sectionOrder = 0;
|
|
209
|
-
let buffer = "";
|
|
210
|
-
try {
|
|
211
|
-
for await (const chunk of result.textStream) {
|
|
212
|
-
buffer += chunk;
|
|
213
|
-
const [objects, remaining] = extractJsonObjects(buffer);
|
|
214
|
-
buffer = remaining;
|
|
215
|
-
for (const obj of objects) {
|
|
216
|
-
if (!obj.html || !obj.label) continue;
|
|
217
|
-
const section = {
|
|
218
|
-
id: nanoid(8),
|
|
219
|
-
order: sectionOrder++,
|
|
220
|
-
html: obj.html,
|
|
221
|
-
label: obj.label
|
|
222
|
-
};
|
|
223
|
-
section.html = addLoadingPlaceholders(section.html);
|
|
224
|
-
allSections.push(section);
|
|
225
|
-
onSection?.(section);
|
|
226
|
-
const slots = findImageSlots(section.html);
|
|
227
|
-
if (slots.length > 0) {
|
|
228
|
-
const sectionRef = section;
|
|
229
|
-
const slotsSnapshot = slots.map((s) => ({ ...s }));
|
|
230
|
-
imagePromises.push(
|
|
231
|
-
(async () => {
|
|
232
|
-
const results = await Promise.allSettled(
|
|
233
|
-
slotsSnapshot.map(async (slot) => {
|
|
234
|
-
let url = null;
|
|
235
|
-
if (openaiApiKey) {
|
|
236
|
-
try {
|
|
237
|
-
const tempUrl = await generateImage(slot.query, openaiApiKey);
|
|
238
|
-
url = persistImage ? await persistImage(tempUrl, slot.query) : tempUrl;
|
|
239
|
-
} catch (e) {
|
|
240
|
-
console.warn(`[dalle] failed for "${slot.query}":`, e);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (!url) {
|
|
244
|
-
const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
|
|
245
|
-
url = img?.url || null;
|
|
246
|
-
}
|
|
247
|
-
url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
|
|
248
|
-
return { slot, url };
|
|
249
|
-
})
|
|
250
|
-
);
|
|
251
|
-
let html = sectionRef.html;
|
|
252
|
-
for (const r of results) {
|
|
253
|
-
if (r.status === "fulfilled" && r.value) {
|
|
254
|
-
const { slot, url } = r.value;
|
|
255
|
-
const replacement = slot.replaceStr.replace("{url}", url);
|
|
256
|
-
html = html.replaceAll(slot.searchStr, replacement);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
if (html !== sectionRef.html) {
|
|
260
|
-
sectionRef.html = html;
|
|
261
|
-
onImageUpdate?.(sectionRef.id, html);
|
|
262
|
-
}
|
|
263
|
-
})()
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (buffer.trim()) {
|
|
269
|
-
let cleaned = buffer.trim();
|
|
270
|
-
if (cleaned.startsWith("```")) {
|
|
271
|
-
cleaned = cleaned.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
|
|
272
|
-
}
|
|
273
|
-
const [lastObjects] = extractJsonObjects(cleaned);
|
|
274
|
-
for (const obj of lastObjects) {
|
|
275
|
-
if (!obj.html || !obj.label) continue;
|
|
276
|
-
const section = {
|
|
277
|
-
id: nanoid(8),
|
|
278
|
-
order: sectionOrder++,
|
|
279
|
-
html: obj.html,
|
|
280
|
-
label: obj.label
|
|
281
|
-
};
|
|
282
|
-
section.html = addLoadingPlaceholders(section.html);
|
|
283
|
-
allSections.push(section);
|
|
284
|
-
onSection?.(section);
|
|
285
|
-
const slots = findImageSlots(section.html);
|
|
286
|
-
if (slots.length > 0) {
|
|
287
|
-
const sectionRef = section;
|
|
288
|
-
const slotsSnapshot = slots.map((s) => ({ ...s }));
|
|
289
|
-
imagePromises.push(
|
|
290
|
-
(async () => {
|
|
291
|
-
const results = await Promise.allSettled(
|
|
292
|
-
slotsSnapshot.map(async (slot) => {
|
|
293
|
-
let url = null;
|
|
294
|
-
const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
|
|
295
|
-
url = img?.url || null;
|
|
296
|
-
url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
|
|
297
|
-
return { slot, url };
|
|
298
|
-
})
|
|
299
|
-
);
|
|
300
|
-
let html = sectionRef.html;
|
|
301
|
-
for (const r of results) {
|
|
302
|
-
if (r.status === "fulfilled" && r.value) {
|
|
303
|
-
const { slot, url } = r.value;
|
|
304
|
-
const replacement = slot.replaceStr.replace("{url}", url);
|
|
305
|
-
html = html.replaceAll(slot.searchStr, replacement);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (html !== sectionRef.html) {
|
|
309
|
-
sectionRef.html = html;
|
|
310
|
-
onImageUpdate?.(sectionRef.id, html);
|
|
311
|
-
}
|
|
312
|
-
})()
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
await Promise.allSettled(imagePromises);
|
|
318
|
-
for (const section of allSections) {
|
|
319
|
-
const before = section.html;
|
|
320
|
-
section.html = section.html.replace(
|
|
321
|
-
/<img\s(?![^>]*\bsrc=)([^>]*?)>/gi,
|
|
322
|
-
(_match, attrs) => {
|
|
323
|
-
const altMatch = attrs.match(/alt="([^"]*?)"/);
|
|
324
|
-
const query = altMatch?.[1] || "image";
|
|
325
|
-
return `<img src="https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}" ${attrs}>`;
|
|
326
|
-
}
|
|
327
|
-
);
|
|
328
|
-
section.html = section.html.replace(
|
|
329
|
-
/data-image-query="([^"]+)"/g,
|
|
330
|
-
(_match, query) => {
|
|
331
|
-
return `src="https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}" data-enriched="placeholder"`;
|
|
332
|
-
}
|
|
333
|
-
);
|
|
334
|
-
if (section.html !== before) {
|
|
335
|
-
onImageUpdate?.(section.id, section.html);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
onDone?.(allSections);
|
|
339
|
-
return allSections;
|
|
340
|
-
} catch (err) {
|
|
341
|
-
const error = err instanceof Error ? err : new Error(err?.message || "Generation failed");
|
|
342
|
-
onError?.(error);
|
|
343
|
-
throw error;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export {
|
|
348
|
-
SYSTEM_PROMPT,
|
|
349
|
-
PROMPT_SUFFIX,
|
|
350
|
-
extractJsonObjects,
|
|
351
|
-
generateLanding
|
|
352
|
-
};
|
|
353
|
-
//# sourceMappingURL=chunk-MZ57RXKT.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 content.push({ type: \"image\", image: referenceImage });\n content.push({\n type: \"text\",\n text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n } else {\n content.push({\n type: \"text\",\n text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n }\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: [{ role: \"user\", content }],\n });\n\n const allSections: Section3[] = [];\n const imagePromises: Promise<void>[] = [];\n let sectionOrder = 0;\n let buffer = \"\";\n\n try {\n for await (const chunk of result.textStream) {\n buffer += chunk;\n\n const [objects, remaining] = extractJsonObjects(buffer);\n buffer = remaining;\n\n for (const obj of objects) {\n if (!obj.html || !obj.label) continue;\n\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n\n // Add loading placeholders so images don't show as broken while DALL-E generates\n section.html = addLoadingPlaceholders(section.html);\n allSections.push(section);\n onSection?.(section);\n\n // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)\n const slots = findImageSlots(section.html);\n if (slots.length > 0) {\n const sectionRef = section;\n const slotsSnapshot = slots.map((s) => ({ ...s }));\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n slotsSnapshot.map(async (slot) => {\n let url: string | null = null;\n // 1. DALL-E if openaiApiKey provided\n if (openaiApiKey) {\n try {\n const tempUrl = await generateImage(slot.query, openaiApiKey);\n url = persistImage ? await persistImage(tempUrl, slot.query) : tempUrl;\n } catch (e) {\n console.warn(`[dalle] failed for \"${slot.query}\":`, e);\n }\n }\n // 2. Pexels fallback\n if (!url) {\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n }\n // 3. Placeholder fallback\n url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;\n return { slot, url };\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n const { slot, url } = r.value;\n const replacement = slot.replaceStr.replace(\"{url}\", url);\n html = html.replaceAll(slot.searchStr, replacement);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\n }\n }\n\n // Parse remaining buffer\n if (buffer.trim()) {\n let cleaned = buffer.trim();\n if (cleaned.startsWith(\"```\")) {\n cleaned = cleaned\n .replace(/^```(?:json)?\\s*/, \"\")\n .replace(/\\s*```$/, \"\");\n }\n const [lastObjects] = extractJsonObjects(cleaned);\n for (const obj of lastObjects) {\n if (!obj.html || !obj.label) continue;\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n // Add loading placeholders so images don't show as broken while DALL-E generates\n section.html = addLoadingPlaceholders(section.html);\n allSections.push(section);\n onSection?.(section);\n\n // Enrich images for remaining-buffer sections too\n const slots = findImageSlots(section.html);\n if (slots.length > 0) {\n const sectionRef = section;\n const slotsSnapshot = slots.map((s) => ({ ...s }));\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n slotsSnapshot.map(async (slot) => {\n let url: string | null = null;\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;\n return { slot, url };\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n const { slot, url } = r.value;\n const replacement = slot.replaceStr.replace(\"{url}\", url);\n html = html.replaceAll(slot.searchStr, replacement);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\n }\n }\n\n // Wait for image enrichment\n await Promise.allSettled(imagePromises);\n\n // Final fallback: any <img> still without src gets a placeholder\n for (const section of allSections) {\n const before = section.html;\n section.html = section.html.replace(\n /<img\\s(?![^>]*\\bsrc=)([^>]*?)>/gi,\n (_match, attrs) => {\n const altMatch = attrs.match(/alt=\"([^\"]*?)\"/);\n const query = altMatch?.[1] || \"image\";\n return `<img src=\"https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}\" ${attrs}>`;\n }\n );\n // Also replace any remaining data-image-query that wasn't enriched\n section.html = section.html.replace(\n /data-image-query=\"([^\"]+)\"/g,\n (_match, query) => {\n return `src=\"https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}\" data-enriched=\"placeholder\"`;\n }\n );\n if (section.html !== before) {\n onImageUpdate?.(section.id, section.html);\n }\n }\n\n onDone?.(allSections);\n return allSections;\n } catch (err: any) {\n const error = err instanceof Error ? err : new Error(err?.message || \"Generation failed\");\n onError?.(error);\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
|