@easybits.cloud/html-tailwind-generator 0.2.34 → 0.2.36
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/dist/chunk-4NOYN34W.js +55 -0
- package/dist/chunk-4NOYN34W.js.map +1 -0
- package/dist/{chunk-7YRGPGE2.js → chunk-6FFDL3TO.js} +18 -14
- package/dist/{chunk-7YRGPGE2.js.map → chunk-6FFDL3TO.js.map} +1 -1
- package/dist/{chunk-3FR2EDTU.js → chunk-LTKQN25A.js} +2 -2
- package/dist/{chunk-QEUHDGSB.js → chunk-TVZENGCX.js} +7 -7
- package/dist/{chunk-QEUHDGSB.js.map → chunk-TVZENGCX.js.map} +1 -1
- package/dist/generate.js +3 -3
- package/dist/generateDocument.d.ts +1 -1
- package/dist/generateDocument.js +3 -3
- package/dist/images.d.ts +1 -0
- package/dist/images.js +1 -1
- package/dist/index.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-VCNJLYAQ.js +0 -55
- package/dist/chunk-VCNJLYAQ.js.map +0 -1
- /package/dist/{chunk-3FR2EDTU.js.map → chunk-LTKQN25A.js.map} +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/images/svgGenerator.ts
|
|
2
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
|
+
import { generateText } from "ai";
|
|
4
|
+
var SVG_SYSTEM_PROMPT = `You are a professional SVG designer. Generate clean, compact SVG graphics for documents.
|
|
5
|
+
|
|
6
|
+
STRICT SIZE RULES:
|
|
7
|
+
- ALWAYS use viewBox="0 0 600 300" (2:1 ratio) \u2014 no exceptions
|
|
8
|
+
- ALWAYS set width="100%" height="auto" \u2014 NEVER use fixed pixel width/height
|
|
9
|
+
- NO internal padding or margins \u2014 content fills the viewBox edge-to-edge (leave only 10-20px padding)
|
|
10
|
+
- Keep SVGs under 2KB \u2014 simplicity is key
|
|
11
|
+
|
|
12
|
+
STYLE RULES:
|
|
13
|
+
- Output ONLY the <svg>...</svg> tag \u2014 no markdown, no explanation
|
|
14
|
+
- Flat design: solid fills, no drop shadows, minimal gradients (max 1-2)
|
|
15
|
+
- Max 8-10 elements total \u2014 prefer fewer, larger shapes over many small ones
|
|
16
|
+
- Color palette: use the provided theme colors, or defaults (#6366f1, #8b5cf6, #ec4899, #14b8a6, #f59e0b)
|
|
17
|
+
- Text: font-family="system-ui, sans-serif", font-size 12-16px, max 5 text labels
|
|
18
|
+
- Self-contained: no external references, all styles inline
|
|
19
|
+
|
|
20
|
+
CHART TYPES:
|
|
21
|
+
- Bar charts (vertical/horizontal) \u2014 max 6 bars, rounded caps
|
|
22
|
+
- Pie/donut charts \u2014 max 5 segments
|
|
23
|
+
- Line charts \u2014 smooth paths, max 8 data points
|
|
24
|
+
- Progress/gauge charts
|
|
25
|
+
- Simple comparison charts
|
|
26
|
+
- Stat cards with visual elements
|
|
27
|
+
|
|
28
|
+
AVOID: complex illustrations, many small elements, decorative borders, nested groups deeper than 2 levels.`;
|
|
29
|
+
async function generateSvg(prompt, anthropicApiKey, options) {
|
|
30
|
+
const apiKey = anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
31
|
+
const anthropic = createAnthropic({ apiKey: apiKey || void 0 });
|
|
32
|
+
const sizeHint = options?.width && options?.height ? ` Target dimensions: ${options.width}x${options.height}px.` : "";
|
|
33
|
+
const colorHint = options?.themeColors ? ` Use these theme colors: ${options.themeColors}.` : "";
|
|
34
|
+
const result = await generateText({
|
|
35
|
+
model: anthropic("claude-haiku-4-5-20251001"),
|
|
36
|
+
system: SVG_SYSTEM_PROMPT,
|
|
37
|
+
messages: [
|
|
38
|
+
{
|
|
39
|
+
role: "user",
|
|
40
|
+
content: `Generate an SVG for: ${prompt}${sizeHint}${colorHint}`
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
maxOutputTokens: 2e3
|
|
44
|
+
});
|
|
45
|
+
const svgMatch = result.text.match(/<svg[\s\S]*<\/svg>/i);
|
|
46
|
+
if (!svgMatch) {
|
|
47
|
+
throw new Error("SVG generation failed \u2014 no <svg> tag in response");
|
|
48
|
+
}
|
|
49
|
+
return svgMatch[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
generateSvg
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=chunk-4NOYN34W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/images/svgGenerator.ts"],"sourcesContent":["import { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { generateText } from \"ai\";\n\nconst SVG_SYSTEM_PROMPT = `You are a professional SVG designer. Generate clean, compact SVG graphics for documents.\n\nSTRICT SIZE RULES:\n- ALWAYS use viewBox=\"0 0 600 300\" (2:1 ratio) — no exceptions\n- ALWAYS set width=\"100%\" height=\"auto\" — NEVER use fixed pixel width/height\n- NO internal padding or margins — content fills the viewBox edge-to-edge (leave only 10-20px padding)\n- Keep SVGs under 2KB — simplicity is key\n\nSTYLE RULES:\n- Output ONLY the <svg>...</svg> tag — no markdown, no explanation\n- Flat design: solid fills, no drop shadows, minimal gradients (max 1-2)\n- Max 8-10 elements total — prefer fewer, larger shapes over many small ones\n- Color palette: use the provided theme colors, or defaults (#6366f1, #8b5cf6, #ec4899, #14b8a6, #f59e0b)\n- Text: font-family=\"system-ui, sans-serif\", font-size 12-16px, max 5 text labels\n- Self-contained: no external references, all styles inline\n\nCHART TYPES:\n- Bar charts (vertical/horizontal) — max 6 bars, rounded caps\n- Pie/donut charts — max 5 segments\n- Line charts — smooth paths, max 8 data points\n- Progress/gauge charts\n- Simple comparison charts\n- Stat cards with visual elements\n\nAVOID: complex illustrations, many small elements, decorative borders, nested groups deeper than 2 levels.`;\n\n\nexport async function generateSvg(\n prompt: string,\n anthropicApiKey?: string,\n options?: { width?: number; height?: number; themeColors?: string }\n): Promise<string> {\n const apiKey = anthropicApiKey || process.env.ANTHROPIC_API_KEY;\n const anthropic = createAnthropic({ apiKey: apiKey || undefined });\n\n const sizeHint = options?.width && options?.height\n ? ` Target dimensions: ${options.width}x${options.height}px.`\n : \"\";\n const colorHint = options?.themeColors\n ? ` Use these theme colors: ${options.themeColors}.`\n : \"\";\n\n const result = await generateText({\n model: anthropic(\"claude-haiku-4-5-20251001\"),\n system: SVG_SYSTEM_PROMPT,\n messages: [\n {\n role: \"user\",\n content: `Generate an SVG for: ${prompt}${sizeHint}${colorHint}`,\n },\n ],\n maxOutputTokens: 2000,\n });\n\n // Extract just the SVG tag\n const svgMatch = result.text.match(/<svg[\\s\\S]*<\\/svg>/i);\n if (!svgMatch) {\n throw new Error(\"SVG generation failed — no <svg> tag in response\");\n }\n\n return svgMatch[0];\n}\n"],"mappings":";AAAA,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAE7B,IAAM,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2B1B,eAAsB,YACpB,QACA,iBACA,SACiB;AACjB,QAAM,SAAS,mBAAmB,QAAQ,IAAI;AAC9C,QAAM,YAAY,gBAAgB,EAAE,QAAQ,UAAU,OAAU,CAAC;AAEjE,QAAM,WAAW,SAAS,SAAS,SAAS,SACxC,uBAAuB,QAAQ,KAAK,IAAI,QAAQ,MAAM,QACtD;AACJ,QAAM,YAAY,SAAS,cACvB,4BAA4B,QAAQ,WAAW,MAC/C;AAEJ,QAAM,SAAS,MAAM,aAAa;AAAA,IAChC,OAAO,UAAU,2BAA2B;AAAA,IAC5C,QAAQ;AAAA,IACR,UAAU;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN,SAAS,wBAAwB,MAAM,GAAG,QAAQ,GAAG,SAAS;AAAA,MAChE;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,EACnB,CAAC;AAGD,QAAM,WAAW,OAAO,KAAK,MAAM,qBAAqB;AACxD,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,uDAAkD;AAAA,EACpE;AAEA,SAAO,SAAS,CAAC;AACnB;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dataUrlToImagePart,
|
|
3
3
|
streamGenerate
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-TVZENGCX.js";
|
|
5
5
|
|
|
6
6
|
// src/generateDocument.ts
|
|
7
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.
|
|
@@ -28,6 +28,15 @@ STRICT PROHIBITIONS:
|
|
|
28
28
|
5. **Max 2 font weights per page** \u2014 Pick 2 (e.g. font-semibold + font-normal, or font-bold + font-light). Don't mix 4-5 weights.
|
|
29
29
|
6. **Generous whitespace** \u2014 Don't fill every centimeter. Leave breathing room. Use py-8, py-12, gap-6, gap-8 liberally. Less content per page = more professional.
|
|
30
30
|
|
|
31
|
+
LAYOUT OVERFLOW PREVENTION \u2014 CRITICAL:
|
|
32
|
+
- Max 2 columns side by side \u2014 each with w-1/2. NEVER use 3+ columns.
|
|
33
|
+
- Decorative sidebars: max w-16 (4rem). NEVER use w-[2.5in] or wider sidebars \u2014 they steal too much space.
|
|
34
|
+
- Stats/metric grids: max 3 items per row (grid-cols-3). Use gap-4 or gap-6.
|
|
35
|
+
- Tables: max 4 columns, use text-xs or text-sm for cell text, px-3 py-2 cell padding.
|
|
36
|
+
- Images: always w-full or max-w-[50%] \u2014 never fixed pixel widths.
|
|
37
|
+
- Text: never use text-6xl or larger except for cover page title. Body text: text-sm or text-base.
|
|
38
|
+
- NEVER use absolute positioning that could overflow \u2014 prefer flex/grid layouts.
|
|
39
|
+
|
|
31
40
|
DESIGN:
|
|
32
41
|
- Clean, sophisticated layouts \u2014 think McKinsey reports, annual reports, premium proposals
|
|
33
42
|
- Typography: strong hierarchy with just 2 weights, clear headings vs body
|
|
@@ -64,19 +73,14 @@ TAILWIND v3 NOTES:
|
|
|
64
73
|
- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
|
|
65
74
|
- Borders: border + border-gray-200 for visible borders
|
|
66
75
|
|
|
67
|
-
EXAMPLE \u2014 Cover page:
|
|
76
|
+
EXAMPLE \u2014 Cover page (simple, no wide sidebars):
|
|
68
77
|
<section class="w-[8.5in] min-h-[11in] relative overflow-hidden bg-white">
|
|
69
|
-
<div class="absolute left-0 top-0 w-
|
|
70
|
-
<div class="
|
|
71
|
-
<div class="
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</
|
|
75
|
-
<div class="flex-1 flex flex-col justify-center px-12">
|
|
76
|
-
<h1 class="text-5xl font-bold text-gray-900 leading-tight">Reporte<br/>Trimestral</h1>
|
|
77
|
-
<div class="w-16 h-1 bg-primary mt-6 mb-4"></div>
|
|
78
|
-
<p class="text-lg font-normal text-gray-500">Resultados y an\xE1lisis del primer trimestre</p>
|
|
79
|
-
</div>
|
|
78
|
+
<div class="absolute left-0 top-0 w-2 h-full bg-primary"></div>
|
|
79
|
+
<div class="flex flex-col justify-center h-[11in] px-[1in]">
|
|
80
|
+
<div class="text-sm font-normal text-primary mb-4">Marzo 2026 \xB7 Versi\xF3n 1.0</div>
|
|
81
|
+
<h1 class="text-5xl font-bold text-gray-900 leading-tight">Reporte<br/>Trimestral</h1>
|
|
82
|
+
<div class="w-16 h-1 bg-primary mt-6 mb-4"></div>
|
|
83
|
+
<p class="text-lg font-normal text-gray-500">Resultados y an\xE1lisis del primer trimestre</p>
|
|
80
84
|
</div>
|
|
81
85
|
</section>
|
|
82
86
|
|
|
@@ -155,4 +159,4 @@ export {
|
|
|
155
159
|
DOCUMENT_PROMPT_SUFFIX,
|
|
156
160
|
generateDocument
|
|
157
161
|
};
|
|
158
|
-
//# sourceMappingURL=chunk-
|
|
162
|
+
//# sourceMappingURL=chunk-6FFDL3TO.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/generateDocument.ts"],"sourcesContent":["import { streamGenerate, dataUrlToImagePart } from \"./streamCore\";\nimport type { Section3 } from \"./types\";\n\nexport const DOCUMENT_SYSTEM_PROMPT = `You are a professional document designer who creates stunning letter-sized (8.5\" × 11\") document pages using HTML + Tailwind CSS.\n\nRULES:\n- Each page is a <section> element sized for letter paper\n- Page structure: <section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden\">\n- The section itself has NO padding — backgrounds, gradients, and decorative elements go edge-to-edge\n- For text content, use an inner wrapper: <div class=\"px-[0.75in] py-[0.5in]\">...content...</div>\n- Cover pages and decorative sections can use full-bleed backgrounds (bg-primary, gradients, images that fill the entire page)\n- Content MUST NOT overflow page boundaries — be conservative with spacing\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import)\n- NO JavaScript, only HTML+Tailwind\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real content from the source material, not Lorem ipsum\n- NOT responsive — fixed letter size, no breakpoints needed\n- Sections can have ANY background — full-bleed color, gradients, or white. Not limited to white paper.\n\nSTRICT PROHIBITIONS:\n1. **NO EMOJI** — Never use emoji characters (🚀❌✅📊 etc.). Instead use inline SVG icons or colored divs. For bullet decorators use small colored circles (<span class=\"w-2 h-2 rounded-full bg-primary inline-block\"></span>) or simple SVG.\n2. **NO Chart.js / NO JavaScript** — Never reference Chart.js, canvas, or any JS library. For data visualization use pure CSS: progress bars (div with percentage width + bg-primary), horizontal bars, styled tables with colored cells. Never use <canvas> or <script>.\n3. **NO buttons or CTAs** — This is a print document, not a web page. No \"Contactar\", \"Ver más\", \"Comprar\" buttons. Use text with contact info instead.\n4. **CONTRAST IS MANDATORY** — Dark/colored backgrounds (bg-primary, bg-primary-dark, bg-secondary, dark gradients) MUST use text-white or text-on-primary. Light backgrounds (white, bg-surface, bg-surface-alt) MUST use text-gray-900 or text-on-surface. NEVER use dark text on dark backgrounds or light text on light backgrounds.\n5. **Max 2 font weights per page** — Pick 2 (e.g. font-semibold + font-normal, or font-bold + font-light). Don't mix 4-5 weights.\n6. **Generous whitespace** — Don't fill every centimeter. Leave breathing room. Use py-8, py-12, gap-6, gap-8 liberally. Less content per page = more professional.\n\nDESIGN:\n- Clean, sophisticated layouts — think McKinsey reports, annual reports, premium proposals\n- Typography: strong hierarchy with just 2 weights, clear headings vs body\n- Tables: alternating row colors (bg-surface-alt), clean borders, generous cell padding (px-4 py-3)\n- Decorative elements: colored sidebars, header bands, geometric accent shapes (pure CSS divs with bg-primary)\n- First page MUST be a cover/title page with impactful design\n- For numerical data: CSS progress bars, styled tables with colored cells, large stat numbers — NEVER canvas/charts\n- Icons: use simple inline SVG (12-20px) for visual accents. Keep SVGs minimal (single path, no complex illustrations)\n\nCSS PROGRESS BARS — use this pattern for data visualization:\n<div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-primary h-3 rounded-full\" style=\"width: 75%\"></div></div>\n\nCOLOR SYSTEM — use semantic classes:\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- Cover pages should use bold full-bleed backgrounds (bg-primary, gradients from-primary to-primary-dark)\n- CONTRAST: bg-primary/bg-primary-dark/bg-secondary → text-white or text-on-primary. White/bg-surface → text-gray-900 or text-on-surface\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\nCHARTS & DATA VISUALIZATION (SVG):\n- For charts, diagrams, and decorative data graphics, use:\n <div data-svg-chart=\"bar chart showing Q1 revenue: Jan $45K, Feb $52K, Mar $61K\" class=\"w-full\"></div>\n- The system generates professional SVG charts with a specialized tool — NEVER draw SVGs yourself\n- Use descriptive prompts with data points: type of chart + what it shows + actual values\n- Examples: \"donut chart: 40% Marketing, 30% Sales, 20% R&D, 10% Admin\", \"line chart showing growth: Q1 $100K, Q2 $150K, Q3 $220K, Q4 $310K\"\n- For simple metrics, still prefer CSS progress bars (they render faster)\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\nEXAMPLE — Cover page:\n<section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden bg-white\">\n <div class=\"absolute left-0 top-0 w-[2.5in] h-full bg-primary\"></div>\n <div class=\"relative z-10 flex h-[11in]\">\n <div class=\"w-[2.5in] flex flex-col justify-end px-8 py-12\">\n <div class=\"text-white text-sm font-normal opacity-80\">Marzo 2026</div>\n <div class=\"text-white text-sm font-normal opacity-80 mt-1\">Versión 1.0</div>\n </div>\n <div class=\"flex-1 flex flex-col justify-center px-12\">\n <h1 class=\"text-5xl font-bold text-gray-900 leading-tight\">Reporte<br/>Trimestral</h1>\n <div class=\"w-16 h-1 bg-primary mt-6 mb-4\"></div>\n <p class=\"text-lg font-normal text-gray-500\">Resultados y análisis del primer trimestre</p>\n </div>\n </div>\n</section>\n\nEXAMPLE — Content page with table + progress bars:\n<section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden bg-white\">\n <div class=\"h-1.5 bg-primary w-full\"></div>\n <div class=\"px-[0.75in] py-[0.5in]\">\n <h2 class=\"text-2xl font-bold text-gray-900 mb-1\">Métricas de Rendimiento</h2>\n <p class=\"text-sm font-normal text-gray-500 mb-8\">Indicadores clave del periodo enero—marzo</p>\n <table class=\"w-full text-sm mb-10\">\n <thead><tr class=\"bg-primary text-white\"><th class=\"px-4 py-3 text-left font-semibold\">Indicador</th><th class=\"px-4 py-3 text-left font-semibold\">Valor</th><th class=\"px-4 py-3 text-left font-semibold\">Meta</th></tr></thead>\n <tbody>\n <tr class=\"bg-surface-alt\"><td class=\"px-4 py-3 text-gray-900\">Ingresos</td><td class=\"px-4 py-3 text-gray-900\">$1.2M</td><td class=\"px-4 py-3 text-gray-900\">$1.5M</td></tr>\n <tr><td class=\"px-4 py-3 text-gray-900\">Clientes nuevos</td><td class=\"px-4 py-3 text-gray-900\">340</td><td class=\"px-4 py-3 text-gray-900\">300</td></tr>\n <tr class=\"bg-surface-alt\"><td class=\"px-4 py-3 text-gray-900\">Retención</td><td class=\"px-4 py-3 text-gray-900\">92%</td><td class=\"px-4 py-3 text-gray-900\">90%</td></tr>\n </tbody>\n </table>\n <h3 class=\"text-lg font-bold text-gray-900 mb-4\">Progreso por Área</h3>\n <div class=\"space-y-4\">\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Ventas</span><span class=\"text-gray-500\">80%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-primary h-3 rounded-full\" style=\"width: 80%\"></div></div></div>\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Marketing</span><span class=\"text-gray-500\">65%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-secondary h-3 rounded-full\" style=\"width: 65%\"></div></div></div>\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Producto</span><span class=\"text-gray-500\">95%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-accent h-3 rounded-full\" style=\"width: 95%\"></div></div></div>\n </div>\n </div>\n</section>`;\n\nexport const DOCUMENT_PROMPT_SUFFIX = `\n\nOUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Page Title\", \"html\": \"<section class='w-[8.5in] min-h-[11in] relative overflow-hidden'>...</section>\"}\n\nGenerate 3-8 pages depending on content length. First page = cover/title page.\nEach page must fit within letter size (8.5\" × 11\"). Be conservative with spacing.\nMake each page visually distinct — different layouts, different accent placements.`;\n\nexport interface GenerateDocumentOptions {\n anthropicApiKey?: string;\n openaiApiKey?: string;\n prompt: string;\n logoUrl?: string;\n referenceImage?: string;\n extraInstructions?: string;\n model?: string;\n pexelsApiKey?: string;\n persistImage?: (tempUrl: string, query: string) => Promise<string>;\n onSection?: (section: Section3) => void;\n onImageUpdate?: (sectionId: string, html: string) => void;\n onDone?: (sections: Section3[]) => void;\n onError?: (error: Error) => void;\n}\n\n/**\n * Generate a multi-page document with streaming AI + image enrichment.\n */\nexport async function generateDocument(options: GenerateDocumentOptions): Promise<Section3[]> {\n const {\n prompt,\n logoUrl,\n referenceImage,\n extraInstructions,\n ...rest\n } = options;\n\n const extra = extraInstructions ? `\\nAdditional instructions: ${extraInstructions}` : \"\";\n // Truncate prompt to prevent token overflow (max ~15K chars ≈ 5K tokens)\n const safePrompt = prompt.length > 15_000 ? prompt.substring(0, 15_000) + \"\\n[...content truncated...]\" : prompt;\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 (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: ${safePrompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`,\n });\n } else {\n content.push({\n type: \"text\",\n text: `Create a professional document for: ${safePrompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`,\n });\n }\n\n return streamGenerate({\n ...rest,\n systemPrompt: DOCUMENT_SYSTEM_PROMPT,\n userContent: content,\n });\n}\n"],"mappings":";;;;;;AAGO,IAAM,yBAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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;AAmG/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;AAEtF,QAAM,aAAa,OAAO,SAAS,OAAS,OAAO,UAAU,GAAG,IAAM,IAAI,gCAAgC;AAC1G,QAAM,kBAAkB,UACpB;AAAA;AAAA,YAAgG,OAAO;AAAA,mEACvG;AAEJ,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,wEAAwE,UAAU,GAAG,eAAe,GAAG,KAAK,GAAG,sBAAsB;AAAA,IAC7I,CAAC;AAAA,EACH,OAAO;AACL,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN,MAAM,uCAAuC,UAAU,GAAG,eAAe,GAAG,KAAK,GAAG,sBAAsB;AAAA,IAC5G,CAAC;AAAA,EACH;AAEA,SAAO,eAAe;AAAA,IACpB,GAAG;AAAA,IACH,cAAc;AAAA,IACd,aAAa;AAAA,EACf,CAAC;AACH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/generateDocument.ts"],"sourcesContent":["import { streamGenerate, dataUrlToImagePart } from \"./streamCore\";\nimport type { Section3 } from \"./types\";\n\nexport const DOCUMENT_SYSTEM_PROMPT = `You are a professional document designer who creates stunning letter-sized (8.5\" × 11\") document pages using HTML + Tailwind CSS.\n\nRULES:\n- Each page is a <section> element sized for letter paper\n- Page structure: <section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden\">\n- The section itself has NO padding — backgrounds, gradients, and decorative elements go edge-to-edge\n- For text content, use an inner wrapper: <div class=\"px-[0.75in] py-[0.5in]\">...content...</div>\n- Cover pages and decorative sections can use full-bleed backgrounds (bg-primary, gradients, images that fill the entire page)\n- Content MUST NOT overflow page boundaries — be conservative with spacing\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import)\n- NO JavaScript, only HTML+Tailwind\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real content from the source material, not Lorem ipsum\n- NOT responsive — fixed letter size, no breakpoints needed\n- Sections can have ANY background — full-bleed color, gradients, or white. Not limited to white paper.\n\nSTRICT PROHIBITIONS:\n1. **NO EMOJI** — Never use emoji characters (🚀❌✅📊 etc.). Instead use inline SVG icons or colored divs. For bullet decorators use small colored circles (<span class=\"w-2 h-2 rounded-full bg-primary inline-block\"></span>) or simple SVG.\n2. **NO Chart.js / NO JavaScript** — Never reference Chart.js, canvas, or any JS library. For data visualization use pure CSS: progress bars (div with percentage width + bg-primary), horizontal bars, styled tables with colored cells. Never use <canvas> or <script>.\n3. **NO buttons or CTAs** — This is a print document, not a web page. No \"Contactar\", \"Ver más\", \"Comprar\" buttons. Use text with contact info instead.\n4. **CONTRAST IS MANDATORY** — Dark/colored backgrounds (bg-primary, bg-primary-dark, bg-secondary, dark gradients) MUST use text-white or text-on-primary. Light backgrounds (white, bg-surface, bg-surface-alt) MUST use text-gray-900 or text-on-surface. NEVER use dark text on dark backgrounds or light text on light backgrounds.\n5. **Max 2 font weights per page** — Pick 2 (e.g. font-semibold + font-normal, or font-bold + font-light). Don't mix 4-5 weights.\n6. **Generous whitespace** — Don't fill every centimeter. Leave breathing room. Use py-8, py-12, gap-6, gap-8 liberally. Less content per page = more professional.\n\nLAYOUT OVERFLOW PREVENTION — CRITICAL:\n- Max 2 columns side by side — each with w-1/2. NEVER use 3+ columns.\n- Decorative sidebars: max w-16 (4rem). NEVER use w-[2.5in] or wider sidebars — they steal too much space.\n- Stats/metric grids: max 3 items per row (grid-cols-3). Use gap-4 or gap-6.\n- Tables: max 4 columns, use text-xs or text-sm for cell text, px-3 py-2 cell padding.\n- Images: always w-full or max-w-[50%] — never fixed pixel widths.\n- Text: never use text-6xl or larger except for cover page title. Body text: text-sm or text-base.\n- NEVER use absolute positioning that could overflow — prefer flex/grid layouts.\n\nDESIGN:\n- Clean, sophisticated layouts — think McKinsey reports, annual reports, premium proposals\n- Typography: strong hierarchy with just 2 weights, clear headings vs body\n- Tables: alternating row colors (bg-surface-alt), clean borders, generous cell padding (px-4 py-3)\n- Decorative elements: colored sidebars, header bands, geometric accent shapes (pure CSS divs with bg-primary)\n- First page MUST be a cover/title page with impactful design\n- For numerical data: CSS progress bars, styled tables with colored cells, large stat numbers — NEVER canvas/charts\n- Icons: use simple inline SVG (12-20px) for visual accents. Keep SVGs minimal (single path, no complex illustrations)\n\nCSS PROGRESS BARS — use this pattern for data visualization:\n<div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-primary h-3 rounded-full\" style=\"width: 75%\"></div></div>\n\nCOLOR SYSTEM — use semantic classes:\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- Cover pages should use bold full-bleed backgrounds (bg-primary, gradients from-primary to-primary-dark)\n- CONTRAST: bg-primary/bg-primary-dark/bg-secondary → text-white or text-on-primary. White/bg-surface → text-gray-900 or text-on-surface\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\nCHARTS & DATA VISUALIZATION (SVG):\n- For charts, diagrams, and decorative data graphics, use:\n <div data-svg-chart=\"bar chart showing Q1 revenue: Jan $45K, Feb $52K, Mar $61K\" class=\"w-full\"></div>\n- The system generates professional SVG charts with a specialized tool — NEVER draw SVGs yourself\n- Use descriptive prompts with data points: type of chart + what it shows + actual values\n- Examples: \"donut chart: 40% Marketing, 30% Sales, 20% R&D, 10% Admin\", \"line chart showing growth: Q1 $100K, Q2 $150K, Q3 $220K, Q4 $310K\"\n- For simple metrics, still prefer CSS progress bars (they render faster)\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\nEXAMPLE — Cover page (simple, no wide sidebars):\n<section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden bg-white\">\n <div class=\"absolute left-0 top-0 w-2 h-full bg-primary\"></div>\n <div class=\"flex flex-col justify-center h-[11in] px-[1in]\">\n <div class=\"text-sm font-normal text-primary mb-4\">Marzo 2026 · Versión 1.0</div>\n <h1 class=\"text-5xl font-bold text-gray-900 leading-tight\">Reporte<br/>Trimestral</h1>\n <div class=\"w-16 h-1 bg-primary mt-6 mb-4\"></div>\n <p class=\"text-lg font-normal text-gray-500\">Resultados y análisis del primer trimestre</p>\n </div>\n</section>\n\nEXAMPLE — Content page with table + progress bars:\n<section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden bg-white\">\n <div class=\"h-1.5 bg-primary w-full\"></div>\n <div class=\"px-[0.75in] py-[0.5in]\">\n <h2 class=\"text-2xl font-bold text-gray-900 mb-1\">Métricas de Rendimiento</h2>\n <p class=\"text-sm font-normal text-gray-500 mb-8\">Indicadores clave del periodo enero—marzo</p>\n <table class=\"w-full text-sm mb-10\">\n <thead><tr class=\"bg-primary text-white\"><th class=\"px-4 py-3 text-left font-semibold\">Indicador</th><th class=\"px-4 py-3 text-left font-semibold\">Valor</th><th class=\"px-4 py-3 text-left font-semibold\">Meta</th></tr></thead>\n <tbody>\n <tr class=\"bg-surface-alt\"><td class=\"px-4 py-3 text-gray-900\">Ingresos</td><td class=\"px-4 py-3 text-gray-900\">$1.2M</td><td class=\"px-4 py-3 text-gray-900\">$1.5M</td></tr>\n <tr><td class=\"px-4 py-3 text-gray-900\">Clientes nuevos</td><td class=\"px-4 py-3 text-gray-900\">340</td><td class=\"px-4 py-3 text-gray-900\">300</td></tr>\n <tr class=\"bg-surface-alt\"><td class=\"px-4 py-3 text-gray-900\">Retención</td><td class=\"px-4 py-3 text-gray-900\">92%</td><td class=\"px-4 py-3 text-gray-900\">90%</td></tr>\n </tbody>\n </table>\n <h3 class=\"text-lg font-bold text-gray-900 mb-4\">Progreso por Área</h3>\n <div class=\"space-y-4\">\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Ventas</span><span class=\"text-gray-500\">80%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-primary h-3 rounded-full\" style=\"width: 80%\"></div></div></div>\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Marketing</span><span class=\"text-gray-500\">65%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-secondary h-3 rounded-full\" style=\"width: 65%\"></div></div></div>\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Producto</span><span class=\"text-gray-500\">95%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-accent h-3 rounded-full\" style=\"width: 95%\"></div></div></div>\n </div>\n </div>\n</section>`;\n\nexport const DOCUMENT_PROMPT_SUFFIX = `\n\nOUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Page Title\", \"html\": \"<section class='w-[8.5in] min-h-[11in] relative overflow-hidden'>...</section>\"}\n\nGenerate 3-8 pages depending on content length. First page = cover/title page.\nEach page must fit within letter size (8.5\" × 11\"). Be conservative with spacing.\nMake each page visually distinct — different layouts, different accent placements.`;\n\nexport interface GenerateDocumentOptions {\n anthropicApiKey?: string;\n openaiApiKey?: string;\n prompt: string;\n logoUrl?: string;\n referenceImage?: string;\n extraInstructions?: string;\n model?: string;\n pexelsApiKey?: string;\n persistImage?: (tempUrl: string, query: string) => Promise<string>;\n onSection?: (section: Section3) => void;\n onImageUpdate?: (sectionId: string, html: string) => void;\n onDone?: (sections: Section3[]) => void;\n onError?: (error: Error) => void;\n}\n\n/**\n * Generate a multi-page document with streaming AI + image enrichment.\n */\nexport async function generateDocument(options: GenerateDocumentOptions): Promise<Section3[]> {\n const {\n prompt,\n logoUrl,\n referenceImage,\n extraInstructions,\n ...rest\n } = options;\n\n const extra = extraInstructions ? `\\nAdditional instructions: ${extraInstructions}` : \"\";\n // Truncate prompt to prevent token overflow (max ~15K chars ≈ 5K tokens)\n const safePrompt = prompt.length > 15_000 ? prompt.substring(0, 15_000) + \"\\n[...content truncated...]\" : prompt;\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 (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: ${safePrompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`,\n });\n } else {\n content.push({\n type: \"text\",\n text: `Create a professional document for: ${safePrompt}${logoInstruction}${extra}${DOCUMENT_PROMPT_SUFFIX}`,\n });\n }\n\n return streamGenerate({\n ...rest,\n systemPrompt: DOCUMENT_SYSTEM_PROMPT,\n userContent: content,\n });\n}\n"],"mappings":";;;;;;AAGO,IAAM,yBAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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;AAuG/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;AAEtF,QAAM,aAAa,OAAO,SAAS,OAAS,OAAO,UAAU,GAAG,IAAM,IAAI,gCAAgC;AAC1G,QAAM,kBAAkB,UACpB;AAAA;AAAA,YAAgG,OAAO;AAAA,mEACvG;AAEJ,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,wEAAwE,UAAU,GAAG,eAAe,GAAG,KAAK,GAAG,sBAAsB;AAAA,IAC7I,CAAC;AAAA,EACH,OAAO;AACL,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN,MAAM,uCAAuC,UAAU,GAAG,eAAe,GAAG,KAAK,GAAG,sBAAsB;AAAA,IAC5G,CAAC;AAAA,EACH;AAEA,SAAO,eAAe;AAAA,IACpB,GAAG;AAAA,IACH,cAAc;AAAA,IACd,aAAa;AAAA,EACf,CAAC;AACH;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dataUrlToImagePart,
|
|
3
3
|
streamGenerate
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-TVZENGCX.js";
|
|
5
5
|
|
|
6
6
|
// src/generate.ts
|
|
7
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.
|
|
@@ -128,4 +128,4 @@ export {
|
|
|
128
128
|
PROMPT_SUFFIX,
|
|
129
129
|
generateLanding
|
|
130
130
|
};
|
|
131
|
-
//# sourceMappingURL=chunk-
|
|
131
|
+
//# sourceMappingURL=chunk-LTKQN25A.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generateSvg
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-4NOYN34W.js";
|
|
4
4
|
import {
|
|
5
5
|
findImageSlots,
|
|
6
6
|
generateImage,
|
|
@@ -176,7 +176,11 @@ async function streamGenerate(options) {
|
|
|
176
176
|
const results = await Promise.allSettled(
|
|
177
177
|
slotsSnapshot.map(async (slot) => {
|
|
178
178
|
let url = null;
|
|
179
|
-
if (
|
|
179
|
+
if (pexelsApiKey) {
|
|
180
|
+
const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
|
|
181
|
+
url = img?.url || null;
|
|
182
|
+
}
|
|
183
|
+
if (!url && openaiApiKey) {
|
|
180
184
|
try {
|
|
181
185
|
const tempUrl = await generateImage(slot.query, openaiApiKey);
|
|
182
186
|
url = persistImage ? await persistImage(tempUrl, slot.query) : tempUrl;
|
|
@@ -184,10 +188,6 @@ async function streamGenerate(options) {
|
|
|
184
188
|
console.warn(`[dalle] failed for "${slot.query}":`, e);
|
|
185
189
|
}
|
|
186
190
|
}
|
|
187
|
-
if (!url) {
|
|
188
|
-
const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
|
|
189
|
-
url = img?.url || null;
|
|
190
|
-
}
|
|
191
191
|
url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
|
|
192
192
|
return { slot, url };
|
|
193
193
|
})
|
|
@@ -270,4 +270,4 @@ export {
|
|
|
270
270
|
extractJsonObjects,
|
|
271
271
|
streamGenerate
|
|
272
272
|
};
|
|
273
|
-
//# sourceMappingURL=chunk-
|
|
273
|
+
//# sourceMappingURL=chunk-TVZENGCX.js.map
|
|
@@ -1 +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 { generateSvg } from \"./images/svgGenerator\";\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 shimmer SVG used as src for loading image placeholders */\nconst LOADING_PLACEHOLDER_SRC = `data:image/svg+xml,${encodeURIComponent('<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"800\" height=\"500\" viewBox=\"0 0 800 500\"><rect fill=\"#f3f4f6\" width=\"800\" height=\"500\" rx=\"12\"/><g opacity=\".4\"><rect x=\"320\" y=\"200\" width=\"160\" height=\"4\" rx=\"2\" fill=\"#d1d5db\"><animate attributeName=\"opacity\" values=\".3;.8;.3\" dur=\"1.5s\" repeatCount=\"indefinite\"/></rect><rect x=\"280\" y=\"215\" width=\"240\" height=\"4\" rx=\"2\" fill=\"#d1d5db\"><animate attributeName=\"opacity\" values=\".3;.8;.3\" dur=\"1.5s\" begin=\".3s\" repeatCount=\"indefinite\"/></rect><rect x=\"340\" y=\"230\" width=\"120\" height=\"4\" rx=\"2\" fill=\"#d1d5db\"><animate attributeName=\"opacity\" values=\".3;.8;.3\" dur=\"1.5s\" begin=\".6s\" repeatCount=\"indefinite\"/></rect></g><g transform=\"translate(376,150)\" opacity=\".3\"><path d=\"M0 28V4a4 4 0 014-4h40a4 4 0 014 4v24a4 4 0 01-4 4H4a4 4 0 01-4-4z\" fill=\"#d1d5db\"/><circle cx=\"14\" cy=\"12\" r=\"4\" fill=\"#9ca3af\"/><path d=\"M4 28l10-10 6 6 8-8 16 16H4z\" fill=\"#9ca3af\" opacity=\".5\"/></g></svg>')}`;\n\n/** Inline SVG placeholder for loading charts */\nconst SVG_LOADING_PLACEHOLDER = `<div class=\"w-full h-48 bg-gray-50 rounded-lg flex items-center justify-center animate-pulse\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#9ca3af\" stroke-width=\"1.5\"><rect x=\"3\" y=\"12\" width=\"4\" height=\"9\" rx=\"1\"/><rect x=\"10\" y=\"7\" width=\"4\" height=\"14\" rx=\"1\"/><rect x=\"17\" y=\"3\" width=\"4\" height=\"18\" rx=\"1\"/></svg></div>`;\n\n/** Replace data-svg-chart divs with loading placeholders */\nexport function addSvgLoadingPlaceholders(html: string): string {\n return html.replace(\n /<div\\s([^>]*?)data-svg-chart=\"([^\"]+)\"([^>]*?)>[\\s\\S]*?<\\/div>/gi,\n (_match, before, chart, after) => {\n return `<div ${before}data-svg-chart=\"${chart}\"${after}>${SVG_LOADING_PLACEHOLDER}</div>`;\n }\n );\n}\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_SRC}\" 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 enrichSvgCharts(sectionRef: Section3) {\n const svgRegex = /<div\\s[^>]*data-svg-chart=\"([^\"]+)\"[^>]*>[\\s\\S]*?<\\/div>/gi;\n const svgMatches: { fullMatch: string; prompt: string }[] = [];\n let svgM: RegExpExecArray | null;\n while ((svgM = svgRegex.exec(sectionRef.html)) !== null) {\n svgMatches.push({ fullMatch: svgM[0], prompt: svgM[1] });\n }\n if (svgMatches.length === 0) return;\n\n const anthropicKey = anthropicApiKey || process.env.ANTHROPIC_API_KEY;\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n svgMatches.map(async ({ fullMatch, prompt }) => {\n try {\n const svg = await generateSvg(prompt, anthropicKey);\n return { fullMatch, svg };\n } catch (e) {\n console.warn(`[svg] failed for \"${prompt}\":`, e);\n return { fullMatch, svg: `<div class=\"w-full h-48 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-sm\">${prompt}</div>` };\n }\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n html = html.replace(r.value.fullMatch, r.value.svg);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\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: addSvgLoadingPlaceholders(addLoadingPlaceholders(obj.html)),\n label: obj.label,\n };\n allSections.push(section);\n onSection?.(section);\n enrichSection(section);\n enrichSvgCharts(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":[]}
|
|
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 { generateSvg } from \"./images/svgGenerator\";\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 shimmer SVG used as src for loading image placeholders */\nconst LOADING_PLACEHOLDER_SRC = `data:image/svg+xml,${encodeURIComponent('<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"800\" height=\"500\" viewBox=\"0 0 800 500\"><rect fill=\"#f3f4f6\" width=\"800\" height=\"500\" rx=\"12\"/><g opacity=\".4\"><rect x=\"320\" y=\"200\" width=\"160\" height=\"4\" rx=\"2\" fill=\"#d1d5db\"><animate attributeName=\"opacity\" values=\".3;.8;.3\" dur=\"1.5s\" repeatCount=\"indefinite\"/></rect><rect x=\"280\" y=\"215\" width=\"240\" height=\"4\" rx=\"2\" fill=\"#d1d5db\"><animate attributeName=\"opacity\" values=\".3;.8;.3\" dur=\"1.5s\" begin=\".3s\" repeatCount=\"indefinite\"/></rect><rect x=\"340\" y=\"230\" width=\"120\" height=\"4\" rx=\"2\" fill=\"#d1d5db\"><animate attributeName=\"opacity\" values=\".3;.8;.3\" dur=\"1.5s\" begin=\".6s\" repeatCount=\"indefinite\"/></rect></g><g transform=\"translate(376,150)\" opacity=\".3\"><path d=\"M0 28V4a4 4 0 014-4h40a4 4 0 014 4v24a4 4 0 01-4 4H4a4 4 0 01-4-4z\" fill=\"#d1d5db\"/><circle cx=\"14\" cy=\"12\" r=\"4\" fill=\"#9ca3af\"/><path d=\"M4 28l10-10 6 6 8-8 16 16H4z\" fill=\"#9ca3af\" opacity=\".5\"/></g></svg>')}`;\n\n/** Inline SVG placeholder for loading charts */\nconst SVG_LOADING_PLACEHOLDER = `<div class=\"w-full h-48 bg-gray-50 rounded-lg flex items-center justify-center animate-pulse\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#9ca3af\" stroke-width=\"1.5\"><rect x=\"3\" y=\"12\" width=\"4\" height=\"9\" rx=\"1\"/><rect x=\"10\" y=\"7\" width=\"4\" height=\"14\" rx=\"1\"/><rect x=\"17\" y=\"3\" width=\"4\" height=\"18\" rx=\"1\"/></svg></div>`;\n\n/** Replace data-svg-chart divs with loading placeholders */\nexport function addSvgLoadingPlaceholders(html: string): string {\n return html.replace(\n /<div\\s([^>]*?)data-svg-chart=\"([^\"]+)\"([^>]*?)>[\\s\\S]*?<\\/div>/gi,\n (_match, before, chart, after) => {\n return `<div ${before}data-svg-chart=\"${chart}\"${after}>${SVG_LOADING_PLACEHOLDER}</div>`;\n }\n );\n}\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_SRC}\" 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 enrichSvgCharts(sectionRef: Section3) {\n const svgRegex = /<div\\s[^>]*data-svg-chart=\"([^\"]+)\"[^>]*>[\\s\\S]*?<\\/div>/gi;\n const svgMatches: { fullMatch: string; prompt: string }[] = [];\n let svgM: RegExpExecArray | null;\n while ((svgM = svgRegex.exec(sectionRef.html)) !== null) {\n svgMatches.push({ fullMatch: svgM[0], prompt: svgM[1] });\n }\n if (svgMatches.length === 0) return;\n\n const anthropicKey = anthropicApiKey || process.env.ANTHROPIC_API_KEY;\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n svgMatches.map(async ({ fullMatch, prompt }) => {\n try {\n const svg = await generateSvg(prompt, anthropicKey);\n return { fullMatch, svg };\n } catch (e) {\n console.warn(`[svg] failed for \"${prompt}\":`, e);\n return { fullMatch, svg: `<div class=\"w-full h-48 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-sm\">${prompt}</div>` };\n }\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n html = html.replace(r.value.fullMatch, r.value.svg);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\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 // 1. Pexels first (free, fast)\n if (pexelsApiKey) {\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n }\n // 2. DALL-E fallback\n if (!url && 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 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: addSvgLoadingPlaceholders(addLoadingPlaceholders(obj.html)),\n label: obj.label,\n };\n allSections.push(section);\n onSection?.(section);\n enrichSection(section);\n enrichSvgCharts(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":";;;;;;;;;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,cAAc;AAWvB,eAAsB,aAAa,MAMhC;AACD,QAAM,eAAe,KAAK,mBAAmB,QAAQ,IAAI;AACzD,MAAI,cAAc;AAChB,UAAM,YAAY,gBAAgB,EAAE,QAAQ,aAAa,CAAC;AAC1D,WAAO,UAAU,KAAK,WAAW,KAAK,gBAAgB;AAAA,EACxD;AACA,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,SAAO,gBAAgB,EAAE,KAAK,WAAW,KAAK,gBAAgB;AAChE;AAKO,SAAS,mBAAmB,SAAiE;AAClG,QAAM,QAAQ,QAAQ,MAAM,4BAA4B;AACxD,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO;AAAA,IACL,OAAO,IAAI,WAAW,OAAO,KAAK,MAAM,CAAC,GAAG,QAAQ,CAAC;AAAA,IACrD,UAAU,MAAM,CAAC;AAAA,EACnB;AACF;AAKO,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;AAGA,IAAM,0BAA0B,sBAAsB,mBAAmB,06BAA06B,CAAC;AAGp/B,IAAM,0BAA0B;AAGzB,SAAS,0BAA0B,MAAsB;AAC9D,SAAO,KAAK;AAAA,IACV;AAAA,IACA,CAAC,QAAQ,QAAQ,OAAO,UAAU;AAChC,aAAO,QAAQ,MAAM,mBAAmB,KAAK,IAAI,KAAK,IAAI,uBAAuB;AAAA,IACnF;AAAA,EACF;AACF;AAGO,SAAS,uBAAuB,MAAsB;AAC3D,SAAO,KAAK;AAAA,IACV;AAAA,IACA,CAAC,QAAQ,QAAQ,OAAO,UAAU;AAChC,UAAI,OAAO,SAAS,MAAM,KAAK,MAAM,SAAS,MAAM,EAAG,QAAO;AAC9D,aAAO,GAAG,MAAM,QAAQ,uBAAuB,uBAAuB,KAAK,UAAU,KAAK,IAAI,KAAK;AAAA,IACrG;AAAA,EACF;AACF;AA+BA,eAAsB,eAAe,SAAqD;AACxF,QAAM;AAAA,IACJ;AAAA,IACA,cAAc;AAAA,IACd,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,eAAe,iBAAiB,QAAQ,IAAI;AAClD,QAAM,QAAQ,MAAM,aAAa;AAAA,IAC/B;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,kBAAkB;AAAA,EACpB,CAAC;AAED,QAAM,SAAS,WAAW;AAAA,IACxB;AAAA,IACA,QAAQ;AAAA,IACR,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,YAAY,CAAC;AAAA,EACnD,CAAC;AAED,QAAM,cAA0B,CAAC;AACjC,QAAM,gBAAiC,CAAC;AACxC,MAAI,eAAe;AACnB,MAAI,SAAS;AAEb,WAAS,gBAAgB,YAAsB;AAC7C,UAAM,WAAW;AACjB,UAAM,aAAsD,CAAC;AAC7D,QAAI;AACJ,YAAQ,OAAO,SAAS,KAAK,WAAW,IAAI,OAAO,MAAM;AACvD,iBAAW,KAAK,EAAE,WAAW,KAAK,CAAC,GAAG,QAAQ,KAAK,CAAC,EAAE,CAAC;AAAA,IACzD;AACA,QAAI,WAAW,WAAW,EAAG;AAE7B,UAAM,eAAe,mBAAmB,QAAQ,IAAI;AACpD,kBAAc;AAAA,OACX,YAAY;AACX,cAAM,UAAU,MAAM,QAAQ;AAAA,UAC5B,WAAW,IAAI,OAAO,EAAE,WAAW,OAAO,MAAM;AAC9C,gBAAI;AACF,oBAAM,MAAM,MAAM,YAAY,QAAQ,YAAY;AAClD,qBAAO,EAAE,WAAW,IAAI;AAAA,YAC1B,SAAS,GAAG;AACV,sBAAQ,KAAK,qBAAqB,MAAM,MAAM,CAAC;AAC/C,qBAAO,EAAE,WAAW,KAAK,0GAA0G,MAAM,SAAS;AAAA,YACpJ;AAAA,UACF,CAAC;AAAA,QACH;AACA,YAAI,OAAO,WAAW;AACtB,mBAAW,KAAK,SAAS;AACvB,cAAI,EAAE,WAAW,eAAe,EAAE,OAAO;AACvC,mBAAO,KAAK,QAAQ,EAAE,MAAM,WAAW,EAAE,MAAM,GAAG;AAAA,UACpD;AAAA,QACF;AACA,YAAI,SAAS,WAAW,MAAM;AAC5B,qBAAW,OAAO;AAClB,0BAAgB,WAAW,IAAI,IAAI;AAAA,QACrC;AAAA,MACF,GAAG;AAAA,IACL;AAAA,EACF;AAEA,WAAS,cAAc,YAAsB;AAC3C,UAAM,QAAQ,eAAe,WAAW,IAAI;AAC5C,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,gBAAgB,MAAM,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;AACjD,kBAAc;AAAA,OACX,YAAY;AACX,cAAM,UAAU,MAAM,QAAQ;AAAA,UAC5B,cAAc,IAAI,OAAO,SAAS;AAChC,gBAAI,MAAqB;AAEzB,gBAAI,cAAc;AAChB,oBAAM,MAAM,MAAM,YAAY,KAAK,OAAO,YAAY,EAAE,MAAM,MAAM,IAAI;AACxE,oBAAM,KAAK,OAAO;AAAA,YACpB;AAEA,gBAAI,CAAC,OAAO,cAAc;AACxB,kBAAI;AACF,sBAAM,UAAU,MAAM,cAAc,KAAK,OAAO,YAAY;AAC5D,sBAAM,eAAe,MAAM,aAAa,SAAS,KAAK,KAAK,IAAI;AAAA,cACjE,SAAS,GAAG;AACV,wBAAQ,KAAK,uBAAuB,KAAK,KAAK,MAAM,CAAC;AAAA,cACvD;AAAA,YACF;AACA,oBAAQ,mDAAmD,mBAAmB,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC;AACtG,mBAAO,EAAE,MAAM,IAAI;AAAA,UACrB,CAAC;AAAA,QACH;AACA,YAAI,OAAO,WAAW;AACtB,mBAAW,KAAK,SAAS;AACvB,cAAI,EAAE,WAAW,eAAe,EAAE,OAAO;AACvC,kBAAM,EAAE,MAAM,IAAI,IAAI,EAAE;AACxB,kBAAM,cAAc,KAAK,WAAW,QAAQ,SAAS,GAAG;AACxD,mBAAO,KAAK,WAAW,KAAK,WAAW,WAAW;AAAA,UACpD;AAAA,QACF;AACA,YAAI,SAAS,WAAW,MAAM;AAC5B,qBAAW,OAAO;AAClB,0BAAgB,WAAW,IAAI,IAAI;AAAA,QACrC;AAAA,MACF,GAAG;AAAA,IACL;AAAA,EACF;AAEA,WAAS,cAAc,KAAU;AAC/B,QAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,MAAO;AAC7B,UAAM,UAAoB;AAAA,MACxB,IAAI,OAAO,CAAC;AAAA,MACZ,OAAO;AAAA,MACP,MAAM,0BAA0B,uBAAuB,IAAI,IAAI,CAAC;AAAA,MAChE,OAAO,IAAI;AAAA,IACb;AACA,gBAAY,KAAK,OAAO;AACxB,gBAAY,OAAO;AACnB,kBAAc,OAAO;AACrB,oBAAgB,OAAO;AAAA,EACzB;AAEA,MAAI;AACF,qBAAiB,SAAS,OAAO,YAAY;AAC3C,gBAAU;AACV,YAAM,CAAC,SAAS,SAAS,IAAI,mBAAmB,MAAM;AACtD,eAAS;AACT,iBAAW,OAAO,QAAS,eAAc,GAAG;AAAA,IAC9C;AAGA,QAAI,OAAO,KAAK,GAAG;AACjB,UAAI,UAAU,OAAO,KAAK;AAC1B,UAAI,QAAQ,WAAW,KAAK,GAAG;AAC7B,kBAAU,QAAQ,QAAQ,oBAAoB,EAAE,EAAE,QAAQ,WAAW,EAAE;AAAA,MACzE;AACA,YAAM,CAAC,WAAW,IAAI,mBAAmB,OAAO;AAChD,iBAAW,OAAO,YAAa,eAAc,GAAG;AAAA,IAClD;AAGA,UAAM,QAAQ,WAAW,aAAa;AAGtC,eAAW,WAAW,aAAa;AACjC,YAAM,SAAS,QAAQ;AACvB,cAAQ,OAAO,QAAQ,KAAK;AAAA,QAC1B;AAAA,QACA,CAAC,QAAQ,UAAU;AACjB,gBAAM,WAAW,MAAM,MAAM,gBAAgB;AAC7C,gBAAM,QAAQ,WAAW,CAAC,KAAK;AAC/B,iBAAO,6DAA6D,mBAAmB,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,KAAK;AAAA,QACtH;AAAA,MACF;AACA,cAAQ,OAAO,QAAQ,KAAK;AAAA,QAC1B;AAAA,QACA,CAAC,QAAQ,UAAU;AACjB,iBAAO,wDAAwD,mBAAmB,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC;AAAA,QACvG;AAAA,MACF;AACA,UAAI,QAAQ,SAAS,QAAQ;AAC3B,wBAAgB,QAAQ,IAAI,QAAQ,IAAI;AAAA,MAC1C;AAAA,IACF;AAEA,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":[]}
|
package/dist/generate.js
CHANGED
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
PROMPT_SUFFIX,
|
|
3
3
|
SYSTEM_PROMPT,
|
|
4
4
|
generateLanding
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-LTKQN25A.js";
|
|
6
6
|
import {
|
|
7
7
|
extractJsonObjects
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-TVZENGCX.js";
|
|
9
|
+
import "./chunk-4NOYN34W.js";
|
|
10
10
|
import "./chunk-FM4IJA64.js";
|
|
11
11
|
export {
|
|
12
12
|
PROMPT_SUFFIX,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { S as Section3 } from './types-Flpl4wDs.js';
|
|
2
2
|
|
|
3
|
-
declare const DOCUMENT_SYSTEM_PROMPT = "You are a professional document designer who creates stunning letter-sized (8.5\" \u00D7 11\") document pages using HTML + Tailwind CSS.\n\nRULES:\n- Each page is a <section> element sized for letter paper\n- Page structure: <section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden\">\n- The section itself has NO padding \u2014 backgrounds, gradients, and decorative elements go edge-to-edge\n- For text content, use an inner wrapper: <div class=\"px-[0.75in] py-[0.5in]\">...content...</div>\n- Cover pages and decorative sections can use full-bleed backgrounds (bg-primary, gradients, images that fill the entire page)\n- Content MUST NOT overflow page boundaries \u2014 be conservative with spacing\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import)\n- NO JavaScript, only HTML+Tailwind\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real content from the source material, not Lorem ipsum\n- NOT responsive \u2014 fixed letter size, no breakpoints needed\n- Sections can have ANY background \u2014 full-bleed color, gradients, or white. Not limited to white paper.\n\nSTRICT PROHIBITIONS:\n1. **NO EMOJI** \u2014 Never use emoji characters (\uD83D\uDE80\u274C\u2705\uD83D\uDCCA etc.). Instead use inline SVG icons or colored divs. For bullet decorators use small colored circles (<span class=\"w-2 h-2 rounded-full bg-primary inline-block\"></span>) or simple SVG.\n2. **NO Chart.js / NO JavaScript** \u2014 Never reference Chart.js, canvas, or any JS library. For data visualization use pure CSS: progress bars (div with percentage width + bg-primary), horizontal bars, styled tables with colored cells. Never use <canvas> or <script>.\n3. **NO buttons or CTAs** \u2014 This is a print document, not a web page. No \"Contactar\", \"Ver m\u00E1s\", \"Comprar\" buttons. Use text with contact info instead.\n4. **CONTRAST IS MANDATORY** \u2014 Dark/colored backgrounds (bg-primary, bg-primary-dark, bg-secondary, dark gradients) MUST use text-white or text-on-primary. Light backgrounds (white, bg-surface, bg-surface-alt) MUST use text-gray-900 or text-on-surface. NEVER use dark text on dark backgrounds or light text on light backgrounds.\n5. **Max 2 font weights per page** \u2014 Pick 2 (e.g. font-semibold + font-normal, or font-bold + font-light). Don't mix 4-5 weights.\n6. **Generous whitespace** \u2014 Don't fill every centimeter. Leave breathing room. Use py-8, py-12, gap-6, gap-8 liberally. Less content per page = more professional.\n\nDESIGN:\n- Clean, sophisticated layouts \u2014 think McKinsey reports, annual reports, premium proposals\n- Typography: strong hierarchy with just 2 weights, clear headings vs body\n- Tables: alternating row colors (bg-surface-alt), clean borders, generous cell padding (px-4 py-3)\n- Decorative elements: colored sidebars, header bands, geometric accent shapes (pure CSS divs with bg-primary)\n- First page MUST be a cover/title page with impactful design\n- For numerical data: CSS progress bars, styled tables with colored cells, large stat numbers \u2014 NEVER canvas/charts\n- Icons: use simple inline SVG (12-20px) for visual accents. Keep SVGs minimal (single path, no complex illustrations)\n\nCSS PROGRESS BARS \u2014 use this pattern for data visualization:\n<div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-primary h-3 rounded-full\" style=\"width: 75%\"></div></div>\n\nCOLOR SYSTEM \u2014 use semantic classes:\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- Cover pages should use bold full-bleed backgrounds (bg-primary, gradients from-primary to-primary-dark)\n- CONTRAST: bg-primary/bg-primary-dark/bg-secondary \u2192 text-white or text-on-primary. White/bg-surface \u2192 text-gray-900 or text-on-surface\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\nCHARTS & DATA VISUALIZATION (SVG):\n- For charts, diagrams, and decorative data graphics, use:\n <div data-svg-chart=\"bar chart showing Q1 revenue: Jan $45K, Feb $52K, Mar $61K\" class=\"w-full\"></div>\n- The system generates professional SVG charts with a specialized tool \u2014 NEVER draw SVGs yourself\n- Use descriptive prompts with data points: type of chart + what it shows + actual values\n- Examples: \"donut chart: 40% Marketing, 30% Sales, 20% R&D, 10% Admin\", \"line chart showing growth: Q1 $100K, Q2 $150K, Q3 $220K, Q4 $310K\"\n- For simple metrics, still prefer CSS progress bars (they render faster)\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\nEXAMPLE \u2014 Cover page:\n<section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden bg-white\">\n <div class=\"absolute left-0 top-0 w-
|
|
3
|
+
declare const DOCUMENT_SYSTEM_PROMPT = "You are a professional document designer who creates stunning letter-sized (8.5\" \u00D7 11\") document pages using HTML + Tailwind CSS.\n\nRULES:\n- Each page is a <section> element sized for letter paper\n- Page structure: <section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden\">\n- The section itself has NO padding \u2014 backgrounds, gradients, and decorative elements go edge-to-edge\n- For text content, use an inner wrapper: <div class=\"px-[0.75in] py-[0.5in]\">...content...</div>\n- Cover pages and decorative sections can use full-bleed backgrounds (bg-primary, gradients, images that fill the entire page)\n- Content MUST NOT overflow page boundaries \u2014 be conservative with spacing\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import)\n- NO JavaScript, only HTML+Tailwind\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real content from the source material, not Lorem ipsum\n- NOT responsive \u2014 fixed letter size, no breakpoints needed\n- Sections can have ANY background \u2014 full-bleed color, gradients, or white. Not limited to white paper.\n\nSTRICT PROHIBITIONS:\n1. **NO EMOJI** \u2014 Never use emoji characters (\uD83D\uDE80\u274C\u2705\uD83D\uDCCA etc.). Instead use inline SVG icons or colored divs. For bullet decorators use small colored circles (<span class=\"w-2 h-2 rounded-full bg-primary inline-block\"></span>) or simple SVG.\n2. **NO Chart.js / NO JavaScript** \u2014 Never reference Chart.js, canvas, or any JS library. For data visualization use pure CSS: progress bars (div with percentage width + bg-primary), horizontal bars, styled tables with colored cells. Never use <canvas> or <script>.\n3. **NO buttons or CTAs** \u2014 This is a print document, not a web page. No \"Contactar\", \"Ver m\u00E1s\", \"Comprar\" buttons. Use text with contact info instead.\n4. **CONTRAST IS MANDATORY** \u2014 Dark/colored backgrounds (bg-primary, bg-primary-dark, bg-secondary, dark gradients) MUST use text-white or text-on-primary. Light backgrounds (white, bg-surface, bg-surface-alt) MUST use text-gray-900 or text-on-surface. NEVER use dark text on dark backgrounds or light text on light backgrounds.\n5. **Max 2 font weights per page** \u2014 Pick 2 (e.g. font-semibold + font-normal, or font-bold + font-light). Don't mix 4-5 weights.\n6. **Generous whitespace** \u2014 Don't fill every centimeter. Leave breathing room. Use py-8, py-12, gap-6, gap-8 liberally. Less content per page = more professional.\n\nLAYOUT OVERFLOW PREVENTION \u2014 CRITICAL:\n- Max 2 columns side by side \u2014 each with w-1/2. NEVER use 3+ columns.\n- Decorative sidebars: max w-16 (4rem). NEVER use w-[2.5in] or wider sidebars \u2014 they steal too much space.\n- Stats/metric grids: max 3 items per row (grid-cols-3). Use gap-4 or gap-6.\n- Tables: max 4 columns, use text-xs or text-sm for cell text, px-3 py-2 cell padding.\n- Images: always w-full or max-w-[50%] \u2014 never fixed pixel widths.\n- Text: never use text-6xl or larger except for cover page title. Body text: text-sm or text-base.\n- NEVER use absolute positioning that could overflow \u2014 prefer flex/grid layouts.\n\nDESIGN:\n- Clean, sophisticated layouts \u2014 think McKinsey reports, annual reports, premium proposals\n- Typography: strong hierarchy with just 2 weights, clear headings vs body\n- Tables: alternating row colors (bg-surface-alt), clean borders, generous cell padding (px-4 py-3)\n- Decorative elements: colored sidebars, header bands, geometric accent shapes (pure CSS divs with bg-primary)\n- First page MUST be a cover/title page with impactful design\n- For numerical data: CSS progress bars, styled tables with colored cells, large stat numbers \u2014 NEVER canvas/charts\n- Icons: use simple inline SVG (12-20px) for visual accents. Keep SVGs minimal (single path, no complex illustrations)\n\nCSS PROGRESS BARS \u2014 use this pattern for data visualization:\n<div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-primary h-3 rounded-full\" style=\"width: 75%\"></div></div>\n\nCOLOR SYSTEM \u2014 use semantic classes:\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- Cover pages should use bold full-bleed backgrounds (bg-primary, gradients from-primary to-primary-dark)\n- CONTRAST: bg-primary/bg-primary-dark/bg-secondary \u2192 text-white or text-on-primary. White/bg-surface \u2192 text-gray-900 or text-on-surface\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\nCHARTS & DATA VISUALIZATION (SVG):\n- For charts, diagrams, and decorative data graphics, use:\n <div data-svg-chart=\"bar chart showing Q1 revenue: Jan $45K, Feb $52K, Mar $61K\" class=\"w-full\"></div>\n- The system generates professional SVG charts with a specialized tool \u2014 NEVER draw SVGs yourself\n- Use descriptive prompts with data points: type of chart + what it shows + actual values\n- Examples: \"donut chart: 40% Marketing, 30% Sales, 20% R&D, 10% Admin\", \"line chart showing growth: Q1 $100K, Q2 $150K, Q3 $220K, Q4 $310K\"\n- For simple metrics, still prefer CSS progress bars (they render faster)\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\nEXAMPLE \u2014 Cover page (simple, no wide sidebars):\n<section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden bg-white\">\n <div class=\"absolute left-0 top-0 w-2 h-full bg-primary\"></div>\n <div class=\"flex flex-col justify-center h-[11in] px-[1in]\">\n <div class=\"text-sm font-normal text-primary mb-4\">Marzo 2026 \u00B7 Versi\u00F3n 1.0</div>\n <h1 class=\"text-5xl font-bold text-gray-900 leading-tight\">Reporte<br/>Trimestral</h1>\n <div class=\"w-16 h-1 bg-primary mt-6 mb-4\"></div>\n <p class=\"text-lg font-normal text-gray-500\">Resultados y an\u00E1lisis del primer trimestre</p>\n </div>\n</section>\n\nEXAMPLE \u2014 Content page with table + progress bars:\n<section class=\"w-[8.5in] min-h-[11in] relative overflow-hidden bg-white\">\n <div class=\"h-1.5 bg-primary w-full\"></div>\n <div class=\"px-[0.75in] py-[0.5in]\">\n <h2 class=\"text-2xl font-bold text-gray-900 mb-1\">M\u00E9tricas de Rendimiento</h2>\n <p class=\"text-sm font-normal text-gray-500 mb-8\">Indicadores clave del periodo enero\u2014marzo</p>\n <table class=\"w-full text-sm mb-10\">\n <thead><tr class=\"bg-primary text-white\"><th class=\"px-4 py-3 text-left font-semibold\">Indicador</th><th class=\"px-4 py-3 text-left font-semibold\">Valor</th><th class=\"px-4 py-3 text-left font-semibold\">Meta</th></tr></thead>\n <tbody>\n <tr class=\"bg-surface-alt\"><td class=\"px-4 py-3 text-gray-900\">Ingresos</td><td class=\"px-4 py-3 text-gray-900\">$1.2M</td><td class=\"px-4 py-3 text-gray-900\">$1.5M</td></tr>\n <tr><td class=\"px-4 py-3 text-gray-900\">Clientes nuevos</td><td class=\"px-4 py-3 text-gray-900\">340</td><td class=\"px-4 py-3 text-gray-900\">300</td></tr>\n <tr class=\"bg-surface-alt\"><td class=\"px-4 py-3 text-gray-900\">Retenci\u00F3n</td><td class=\"px-4 py-3 text-gray-900\">92%</td><td class=\"px-4 py-3 text-gray-900\">90%</td></tr>\n </tbody>\n </table>\n <h3 class=\"text-lg font-bold text-gray-900 mb-4\">Progreso por \u00C1rea</h3>\n <div class=\"space-y-4\">\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Ventas</span><span class=\"text-gray-500\">80%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-primary h-3 rounded-full\" style=\"width: 80%\"></div></div></div>\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Marketing</span><span class=\"text-gray-500\">65%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-secondary h-3 rounded-full\" style=\"width: 65%\"></div></div></div>\n <div><div class=\"flex justify-between text-sm mb-1\"><span class=\"text-gray-900 font-semibold\">Producto</span><span class=\"text-gray-500\">95%</span></div><div class=\"w-full bg-gray-200 rounded-full h-3\"><div class=\"bg-accent h-3 rounded-full\" style=\"width: 95%\"></div></div></div>\n </div>\n </div>\n</section>";
|
|
4
4
|
declare const DOCUMENT_PROMPT_SUFFIX = "\n\nOUTPUT FORMAT: NDJSON \u2014 one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Page Title\", \"html\": \"<section class='w-[8.5in] min-h-[11in] relative overflow-hidden'>...</section>\"}\n\nGenerate 3-8 pages depending on content length. First page = cover/title page.\nEach page must fit within letter size (8.5\" \u00D7 11\"). Be conservative with spacing.\nMake each page visually distinct \u2014 different layouts, different accent placements.";
|
|
5
5
|
interface GenerateDocumentOptions {
|
|
6
6
|
anthropicApiKey?: string;
|
package/dist/generateDocument.js
CHANGED
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
DOCUMENT_PROMPT_SUFFIX,
|
|
3
3
|
DOCUMENT_SYSTEM_PROMPT,
|
|
4
4
|
generateDocument
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-6FFDL3TO.js";
|
|
6
|
+
import "./chunk-TVZENGCX.js";
|
|
7
|
+
import "./chunk-4NOYN34W.js";
|
|
8
8
|
import "./chunk-FM4IJA64.js";
|
|
9
9
|
export {
|
|
10
10
|
DOCUMENT_PROMPT_SUFFIX,
|
package/dist/images.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ declare function generateImage(query: string, openaiApiKey: string): Promise<str
|
|
|
38
38
|
declare function generateSvg(prompt: string, anthropicApiKey?: string, options?: {
|
|
39
39
|
width?: number;
|
|
40
40
|
height?: number;
|
|
41
|
+
themeColors?: string;
|
|
41
42
|
}): Promise<string>;
|
|
42
43
|
|
|
43
44
|
export { type EnrichImagesOptions, type PexelsResult, enrichImages, findImageSlots, generateImage, generateSvg, searchImage };
|
package/dist/images.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -10,18 +10,18 @@ import {
|
|
|
10
10
|
PROMPT_SUFFIX,
|
|
11
11
|
SYSTEM_PROMPT,
|
|
12
12
|
generateLanding
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-LTKQN25A.js";
|
|
14
14
|
import {
|
|
15
15
|
DOCUMENT_PROMPT_SUFFIX,
|
|
16
16
|
DOCUMENT_SYSTEM_PROMPT,
|
|
17
17
|
generateDocument
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-6FFDL3TO.js";
|
|
19
19
|
import {
|
|
20
20
|
extractJsonObjects
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-TVZENGCX.js";
|
|
22
22
|
import {
|
|
23
23
|
generateSvg
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-4NOYN34W.js";
|
|
25
25
|
import {
|
|
26
26
|
REFINE_SYSTEM,
|
|
27
27
|
refineLanding
|
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.36",
|
|
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",
|
package/dist/chunk-VCNJLYAQ.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// src/images/svgGenerator.ts
|
|
2
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
|
-
import { generateText } from "ai";
|
|
4
|
-
var SVG_SYSTEM_PROMPT = `You are a professional SVG designer. Generate clean, responsive SVG graphics for documents.
|
|
5
|
-
|
|
6
|
-
RULES:
|
|
7
|
-
- Output ONLY the <svg>...</svg> tag \u2014 no markdown, no explanation, no wrapper
|
|
8
|
-
- Use viewBox for responsiveness (e.g. viewBox="0 0 600 400")
|
|
9
|
-
- Width/height should be 100% to fill container: width="100%" height="100%"
|
|
10
|
-
- Use modern gradients (linearGradient, radialGradient) for professional look
|
|
11
|
-
- Color palette: use the provided theme colors or professional defaults (#6366f1, #8b5cf6, #ec4899, #14b8a6, #f59e0b)
|
|
12
|
-
- Text in SVGs: use font-family="system-ui, sans-serif"
|
|
13
|
-
- Keep SVGs under 3KB when possible \u2014 avoid unnecessary complexity
|
|
14
|
-
- Charts: clean axes, rounded bars/lines, subtle grid lines, data labels
|
|
15
|
-
- Diagrams: clear hierarchy, connecting lines, rounded rectangles, icons as simple paths
|
|
16
|
-
- Decorative: geometric patterns, abstract shapes, gradients
|
|
17
|
-
|
|
18
|
-
CHART TYPES you can generate:
|
|
19
|
-
- Bar charts (vertical/horizontal)
|
|
20
|
-
- Pie/donut charts
|
|
21
|
-
- Line charts
|
|
22
|
-
- Progress/gauge charts
|
|
23
|
-
- Funnel diagrams
|
|
24
|
-
- Simple flowcharts
|
|
25
|
-
- Comparison charts
|
|
26
|
-
- Timeline graphics
|
|
27
|
-
- Stat cards with visual elements
|
|
28
|
-
|
|
29
|
-
IMPORTANT: The SVG must be self-contained (no external references). All styles inline.`;
|
|
30
|
-
async function generateSvg(prompt, anthropicApiKey, options) {
|
|
31
|
-
const apiKey = anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
32
|
-
const anthropic = createAnthropic({ apiKey: apiKey || void 0 });
|
|
33
|
-
const sizeHint = options?.width && options?.height ? ` Target dimensions: ${options.width}x${options.height}px.` : "";
|
|
34
|
-
const result = await generateText({
|
|
35
|
-
model: anthropic("claude-sonnet-4-6"),
|
|
36
|
-
system: SVG_SYSTEM_PROMPT,
|
|
37
|
-
messages: [
|
|
38
|
-
{
|
|
39
|
-
role: "user",
|
|
40
|
-
content: `Generate an SVG for: ${prompt}${sizeHint}`
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
maxOutputTokens: 4e3
|
|
44
|
-
});
|
|
45
|
-
const svgMatch = result.text.match(/<svg[\s\S]*<\/svg>/i);
|
|
46
|
-
if (!svgMatch) {
|
|
47
|
-
throw new Error("SVG generation failed \u2014 no <svg> tag in response");
|
|
48
|
-
}
|
|
49
|
-
return svgMatch[0];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export {
|
|
53
|
-
generateSvg
|
|
54
|
-
};
|
|
55
|
-
//# sourceMappingURL=chunk-VCNJLYAQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/images/svgGenerator.ts"],"sourcesContent":["import { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { generateText } from \"ai\";\n\nconst SVG_SYSTEM_PROMPT = `You are a professional SVG designer. Generate clean, responsive SVG graphics for documents.\n\nRULES:\n- Output ONLY the <svg>...</svg> tag — no markdown, no explanation, no wrapper\n- Use viewBox for responsiveness (e.g. viewBox=\"0 0 600 400\")\n- Width/height should be 100% to fill container: width=\"100%\" height=\"100%\"\n- Use modern gradients (linearGradient, radialGradient) for professional look\n- Color palette: use the provided theme colors or professional defaults (#6366f1, #8b5cf6, #ec4899, #14b8a6, #f59e0b)\n- Text in SVGs: use font-family=\"system-ui, sans-serif\"\n- Keep SVGs under 3KB when possible — avoid unnecessary complexity\n- Charts: clean axes, rounded bars/lines, subtle grid lines, data labels\n- Diagrams: clear hierarchy, connecting lines, rounded rectangles, icons as simple paths\n- Decorative: geometric patterns, abstract shapes, gradients\n\nCHART TYPES you can generate:\n- Bar charts (vertical/horizontal)\n- Pie/donut charts\n- Line charts\n- Progress/gauge charts\n- Funnel diagrams\n- Simple flowcharts\n- Comparison charts\n- Timeline graphics\n- Stat cards with visual elements\n\nIMPORTANT: The SVG must be self-contained (no external references). All styles inline.`;\n\nexport async function generateSvg(\n prompt: string,\n anthropicApiKey?: string,\n options?: { width?: number; height?: number }\n): Promise<string> {\n const apiKey = anthropicApiKey || process.env.ANTHROPIC_API_KEY;\n const anthropic = createAnthropic({ apiKey: apiKey || undefined });\n\n const sizeHint = options?.width && options?.height\n ? ` Target dimensions: ${options.width}x${options.height}px.`\n : \"\";\n\n const result = await generateText({\n model: anthropic(\"claude-sonnet-4-6\"),\n system: SVG_SYSTEM_PROMPT,\n messages: [\n {\n role: \"user\",\n content: `Generate an SVG for: ${prompt}${sizeHint}`,\n },\n ],\n maxOutputTokens: 4000,\n });\n\n // Extract just the SVG tag\n const svgMatch = result.text.match(/<svg[\\s\\S]*<\\/svg>/i);\n if (!svgMatch) {\n throw new Error(\"SVG generation failed — no <svg> tag in response\");\n }\n\n return svgMatch[0];\n}\n"],"mappings":";AAAA,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAE7B,IAAM,oBAAoB;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;AA2B1B,eAAsB,YACpB,QACA,iBACA,SACiB;AACjB,QAAM,SAAS,mBAAmB,QAAQ,IAAI;AAC9C,QAAM,YAAY,gBAAgB,EAAE,QAAQ,UAAU,OAAU,CAAC;AAEjE,QAAM,WAAW,SAAS,SAAS,SAAS,SACxC,uBAAuB,QAAQ,KAAK,IAAI,QAAQ,MAAM,QACtD;AAEJ,QAAM,SAAS,MAAM,aAAa;AAAA,IAChC,OAAO,UAAU,mBAAmB;AAAA,IACpC,QAAQ;AAAA,IACR,UAAU;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN,SAAS,wBAAwB,MAAM,GAAG,QAAQ;AAAA,MACpD;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,EACnB,CAAC;AAGD,QAAM,WAAW,OAAO,KAAK,MAAM,qBAAqB;AACxD,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,uDAAkD;AAAA,EACpE;AAEA,SAAO,SAAS,CAAC;AACnB;","names":[]}
|
|
File without changes
|