@easybits.cloud/html-tailwind-generator 0.1.5 → 0.2.0
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/{src/images/enrichImages.ts → dist/chunk-5HSVOF2J.js} +65 -53
- package/dist/chunk-5HSVOF2J.js.map +1 -0
- package/{src/iframeScript.ts → dist/chunk-5TYGSZAF.js} +259 -10
- package/dist/chunk-5TYGSZAF.js.map +1 -0
- package/{src/refine.ts → dist/chunk-GMJR2GXL.js} +30 -60
- package/dist/chunk-GMJR2GXL.js.map +1 -0
- package/dist/chunk-LQ65H4AO.js +41 -0
- package/dist/chunk-LQ65H4AO.js.map +1 -0
- package/{src/generate.ts → dist/chunk-PK26CWDO.js} +67 -108
- package/dist/chunk-PK26CWDO.js.map +1 -0
- package/dist/chunk-RTGCZUNJ.js +1 -0
- package/dist/chunk-RTGCZUNJ.js.map +1 -0
- package/dist/chunk-XM3D3TTJ.js +852 -0
- package/dist/chunk-XM3D3TTJ.js.map +1 -0
- package/dist/components.d.ts +57 -0
- package/dist/components.js +14 -0
- package/dist/components.js.map +1 -0
- package/dist/deploy.d.ts +39 -0
- package/dist/deploy.js +10 -0
- package/dist/deploy.js.map +1 -0
- package/dist/generate.d.ts +41 -0
- package/dist/generate.js +14 -0
- package/dist/generate.js.map +1 -0
- package/dist/images.d.ts +30 -0
- package/dist/images.js +14 -0
- package/dist/images.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/refine.d.ts +32 -0
- package/dist/refine.js +10 -0
- package/dist/refine.js.map +1 -0
- package/dist/themes-DOoj19c8.d.ts +35 -0
- package/dist/types-Flpl4wDs.d.ts +31 -0
- package/package.json +53 -50
- package/src/buildHtml.ts +0 -78
- package/src/components/Canvas.tsx +0 -162
- package/src/components/CodeEditor.tsx +0 -239
- package/src/components/FloatingToolbar.tsx +0 -350
- package/src/components/SectionList.tsx +0 -217
- package/src/components/index.ts +0 -4
- package/src/deploy.ts +0 -73
- package/src/images/dalleImages.ts +0 -29
- package/src/images/index.ts +0 -3
- package/src/images/pexels.ts +0 -27
- package/src/index.ts +0 -58
- package/src/themes.ts +0 -204
- package/src/types.ts +0 -30
|
@@ -1,71 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
enrichImages
|
|
3
|
+
} from "./chunk-5HSVOF2J.js";
|
|
4
|
+
|
|
5
|
+
// src/refine.ts
|
|
1
6
|
import { streamText } from "ai";
|
|
2
7
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
async function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {
|
|
8
|
+
async function resolveModel(opts) {
|
|
6
9
|
const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
7
10
|
if (openaiKey) {
|
|
8
11
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
9
12
|
const openai = createOpenAI({ apiKey: openaiKey });
|
|
10
13
|
return openai(opts.modelId || opts.defaultOpenai);
|
|
11
14
|
}
|
|
12
|
-
const anthropic = opts.anthropicApiKey
|
|
13
|
-
? createAnthropic({ apiKey: opts.anthropicApiKey })
|
|
14
|
-
: createAnthropic();
|
|
15
|
+
const anthropic = opts.anthropicApiKey ? createAnthropic({ apiKey: opts.anthropicApiKey }) : createAnthropic();
|
|
15
16
|
return anthropic(opts.modelId || opts.defaultAnthropic);
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
export const REFINE_SYSTEM = `You are an expert HTML/Tailwind CSS developer. You receive the current HTML of a landing page section and a user instruction.
|
|
18
|
+
var REFINE_SYSTEM = `You are an expert HTML/Tailwind CSS developer. You receive the current HTML of a landing page section and a user instruction.
|
|
19
19
|
|
|
20
20
|
RULES:
|
|
21
|
-
- Return ONLY the modified HTML
|
|
21
|
+
- Return ONLY the modified HTML \u2014 no full page, no <html>/<head>/<body> tags
|
|
22
22
|
- Use Tailwind CSS classes (CDN loaded)
|
|
23
23
|
- You may use inline styles for specific adjustments
|
|
24
24
|
- Images: use data-image-query="english search query" for new images
|
|
25
25
|
- Keep all text in its original language unless asked to translate
|
|
26
|
-
- Be creative
|
|
27
|
-
- Return raw HTML only
|
|
26
|
+
- Be creative \u2014 don't just make minimal changes, improve the design
|
|
27
|
+
- Return raw HTML only \u2014 no markdown fences, no explanations
|
|
28
28
|
|
|
29
|
-
COLOR SYSTEM
|
|
29
|
+
COLOR SYSTEM \u2014 CRITICAL:
|
|
30
30
|
- 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
|
|
31
31
|
- NEVER use hardcoded colors: NO bg-gray-*, bg-black, bg-white, text-gray-*, text-black, text-white, etc.
|
|
32
32
|
- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.
|
|
33
|
-
- CONTRAST RULE: on bg-primary/bg-primary-dark
|
|
33
|
+
- CONTRAST RULE: on bg-primary/bg-primary-dark \u2192 text-on-primary. On bg-surface/bg-surface-alt \u2192 text-on-surface/text-on-surface-muted. Never mismatch.
|
|
34
34
|
|
|
35
35
|
TAILWIND v3 NOTES:
|
|
36
36
|
- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
|
|
37
37
|
- Borders: border + border-gray-200 for visible borders`;
|
|
38
|
-
|
|
39
|
-
export interface RefineOptions {
|
|
40
|
-
/** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
|
|
41
|
-
anthropicApiKey?: string;
|
|
42
|
-
/** OpenAI API key. If provided, uses GPT-4o-mini instead of Claude */
|
|
43
|
-
openaiApiKey?: string;
|
|
44
|
-
/** Current HTML of the section being refined */
|
|
45
|
-
currentHtml: string;
|
|
46
|
-
/** User instruction for refinement */
|
|
47
|
-
instruction: string;
|
|
48
|
-
/** Reference image (base64 data URI) for vision-based refinement */
|
|
49
|
-
referenceImage?: string;
|
|
50
|
-
/** Custom system prompt (overrides default REFINE_SYSTEM) */
|
|
51
|
-
systemPrompt?: string;
|
|
52
|
-
/** Model ID (default: gpt-4o-mini/gpt-4o for OpenAI, claude-haiku/claude-sonnet for Anthropic) */
|
|
53
|
-
model?: string;
|
|
54
|
-
/** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
|
|
55
|
-
pexelsApiKey?: string;
|
|
56
|
-
/** Called with accumulated HTML as it streams */
|
|
57
|
-
onChunk?: (html: string) => void;
|
|
58
|
-
/** Called when refinement is complete with final enriched HTML */
|
|
59
|
-
onDone?: (html: string) => void;
|
|
60
|
-
/** Called on error */
|
|
61
|
-
onError?: (error: Error) => void;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Refine a landing page section with streaming AI.
|
|
66
|
-
* Returns the final enriched HTML.
|
|
67
|
-
*/
|
|
68
|
-
export async function refineLanding(options: RefineOptions): Promise<string> {
|
|
38
|
+
async function refineLanding(options) {
|
|
69
39
|
const {
|
|
70
40
|
anthropicApiKey,
|
|
71
41
|
openaiApiKey: _openaiApiKey,
|
|
@@ -77,52 +47,52 @@ export async function refineLanding(options: RefineOptions): Promise<string> {
|
|
|
77
47
|
pexelsApiKey,
|
|
78
48
|
onChunk,
|
|
79
49
|
onDone,
|
|
80
|
-
onError
|
|
50
|
+
onError
|
|
81
51
|
} = options;
|
|
82
|
-
|
|
83
52
|
const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;
|
|
84
53
|
const defaultOpenai = referenceImage ? "gpt-4o" : "gpt-4o-mini";
|
|
85
54
|
const defaultAnthropic = referenceImage ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001";
|
|
86
55
|
const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai, defaultAnthropic });
|
|
87
|
-
|
|
88
|
-
// Build content (supports multimodal with reference image)
|
|
89
|
-
const content: any[] = [];
|
|
56
|
+
const content = [];
|
|
90
57
|
if (referenceImage) {
|
|
91
58
|
content.push({ type: "image", image: referenceImage });
|
|
92
59
|
}
|
|
93
60
|
content.push({
|
|
94
61
|
type: "text",
|
|
95
|
-
text: `Current HTML
|
|
96
|
-
|
|
62
|
+
text: `Current HTML:
|
|
63
|
+
${currentHtml}
|
|
97
64
|
|
|
65
|
+
Instruction: ${instruction}
|
|
66
|
+
|
|
67
|
+
Return the updated HTML.`
|
|
68
|
+
});
|
|
98
69
|
const result = streamText({
|
|
99
70
|
model,
|
|
100
71
|
system: systemPrompt,
|
|
101
|
-
messages: [{ role: "user", content }]
|
|
72
|
+
messages: [{ role: "user", content }]
|
|
102
73
|
});
|
|
103
|
-
|
|
104
74
|
try {
|
|
105
75
|
let accumulated = "";
|
|
106
|
-
|
|
107
76
|
for await (const chunk of result.textStream) {
|
|
108
77
|
accumulated += chunk;
|
|
109
78
|
onChunk?.(accumulated);
|
|
110
79
|
}
|
|
111
|
-
|
|
112
|
-
// Clean up markdown fences if present
|
|
113
80
|
let html = accumulated.trim();
|
|
114
81
|
if (html.startsWith("```")) {
|
|
115
82
|
html = html.replace(/^```(?:html|xml)?\s*/, "").replace(/\s*```$/, "");
|
|
116
83
|
}
|
|
117
|
-
|
|
118
|
-
// Enrich images (DALL-E if openaiApiKey, otherwise Pexels)
|
|
119
84
|
html = await enrichImages(html, pexelsApiKey, openaiApiKey);
|
|
120
|
-
|
|
121
85
|
onDone?.(html);
|
|
122
86
|
return html;
|
|
123
|
-
} catch (err
|
|
87
|
+
} catch (err) {
|
|
124
88
|
const error = err instanceof Error ? err : new Error(err?.message || "Refine failed");
|
|
125
89
|
onError?.(error);
|
|
126
90
|
throw error;
|
|
127
91
|
}
|
|
128
92
|
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
REFINE_SYSTEM,
|
|
96
|
+
refineLanding
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=chunk-GMJR2GXL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/refine.ts"],"sourcesContent":["import { streamText } from \"ai\";\nimport { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { enrichImages } from \"./images/enrichImages\";\n\nasync function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {\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 const anthropic = opts.anthropicApiKey\n ? createAnthropic({ apiKey: opts.anthropicApiKey })\n : createAnthropic();\n return anthropic(opts.modelId || opts.defaultAnthropic);\n}\n\nexport const REFINE_SYSTEM = `You are an expert HTML/Tailwind CSS developer. You receive the current HTML of a landing page section and a user instruction.\n\nRULES:\n- Return ONLY the modified HTML — no full page, no <html>/<head>/<body> tags\n- Use Tailwind CSS classes (CDN loaded)\n- You may use inline styles for specific adjustments\n- Images: use data-image-query=\"english search query\" for new images\n- Keep all text in its original language unless asked to translate\n- Be creative — don't just make minimal changes, improve the design\n- Return raw HTML only — no markdown fences, no explanations\n\nCOLOR SYSTEM — CRITICAL:\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 colors: NO bg-gray-*, bg-black, bg-white, text-gray-*, text-black, text-white, etc.\n- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.\n- CONTRAST RULE: on bg-primary/bg-primary-dark → text-on-primary. On bg-surface/bg-surface-alt → text-on-surface/text-on-surface-muted. Never mismatch.\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 interface RefineOptions {\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */\n anthropicApiKey?: string;\n /** OpenAI API key. If provided, uses GPT-4o-mini instead of Claude */\n openaiApiKey?: string;\n /** Current HTML of the section being refined */\n currentHtml: string;\n /** User instruction for refinement */\n instruction: string;\n /** Reference image (base64 data URI) for vision-based refinement */\n referenceImage?: string;\n /** Custom system prompt (overrides default REFINE_SYSTEM) */\n systemPrompt?: string;\n /** Model ID (default: gpt-4o-mini/gpt-4o for OpenAI, claude-haiku/claude-sonnet 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 accumulated HTML as it streams */\n onChunk?: (html: string) => void;\n /** Called when refinement is complete with final enriched HTML */\n onDone?: (html: string) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n}\n\n/**\n * Refine a landing page section with streaming AI.\n * Returns the final enriched HTML.\n */\nexport async function refineLanding(options: RefineOptions): Promise<string> {\n const {\n anthropicApiKey,\n openaiApiKey: _openaiApiKey,\n currentHtml,\n instruction,\n referenceImage,\n systemPrompt = REFINE_SYSTEM,\n model: modelId,\n pexelsApiKey,\n onChunk,\n onDone,\n onError,\n } = options;\n\n const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;\n const defaultOpenai = referenceImage ? \"gpt-4o\" : \"gpt-4o-mini\";\n const defaultAnthropic = referenceImage ? \"claude-sonnet-4-6\" : \"claude-haiku-4-5-20251001\";\n const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai, defaultAnthropic });\n\n // Build content (supports multimodal with reference image)\n const content: any[] = [];\n if (referenceImage) {\n content.push({ type: \"image\", image: referenceImage });\n }\n content.push({\n type: \"text\",\n text: `Current HTML:\\n${currentHtml}\\n\\nInstruction: ${instruction}\\n\\nReturn the updated HTML.`,\n });\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: [{ role: \"user\", content }],\n });\n\n try {\n let accumulated = \"\";\n\n for await (const chunk of result.textStream) {\n accumulated += chunk;\n onChunk?.(accumulated);\n }\n\n // Clean up markdown fences if present\n let html = accumulated.trim();\n if (html.startsWith(\"```\")) {\n html = html.replace(/^```(?:html|xml)?\\s*/, \"\").replace(/\\s*```$/, \"\");\n }\n\n // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)\n html = await enrichImages(html, pexelsApiKey, openaiApiKey);\n\n onDone?.(html);\n return html;\n } catch (err: any) {\n const error = err instanceof Error ? err : new Error(err?.message || \"Refine failed\");\n onError?.(error);\n throw error;\n }\n}\n"],"mappings":";;;;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAGhC,eAAe,aAAa,MAA8H;AACxJ,QAAM,YAAY,KAAK,gBAAgB,QAAQ,IAAI;AACnD,MAAI,WAAW;AACb,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,gBAAgB;AACtD,UAAM,SAAS,aAAa,EAAE,QAAQ,UAAU,CAAC;AACjD,WAAO,OAAO,KAAK,WAAW,KAAK,aAAa;AAAA,EAClD;AACA,QAAM,YAAY,KAAK,kBACnB,gBAAgB,EAAE,QAAQ,KAAK,gBAAgB,CAAC,IAChD,gBAAgB;AACpB,SAAO,UAAU,KAAK,WAAW,KAAK,gBAAgB;AACxD;AAEO,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkD7B,eAAsB,cAAc,SAAyC;AAC3E,QAAM;AAAA,IACJ;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,eAAe,iBAAiB,QAAQ,IAAI;AAClD,QAAM,gBAAgB,iBAAiB,WAAW;AAClD,QAAM,mBAAmB,iBAAiB,sBAAsB;AAChE,QAAM,QAAQ,MAAM,aAAa,EAAE,cAAc,iBAAiB,SAAS,eAAe,iBAAiB,CAAC;AAG5G,QAAM,UAAiB,CAAC;AACxB,MAAI,gBAAgB;AAClB,YAAQ,KAAK,EAAE,MAAM,SAAS,OAAO,eAAe,CAAC;AAAA,EACvD;AACA,UAAQ,KAAK;AAAA,IACX,MAAM;AAAA,IACN,MAAM;AAAA,EAAkB,WAAW;AAAA;AAAA,eAAoB,WAAW;AAAA;AAAA;AAAA,EACpE,CAAC;AAED,QAAM,SAAS,WAAW;AAAA,IACxB;AAAA,IACA,QAAQ;AAAA,IACR,UAAU,CAAC,EAAE,MAAM,QAAQ,QAAQ,CAAC;AAAA,EACtC,CAAC;AAED,MAAI;AACF,QAAI,cAAc;AAElB,qBAAiB,SAAS,OAAO,YAAY;AAC3C,qBAAe;AACf,gBAAU,WAAW;AAAA,IACvB;AAGA,QAAI,OAAO,YAAY,KAAK;AAC5B,QAAI,KAAK,WAAW,KAAK,GAAG;AAC1B,aAAO,KAAK,QAAQ,wBAAwB,EAAE,EAAE,QAAQ,WAAW,EAAE;AAAA,IACvE;AAGA,WAAO,MAAM,aAAa,MAAM,cAAc,YAAY;AAE1D,aAAS,IAAI;AACb,WAAO;AAAA,EACT,SAAS,KAAU;AACjB,UAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,KAAK,WAAW,eAAe;AACpF,cAAU,KAAK;AACf,UAAM;AAAA,EACR;AACF;","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDeployHtml
|
|
3
|
+
} from "./chunk-5TYGSZAF.js";
|
|
4
|
+
|
|
5
|
+
// src/deploy.ts
|
|
6
|
+
async function deployToS3(options) {
|
|
7
|
+
const { sections, theme, customColors, upload } = options;
|
|
8
|
+
const html = buildDeployHtml(sections, theme, customColors);
|
|
9
|
+
return upload(html);
|
|
10
|
+
}
|
|
11
|
+
async function deployToEasyBits(options) {
|
|
12
|
+
const {
|
|
13
|
+
apiKey,
|
|
14
|
+
slug,
|
|
15
|
+
sections,
|
|
16
|
+
theme,
|
|
17
|
+
customColors,
|
|
18
|
+
baseUrl = "https://easybits.cloud"
|
|
19
|
+
} = options;
|
|
20
|
+
const html = buildDeployHtml(sections, theme, customColors);
|
|
21
|
+
const res = await fetch(`${baseUrl}/api/v2/websites`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
Authorization: `Bearer ${apiKey}`
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ slug, html })
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
const error = await res.json().catch(() => ({ error: "Deploy failed" }));
|
|
31
|
+
throw new Error(error.error || "Deploy failed");
|
|
32
|
+
}
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
return data.url || `https://${slug}.easybits.cloud`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
deployToS3,
|
|
39
|
+
deployToEasyBits
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=chunk-LQ65H4AO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/deploy.ts"],"sourcesContent":["import { buildDeployHtml } from \"./buildHtml\";\nimport type { Section3 } from \"./types\";\nimport type { CustomColors } from \"./themes\";\n\nexport interface DeployToS3Options {\n /** The sections to deploy */\n sections: Section3[];\n /** Theme ID */\n theme?: string;\n /** Custom colors (when theme is \"custom\") */\n customColors?: CustomColors;\n /** S3-compatible upload function. Receives the HTML string, returns the URL */\n upload: (html: string) => Promise<string>;\n}\n\n/**\n * Deploy a landing page to any S3-compatible storage.\n * The consumer provides their own upload function.\n */\nexport async function deployToS3(options: DeployToS3Options): Promise<string> {\n const { sections, theme, customColors, upload } = options;\n const html = buildDeployHtml(sections, theme, customColors);\n return upload(html);\n}\n\nexport interface DeployToEasyBitsOptions {\n /** EasyBits API key */\n apiKey: string;\n /** Website slug (e.g. \"my-landing\" → my-landing.easybits.cloud) */\n slug: string;\n /** The sections to deploy */\n sections: Section3[];\n /** Theme ID */\n theme?: string;\n /** Custom colors (when theme is \"custom\") */\n customColors?: CustomColors;\n /** EasyBits API base URL (default: https://easybits.cloud) */\n baseUrl?: string;\n}\n\n/**\n * Deploy a landing page to EasyBits hosting (slug.easybits.cloud).\n * Uses the EasyBits API to create/update a website.\n */\nexport async function deployToEasyBits(options: DeployToEasyBitsOptions): Promise<string> {\n const {\n apiKey,\n slug,\n sections,\n theme,\n customColors,\n baseUrl = \"https://easybits.cloud\",\n } = options;\n\n const html = buildDeployHtml(sections, theme, customColors);\n\n const res = await fetch(`${baseUrl}/api/v2/websites`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ slug, html }),\n });\n\n if (!res.ok) {\n const error = await res.json().catch(() => ({ error: \"Deploy failed\" }));\n throw new Error(error.error || \"Deploy failed\");\n }\n\n const data = await res.json();\n return data.url || `https://${slug}.easybits.cloud`;\n}\n"],"mappings":";;;;;AAmBA,eAAsB,WAAW,SAA6C;AAC5E,QAAM,EAAE,UAAU,OAAO,cAAc,OAAO,IAAI;AAClD,QAAM,OAAO,gBAAgB,UAAU,OAAO,YAAY;AAC1D,SAAO,OAAO,IAAI;AACpB;AAqBA,eAAsB,iBAAiB,SAAmD;AACxF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,EACZ,IAAI;AAEJ,QAAM,OAAO,gBAAgB,UAAU,OAAO,YAAY;AAE1D,QAAM,MAAM,MAAM,MAAM,GAAG,OAAO,oBAAoB;AAAA,IACpD,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,MAAM;AAAA,IACjC;AAAA,IACA,MAAM,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AAAA,EACrC,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,QAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,EAAE,OAAO,gBAAgB,EAAE;AACvE,UAAM,IAAI,MAAM,MAAM,SAAS,eAAe;AAAA,EAChD;AAEA,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,SAAO,KAAK,OAAO,WAAW,IAAI;AACpC;","names":[]}
|
|
@@ -1,25 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findImageSlots,
|
|
3
|
+
generateImage,
|
|
4
|
+
searchImage
|
|
5
|
+
} from "./chunk-5HSVOF2J.js";
|
|
6
|
+
|
|
7
|
+
// src/generate.ts
|
|
1
8
|
import { streamText } from "ai";
|
|
2
9
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
10
|
import { nanoid } from "nanoid";
|
|
4
|
-
|
|
5
|
-
import { searchImage } from "./images/pexels";
|
|
6
|
-
import { generateImage } from "./images/dalleImages";
|
|
7
|
-
import type { Section3 } from "./types";
|
|
8
|
-
|
|
9
|
-
async function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {
|
|
11
|
+
async function resolveModel(opts) {
|
|
10
12
|
const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
11
13
|
if (openaiKey) {
|
|
12
14
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
13
15
|
const openai = createOpenAI({ apiKey: openaiKey });
|
|
14
16
|
return openai(opts.modelId || opts.defaultOpenai);
|
|
15
17
|
}
|
|
16
|
-
const anthropic = opts.anthropicApiKey
|
|
17
|
-
? createAnthropic({ apiKey: opts.anthropicApiKey })
|
|
18
|
-
: createAnthropic();
|
|
18
|
+
const anthropic = opts.anthropicApiKey ? createAnthropic({ apiKey: opts.anthropicApiKey }) : createAnthropic();
|
|
19
19
|
return anthropic(opts.modelId || opts.defaultAnthropic);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
export 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.
|
|
21
|
+
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.
|
|
23
22
|
|
|
24
23
|
RULES:
|
|
25
24
|
- Each section is a complete <section> tag with Tailwind CSS classes
|
|
@@ -28,24 +27,24 @@ RULES:
|
|
|
28
27
|
- Each section must be independent and self-contained
|
|
29
28
|
- Responsive: mobile-first with sm/md/lg/xl breakpoints
|
|
30
29
|
- All text content in Spanish unless the prompt specifies otherwise
|
|
31
|
-
- Use real-looking content (not Lorem ipsum)
|
|
30
|
+
- Use real-looking content (not Lorem ipsum) \u2014 make it specific to the prompt
|
|
32
31
|
|
|
33
|
-
IMAGES
|
|
32
|
+
IMAGES \u2014 CRITICAL:
|
|
34
33
|
- Use <img data-image-query="english search query" alt="description" class="..."/>
|
|
35
|
-
- NEVER include a src attribute
|
|
34
|
+
- NEVER include a src attribute \u2014 the system auto-replaces data-image-query with a real image URL
|
|
36
35
|
- 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>)
|
|
37
36
|
|
|
38
|
-
COLOR SYSTEM
|
|
37
|
+
COLOR SYSTEM \u2014 CRITICAL (READ CAREFULLY):
|
|
39
38
|
- 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
|
|
40
39
|
- 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.
|
|
41
40
|
- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.
|
|
42
41
|
- ALL backgrounds MUST use: bg-primary, bg-primary-dark, bg-surface, bg-surface-alt
|
|
43
42
|
- ALL text MUST use: text-on-surface, text-on-surface-muted, text-on-primary, text-primary, text-accent
|
|
44
|
-
- CONTRAST RULE: on bg-primary or bg-primary-dark
|
|
43
|
+
- CONTRAST RULE: on bg-primary or bg-primary-dark \u2192 use text-on-primary. On bg-surface or bg-surface-alt \u2192 use text-on-surface or text-on-surface-muted. NEVER put text-on-surface on bg-primary or vice versa. text-accent is decorative \u2014 use sparingly on bg-surface/bg-surface-alt only.
|
|
45
44
|
- For gradients: from-primary to-primary-dark, from-surface to-surface-alt
|
|
46
45
|
- For hover: hover:bg-primary-dark, hover:bg-primary-light
|
|
47
46
|
|
|
48
|
-
DESIGN PHILOSOPHY
|
|
47
|
+
DESIGN PHILOSOPHY \u2014 what separates good from GREAT:
|
|
49
48
|
- WHITESPACE is your best friend. Generous padding (py-24, py-32, px-8). Let elements breathe.
|
|
50
49
|
- CONTRAST: mix dark sections with light ones. Alternate bg-primary and bg-surface sections.
|
|
51
50
|
- TYPOGRAPHY: use extreme size differences for hierarchy (text-7xl headline next to text-sm label)
|
|
@@ -54,37 +53,31 @@ DESIGN PHILOSOPHY — what separates good from GREAT:
|
|
|
54
53
|
- TEXTURE: use subtle patterns, gradients, border treatments, rounded-3xl mixed with sharp edges
|
|
55
54
|
- Each section should have a COMPLETELY DIFFERENT layout from the others
|
|
56
55
|
|
|
57
|
-
HERO SECTION
|
|
56
|
+
HERO SECTION \u2014 your masterpiece:
|
|
58
57
|
- Bento-grid or asymmetric layout, NOT a generic centered hero
|
|
59
58
|
- Large headline block + smaller stat/metric cards in a grid
|
|
60
59
|
- Real social proof: "2,847+ users", avatar stack (colored divs with initials), star ratings
|
|
61
60
|
- Bold oversized headline (text-6xl/7xl font-black leading-none)
|
|
62
61
|
- Tag/label above headline (uppercase, tracking-wider, text-xs)
|
|
63
|
-
- 2 CTAs: primary (large, with
|
|
62
|
+
- 2 CTAs: primary (large, with \u2192 arrow) + secondary (ghost/outlined)
|
|
64
63
|
- Real image via data-image-query
|
|
65
64
|
- Min height: min-h-[90vh] with generous padding
|
|
66
65
|
|
|
67
66
|
TAILWIND v3 NOTES:
|
|
68
67
|
- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
|
|
69
68
|
- Borders: border + border-gray-200 for visible borders`;
|
|
69
|
+
var PROMPT_SUFFIX = `
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
OUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.
|
|
71
|
+
OUTPUT FORMAT: NDJSON \u2014 one JSON object per line, NO wrapper array, NO markdown fences.
|
|
74
72
|
Each line: {"label": "Short Label", "html": "<section>...</section>"}
|
|
75
73
|
|
|
76
74
|
Generate 7-9 sections. Always start with Hero and end with Footer.
|
|
77
|
-
IMPORTANT: Make each section VISUALLY UNIQUE
|
|
75
|
+
IMPORTANT: Make each section VISUALLY UNIQUE \u2014 different layouts, different background colors, different grid structures.
|
|
78
76
|
Think like a premium design agency creating a $50K landing page.
|
|
79
77
|
NO generic Bootstrap layouts. Use creative grids, bento layouts, overlapping elements, asymmetric columns.`;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* Extract complete JSON objects from accumulated text using brace-depth tracking.
|
|
83
|
-
*/
|
|
84
|
-
export function extractJsonObjects(text: string): [any[], string] {
|
|
85
|
-
const objects: any[] = [];
|
|
78
|
+
function extractJsonObjects(text) {
|
|
79
|
+
const objects = [];
|
|
86
80
|
let remaining = text;
|
|
87
|
-
|
|
88
81
|
while (remaining.length > 0) {
|
|
89
82
|
remaining = remaining.trimStart();
|
|
90
83
|
if (!remaining.startsWith("{")) {
|
|
@@ -93,69 +86,45 @@ export function extractJsonObjects(text: string): [any[], string] {
|
|
|
93
86
|
remaining = remaining.slice(nextBrace);
|
|
94
87
|
continue;
|
|
95
88
|
}
|
|
96
|
-
|
|
97
89
|
let depth = 0;
|
|
98
90
|
let inString = false;
|
|
99
91
|
let escape = false;
|
|
100
92
|
let end = -1;
|
|
101
|
-
|
|
102
93
|
for (let i = 0; i < remaining.length; i++) {
|
|
103
94
|
const ch = remaining[i];
|
|
104
|
-
if (escape) {
|
|
105
|
-
|
|
106
|
-
|
|
95
|
+
if (escape) {
|
|
96
|
+
escape = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === "\\") {
|
|
100
|
+
escape = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '"') {
|
|
104
|
+
inString = !inString;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
107
|
if (inString) continue;
|
|
108
108
|
if (ch === "{") depth++;
|
|
109
|
-
if (ch === "}") {
|
|
109
|
+
if (ch === "}") {
|
|
110
|
+
depth--;
|
|
111
|
+
if (depth === 0) {
|
|
112
|
+
end = i;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
110
116
|
}
|
|
111
|
-
|
|
112
117
|
if (end === -1) break;
|
|
113
|
-
|
|
114
118
|
const candidate = remaining.slice(0, end + 1);
|
|
115
119
|
remaining = remaining.slice(end + 1);
|
|
116
|
-
|
|
117
120
|
try {
|
|
118
121
|
objects.push(JSON.parse(candidate));
|
|
119
122
|
} catch {
|
|
120
|
-
// malformed, skip
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
|
-
|
|
124
125
|
return [objects, remaining];
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
-
export interface GenerateOptions {
|
|
128
|
-
/** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
|
|
129
|
-
anthropicApiKey?: string;
|
|
130
|
-
/** OpenAI API key. If provided, uses GPT-4o instead of Claude */
|
|
131
|
-
openaiApiKey?: string;
|
|
132
|
-
/** Landing page description prompt */
|
|
133
|
-
prompt: string;
|
|
134
|
-
/** Reference image (base64 data URI) for vision-based generation */
|
|
135
|
-
referenceImage?: string;
|
|
136
|
-
/** Extra instructions appended to the prompt */
|
|
137
|
-
extraInstructions?: string;
|
|
138
|
-
/** Custom system prompt (overrides default SYSTEM_PROMPT) */
|
|
139
|
-
systemPrompt?: string;
|
|
140
|
-
/** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */
|
|
141
|
-
model?: string;
|
|
142
|
-
/** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
|
|
143
|
-
pexelsApiKey?: string;
|
|
144
|
-
/** Called when a new section is parsed from the stream */
|
|
145
|
-
onSection?: (section: Section3) => void;
|
|
146
|
-
/** Called when a section's images are enriched */
|
|
147
|
-
onImageUpdate?: (sectionId: string, html: string) => void;
|
|
148
|
-
/** Called when generation is complete */
|
|
149
|
-
onDone?: (sections: Section3[]) => void;
|
|
150
|
-
/** Called on error */
|
|
151
|
-
onError?: (error: Error) => void;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Generate a landing page with streaming AI + image enrichment.
|
|
156
|
-
* Returns all generated sections when complete.
|
|
157
|
-
*/
|
|
158
|
-
export async function generateLanding(options: GenerateOptions): Promise<Section3[]> {
|
|
127
|
+
async function generateLanding(options) {
|
|
159
128
|
const {
|
|
160
129
|
anthropicApiKey,
|
|
161
130
|
openaiApiKey: _openaiApiKey,
|
|
@@ -168,60 +137,49 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
|
|
|
168
137
|
onSection,
|
|
169
138
|
onImageUpdate,
|
|
170
139
|
onDone,
|
|
171
|
-
onError
|
|
140
|
+
onError
|
|
172
141
|
} = options;
|
|
173
|
-
|
|
174
142
|
const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;
|
|
175
143
|
const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: "gpt-4o", defaultAnthropic: "claude-sonnet-4-6" });
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const content: any[] = [];
|
|
144
|
+
const extra = extraInstructions ? `
|
|
145
|
+
Additional instructions: ${extraInstructions}` : "";
|
|
146
|
+
const content = [];
|
|
180
147
|
if (referenceImage) {
|
|
181
148
|
content.push({ type: "image", image: referenceImage });
|
|
182
149
|
content.push({
|
|
183
150
|
type: "text",
|
|
184
|
-
text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}
|
|
151
|
+
text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`
|
|
185
152
|
});
|
|
186
153
|
} else {
|
|
187
154
|
content.push({
|
|
188
155
|
type: "text",
|
|
189
|
-
text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}
|
|
156
|
+
text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`
|
|
190
157
|
});
|
|
191
158
|
}
|
|
192
|
-
|
|
193
159
|
const result = streamText({
|
|
194
160
|
model,
|
|
195
161
|
system: systemPrompt,
|
|
196
|
-
messages: [{ role: "user", content }]
|
|
162
|
+
messages: [{ role: "user", content }]
|
|
197
163
|
});
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const imagePromises: Promise<void>[] = [];
|
|
164
|
+
const allSections = [];
|
|
165
|
+
const imagePromises = [];
|
|
201
166
|
let sectionOrder = 0;
|
|
202
167
|
let buffer = "";
|
|
203
|
-
|
|
204
168
|
try {
|
|
205
169
|
for await (const chunk of result.textStream) {
|
|
206
170
|
buffer += chunk;
|
|
207
|
-
|
|
208
171
|
const [objects, remaining] = extractJsonObjects(buffer);
|
|
209
172
|
buffer = remaining;
|
|
210
|
-
|
|
211
173
|
for (const obj of objects) {
|
|
212
174
|
if (!obj.html || !obj.label) continue;
|
|
213
|
-
|
|
214
|
-
const section: Section3 = {
|
|
175
|
+
const section = {
|
|
215
176
|
id: nanoid(8),
|
|
216
177
|
order: sectionOrder++,
|
|
217
178
|
html: obj.html,
|
|
218
|
-
label: obj.label
|
|
179
|
+
label: obj.label
|
|
219
180
|
};
|
|
220
|
-
|
|
221
181
|
allSections.push(section);
|
|
222
182
|
onSection?.(section);
|
|
223
|
-
|
|
224
|
-
// Enrich images (DALL-E if openaiApiKey, otherwise Pexels)
|
|
225
183
|
const slots = findImageSlots(section.html);
|
|
226
184
|
if (slots.length > 0) {
|
|
227
185
|
const sectionRef = section;
|
|
@@ -230,7 +188,7 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
|
|
|
230
188
|
(async () => {
|
|
231
189
|
const results = await Promise.allSettled(
|
|
232
190
|
slotsSnapshot.map(async (slot) => {
|
|
233
|
-
let url
|
|
191
|
+
let url = null;
|
|
234
192
|
if (openaiApiKey) {
|
|
235
193
|
url = await generateImage(slot.query, openaiApiKey).catch(() => null);
|
|
236
194
|
}
|
|
@@ -259,37 +217,38 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
|
|
|
259
217
|
}
|
|
260
218
|
}
|
|
261
219
|
}
|
|
262
|
-
|
|
263
|
-
// Parse remaining buffer
|
|
264
220
|
if (buffer.trim()) {
|
|
265
221
|
let cleaned = buffer.trim();
|
|
266
222
|
if (cleaned.startsWith("```")) {
|
|
267
|
-
cleaned = cleaned
|
|
268
|
-
.replace(/^```(?:json)?\s*/, "")
|
|
269
|
-
.replace(/\s*```$/, "");
|
|
223
|
+
cleaned = cleaned.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
|
|
270
224
|
}
|
|
271
225
|
const [lastObjects] = extractJsonObjects(cleaned);
|
|
272
226
|
for (const obj of lastObjects) {
|
|
273
227
|
if (!obj.html || !obj.label) continue;
|
|
274
|
-
const section
|
|
228
|
+
const section = {
|
|
275
229
|
id: nanoid(8),
|
|
276
230
|
order: sectionOrder++,
|
|
277
231
|
html: obj.html,
|
|
278
|
-
label: obj.label
|
|
232
|
+
label: obj.label
|
|
279
233
|
};
|
|
280
234
|
allSections.push(section);
|
|
281
235
|
onSection?.(section);
|
|
282
236
|
}
|
|
283
237
|
}
|
|
284
|
-
|
|
285
|
-
// Wait for image enrichment
|
|
286
238
|
await Promise.allSettled(imagePromises);
|
|
287
|
-
|
|
288
239
|
onDone?.(allSections);
|
|
289
240
|
return allSections;
|
|
290
|
-
} catch (err
|
|
241
|
+
} catch (err) {
|
|
291
242
|
const error = err instanceof Error ? err : new Error(err?.message || "Generation failed");
|
|
292
243
|
onError?.(error);
|
|
293
244
|
throw error;
|
|
294
245
|
}
|
|
295
246
|
}
|
|
247
|
+
|
|
248
|
+
export {
|
|
249
|
+
SYSTEM_PROMPT,
|
|
250
|
+
PROMPT_SUFFIX,
|
|
251
|
+
extractJsonObjects,
|
|
252
|
+
generateLanding
|
|
253
|
+
};
|
|
254
|
+
//# sourceMappingURL=chunk-PK26CWDO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generate.ts"],"sourcesContent":["import { streamText } from \"ai\";\nimport { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { nanoid } from \"nanoid\";\nimport { findImageSlots } 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 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 const anthropic = opts.anthropicApiKey\n ? createAnthropic({ apiKey: opts.anthropicApiKey })\n : createAnthropic();\n return anthropic(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\nIMAGES — CRITICAL:\n- Use <img data-image-query=\"english search query\" alt=\"description\" class=\"...\"/>\n- NEVER include a src attribute — the system auto-replaces data-image-query with a real image URL\n- For avatar-like elements, use colored divs with initials instead of img tags (e.g. <div class=\"w-10 h-10 rounded-full bg-primary flex items-center justify-center text-on-primary font-bold\">JD</div>)\n\nCOLOR SYSTEM — CRITICAL (READ CAREFULLY):\n- Use semantic color classes: bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary, bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted, bg-secondary, text-secondary, bg-accent, text-accent\n- NEVER use hardcoded Tailwind color classes: NO bg-gray-*, bg-black, bg-white, bg-indigo-*, bg-blue-*, bg-purple-*, text-gray-*, text-black, text-white, etc.\n- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.\n- ALL backgrounds MUST use: bg-primary, bg-primary-dark, bg-surface, bg-surface-alt\n- ALL text MUST use: text-on-surface, text-on-surface-muted, text-on-primary, text-primary, text-accent\n- CONTRAST RULE: on bg-primary or bg-primary-dark → use text-on-primary. On bg-surface or bg-surface-alt → use text-on-surface or text-on-surface-muted. NEVER put text-on-surface on bg-primary or vice versa. text-accent is decorative — use sparingly on bg-surface/bg-surface-alt only.\n- 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\nHERO SECTION — your masterpiece:\n- Bento-grid or asymmetric layout, NOT a generic centered hero\n- Large headline block + smaller stat/metric cards in a grid\n- Real social proof: \"2,847+ users\", avatar stack (colored divs with initials), star ratings\n- Bold oversized headline (text-6xl/7xl font-black leading-none)\n- Tag/label above headline (uppercase, tracking-wider, text-xs)\n- 2 CTAs: primary (large, with → arrow) + secondary (ghost/outlined)\n- Real image via data-image-query\n- Min height: min-h-[90vh] with generous padding\n\nTAILWIND v3 NOTES:\n- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)\n- Borders: border + border-gray-200 for visible borders`;\n\nexport const PROMPT_SUFFIX = `\n\nOUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Short Label\", \"html\": \"<section>...</section>\"}\n\nGenerate 7-9 sections. Always start with Hero and end with Footer.\nIMPORTANT: Make each section VISUALLY UNIQUE — different layouts, different background colors, different grid structures.\nThink like a premium design agency creating a $50K landing page.\nNO generic Bootstrap layouts. Use creative grids, bento layouts, overlapping elements, asymmetric columns.`;\n\n/**\n * Extract complete JSON objects from accumulated text using brace-depth tracking.\n */\nexport function extractJsonObjects(text: string): [any[], string] {\n const objects: any[] = [];\n let remaining = text;\n\n while (remaining.length > 0) {\n remaining = remaining.trimStart();\n if (!remaining.startsWith(\"{\")) {\n const nextBrace = remaining.indexOf(\"{\");\n if (nextBrace === -1) break;\n remaining = remaining.slice(nextBrace);\n continue;\n }\n\n let depth = 0;\n let inString = false;\n let escape = false;\n let end = -1;\n\n for (let i = 0; i < remaining.length; i++) {\n const ch = remaining[i];\n if (escape) { escape = false; continue; }\n if (ch === \"\\\\\") { escape = true; continue; }\n if (ch === '\"') { inString = !inString; continue; }\n if (inString) continue;\n if (ch === \"{\") depth++;\n if (ch === \"}\") { depth--; if (depth === 0) { end = i; break; } }\n }\n\n if (end === -1) break;\n\n const candidate = remaining.slice(0, end + 1);\n remaining = remaining.slice(end + 1);\n\n try {\n objects.push(JSON.parse(candidate));\n } catch {\n // malformed, skip\n }\n }\n\n return [objects, remaining];\n}\n\nexport interface GenerateOptions {\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */\n anthropicApiKey?: string;\n /** OpenAI API key. If provided, uses GPT-4o instead of Claude */\n openaiApiKey?: string;\n /** Landing page description prompt */\n prompt: string;\n /** Reference image (base64 data URI) for vision-based generation */\n referenceImage?: string;\n /** Extra instructions appended to the prompt */\n extraInstructions?: string;\n /** Custom system prompt (overrides default SYSTEM_PROMPT) */\n systemPrompt?: string;\n /** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */\n model?: string;\n /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */\n pexelsApiKey?: string;\n /** Called 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 onSection,\n onImageUpdate,\n onDone,\n onError,\n } = options;\n\n const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;\n const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: \"gpt-4o\", defaultAnthropic: \"claude-sonnet-4-6\" });\n\n // Build prompt content (supports multimodal with reference image)\n const extra = extraInstructions ? `\\nAdditional instructions: ${extraInstructions}` : \"\";\n const content: any[] = [];\n if (referenceImage) {\n content.push({ type: \"image\", image: referenceImage });\n content.push({\n type: \"text\",\n text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n } else {\n content.push({\n type: \"text\",\n text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n }\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: [{ role: \"user\", content }],\n });\n\n const allSections: Section3[] = [];\n const imagePromises: Promise<void>[] = [];\n let sectionOrder = 0;\n let buffer = \"\";\n\n try {\n for await (const chunk of result.textStream) {\n buffer += chunk;\n\n const [objects, remaining] = extractJsonObjects(buffer);\n buffer = remaining;\n\n for (const obj of objects) {\n if (!obj.html || !obj.label) continue;\n\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n\n allSections.push(section);\n onSection?.(section);\n\n // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)\n const slots = findImageSlots(section.html);\n if (slots.length > 0) {\n const sectionRef = section;\n const slotsSnapshot = slots.map((s) => ({ ...s }));\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n slotsSnapshot.map(async (slot) => {\n let url: string | null = null;\n if (openaiApiKey) {\n url = await generateImage(slot.query, openaiApiKey).catch(() => null);\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 }\n\n // Parse remaining buffer\n if (buffer.trim()) {\n let cleaned = buffer.trim();\n if (cleaned.startsWith(\"```\")) {\n cleaned = cleaned\n .replace(/^```(?:json)?\\s*/, \"\")\n .replace(/\\s*```$/, \"\");\n }\n const [lastObjects] = extractJsonObjects(cleaned);\n for (const obj of lastObjects) {\n if (!obj.html || !obj.label) continue;\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n allSections.push(section);\n onSection?.(section);\n }\n }\n\n // Wait for image enrichment\n await Promise.allSettled(imagePromises);\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":";;;;;;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,cAAc;AAMvB,eAAe,aAAa,MAA8H;AACxJ,QAAM,YAAY,KAAK,gBAAgB,QAAQ,IAAI;AACnD,MAAI,WAAW;AACb,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,gBAAgB;AACtD,UAAM,SAAS,aAAa,EAAE,QAAQ,UAAU,CAAC;AACjD,WAAO,OAAO,KAAK,WAAW,KAAK,aAAa;AAAA,EAClD;AACA,QAAM,YAAY,KAAK,kBACnB,gBAAgB,EAAE,QAAQ,KAAK,gBAAgB,CAAC,IAChD,gBAAgB;AACpB,SAAO,UAAU,KAAK,WAAW,KAAK,gBAAgB;AACxD;AAEO,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;AAiDtB,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAatB,SAAS,mBAAmB,MAA+B;AAChE,QAAM,UAAiB,CAAC;AACxB,MAAI,YAAY;AAEhB,SAAO,UAAU,SAAS,GAAG;AAC3B,gBAAY,UAAU,UAAU;AAChC,QAAI,CAAC,UAAU,WAAW,GAAG,GAAG;AAC9B,YAAM,YAAY,UAAU,QAAQ,GAAG;AACvC,UAAI,cAAc,GAAI;AACtB,kBAAY,UAAU,MAAM,SAAS;AACrC;AAAA,IACF;AAEA,QAAI,QAAQ;AACZ,QAAI,WAAW;AACf,QAAI,SAAS;AACb,QAAI,MAAM;AAEV,aAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AACzC,YAAM,KAAK,UAAU,CAAC;AACtB,UAAI,QAAQ;AAAE,iBAAS;AAAO;AAAA,MAAU;AACxC,UAAI,OAAO,MAAM;AAAE,iBAAS;AAAM;AAAA,MAAU;AAC5C,UAAI,OAAO,KAAK;AAAE,mBAAW,CAAC;AAAU;AAAA,MAAU;AAClD,UAAI,SAAU;AACd,UAAI,OAAO,IAAK;AAChB,UAAI,OAAO,KAAK;AAAE;AAAS,YAAI,UAAU,GAAG;AAAE,gBAAM;AAAG;AAAA,QAAO;AAAA,MAAE;AAAA,IAClE;AAEA,QAAI,QAAQ,GAAI;AAEhB,UAAM,YAAY,UAAU,MAAM,GAAG,MAAM,CAAC;AAC5C,gBAAY,UAAU,MAAM,MAAM,CAAC;AAEnC,QAAI;AACF,cAAQ,KAAK,KAAK,MAAM,SAAS,CAAC;AAAA,IACpC,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,CAAC,SAAS,SAAS;AAC5B;AAiCA,eAAsB,gBAAgB,SAA+C;AACnF,QAAM;AAAA,IACJ;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,eAAe,iBAAiB,QAAQ,IAAI;AAClD,QAAM,QAAQ,MAAM,aAAa,EAAE,cAAc,iBAAiB,SAAS,eAAe,UAAU,kBAAkB,oBAAoB,CAAC;AAG3I,QAAM,QAAQ,oBAAoB;AAAA,2BAA8B,iBAAiB,KAAK;AACtF,QAAM,UAAiB,CAAC;AACxB,MAAI,gBAAgB;AAClB,YAAQ,KAAK,EAAE,MAAM,SAAS,OAAO,eAAe,CAAC;AACrD,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,QAAM,SAAS,WAAW;AAAA,IACxB;AAAA,IACA,QAAQ;AAAA,IACR,UAAU,CAAC,EAAE,MAAM,QAAQ,QAAQ,CAAC;AAAA,EACtC,CAAC;AAED,QAAM,cAA0B,CAAC;AACjC,QAAM,gBAAiC,CAAC;AACxC,MAAI,eAAe;AACnB,MAAI,SAAS;AAEb,MAAI;AACF,qBAAiB,SAAS,OAAO,YAAY;AAC3C,gBAAU;AAEV,YAAM,CAAC,SAAS,SAAS,IAAI,mBAAmB,MAAM;AACtD,eAAS;AAET,iBAAW,OAAO,SAAS;AACzB,YAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,MAAO;AAE7B,cAAM,UAAoB;AAAA,UACxB,IAAI,OAAO,CAAC;AAAA,UACZ,OAAO;AAAA,UACP,MAAM,IAAI;AAAA,UACV,OAAO,IAAI;AAAA,QACb;AAEA,oBAAY,KAAK,OAAO;AACxB,oBAAY,OAAO;AAGnB,cAAM,QAAQ,eAAe,QAAQ,IAAI;AACzC,YAAI,MAAM,SAAS,GAAG;AACpB,gBAAM,aAAa;AACnB,gBAAM,gBAAgB,MAAM,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;AACjD,wBAAc;AAAA,aACX,YAAY;AACX,oBAAM,UAAU,MAAM,QAAQ;AAAA,gBAC5B,cAAc,IAAI,OAAO,SAAS;AAChC,sBAAI,MAAqB;AACzB,sBAAI,cAAc;AAChB,0BAAM,MAAM,cAAc,KAAK,OAAO,YAAY,EAAE,MAAM,MAAM,IAAI;AAAA,kBACtE;AACA,sBAAI,CAAC,KAAK;AACR,0BAAM,MAAM,MAAM,YAAY,KAAK,OAAO,YAAY,EAAE,MAAM,MAAM,IAAI;AACxE,0BAAM,KAAK,OAAO;AAAA,kBACpB;AACA,0BAAQ,mDAAmD,mBAAmB,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC;AACtG,yBAAO,EAAE,MAAM,IAAI;AAAA,gBACrB,CAAC;AAAA,cACH;AACA,kBAAI,OAAO,WAAW;AACtB,yBAAW,KAAK,SAAS;AACvB,oBAAI,EAAE,WAAW,eAAe,EAAE,OAAO;AACvC,wBAAM,EAAE,MAAM,IAAI,IAAI,EAAE;AACxB,wBAAM,cAAc,KAAK,WAAW,QAAQ,SAAS,GAAG;AACxD,yBAAO,KAAK,WAAW,KAAK,WAAW,WAAW;AAAA,gBACpD;AAAA,cACF;AACA,kBAAI,SAAS,WAAW,MAAM;AAC5B,2BAAW,OAAO;AAClB,gCAAgB,WAAW,IAAI,IAAI;AAAA,cACrC;AAAA,YACF,GAAG;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,OAAO,KAAK,GAAG;AACjB,UAAI,UAAU,OAAO,KAAK;AAC1B,UAAI,QAAQ,WAAW,KAAK,GAAG;AAC7B,kBAAU,QACP,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,WAAW,EAAE;AAAA,MAC1B;AACA,YAAM,CAAC,WAAW,IAAI,mBAAmB,OAAO;AAChD,iBAAW,OAAO,aAAa;AAC7B,YAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,MAAO;AAC7B,cAAM,UAAoB;AAAA,UACxB,IAAI,OAAO,CAAC;AAAA,UACZ,OAAO;AAAA,UACP,MAAM,IAAI;AAAA,UACV,OAAO,IAAI;AAAA,QACb;AACA,oBAAY,KAAK,OAAO;AACxB,oBAAY,OAAO;AAAA,MACrB;AAAA,IACF;AAGA,UAAM,QAAQ,WAAW,aAAa;AAEtC,aAAS,WAAW;AACpB,WAAO;AAAA,EACT,SAAS,KAAU;AACjB,UAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,KAAK,WAAW,mBAAmB;AACxF,cAAU,KAAK;AACf,UAAM;AAAA,EACR;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-RTGCZUNJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|