@easybits.cloud/html-tailwind-generator 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,12 +6,12 @@ Built and maintained by [EasyBits](https://easybits.cloud).
6
6
 
7
7
  ## Features
8
8
 
9
- - **AI Generation** — Streaming landing page creation with Claude (Sonnet for generation, Haiku for refinement)
9
+ - **AI Generation** — Streaming landing page creation with Claude or OpenAI GPT-4o
10
10
  - **Canvas Editor** — iframe-based preview with click-to-select, inline text editing, section reorder
11
11
  - **Floating Toolbar** — AI prompt input, style presets (Minimal, Cards, Bold, Glass, Dark), reference image support
12
12
  - **Code Editor** — CodeMirror 6 with HTML syntax, flash highlight on scroll-to-code, format, Cmd+S
13
13
  - **Theme System** — 5 preset themes (Neutral, Dark, Slate, Midnight, Warm) + custom multi-color picker
14
- - **Image Enrichment** — Auto-replace placeholder images with Pexels stock photos
14
+ - **Image Enrichment** — Auto-replace placeholder images with Pexels stock photos or DALL-E generated images
15
15
  - **Deploy** — To EasyBits hosting (`slug.easybits.cloud`) or any S3-compatible storage
16
16
 
17
17
  ## Install
@@ -20,16 +20,41 @@ Built and maintained by [EasyBits](https://easybits.cloud).
20
20
  npm install @easybits.cloud/html-tailwind-generator
21
21
  ```
22
22
 
23
+ ## Environment Variables
24
+
25
+ All API keys can be set via environment variables instead of passing them explicitly:
26
+
27
+ | Variable | Used by | Description |
28
+ |----------|---------|-------------|
29
+ | `OPENAI_API_KEY` | `generateLanding`, `refineLanding`, DALL-E images | OpenAI API key — enables GPT-4o generation + DALL-E 3 images |
30
+ | `ANTHROPIC_API_KEY` | `generateLanding`, `refineLanding` | Anthropic API key (auto-read by `@ai-sdk/anthropic`) |
31
+ | `PEXELS_API_KEY` | `enrichImages`, `searchImage` | Pexels stock photo API key |
32
+
33
+ **Priority**: If both `OPENAI_API_KEY` and `ANTHROPIC_API_KEY` are set, OpenAI takes precedence. To force Anthropic, pass `anthropicApiKey` explicitly and omit `openaiApiKey`.
34
+
23
35
  ## Quick Start
24
36
 
25
37
  ### Generate a landing page (server-side)
26
38
 
39
+ The simplest usage — set either `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` (plus `PEXELS_API_KEY` for stock photos) in your `.env`:
40
+
27
41
  ```ts
28
42
  import { generateLanding } from "@easybits.cloud/html-tailwind-generator/generate";
29
43
 
44
+ const sections = await generateLanding({
45
+ prompt: "SaaS de gestión de proyectos para equipos remotos",
46
+ onSection(section) {
47
+ console.log("New section:", section.label);
48
+ },
49
+ });
50
+ ```
51
+
52
+ You can also pass keys explicitly if you prefer:
53
+
54
+ ```ts
30
55
  const sections = await generateLanding({
31
56
  anthropicApiKey: "sk-ant-...",
32
- pexelsApiKey: "...", // optional, for stock photos
57
+ pexelsApiKey: "...",
33
58
  prompt: "SaaS de gestión de proyectos para equipos remotos",
34
59
  onSection(section) {
35
60
  console.log("New section:", section.label);
@@ -38,17 +63,33 @@ const sections = await generateLanding({
38
63
  console.log("Images enriched for", id);
39
64
  },
40
65
  });
66
+ ```
67
+
68
+ ### Generate with OpenAI + DALL-E
41
69
 
42
- console.log(`Generated ${sections.length} sections`);
70
+ Pass `openaiApiKey` to use GPT-4o for text and DALL-E 3 for images (one key for everything):
71
+
72
+ ```ts
73
+ const sections = await generateLanding({
74
+ openaiApiKey: "sk-...",
75
+ prompt: "SaaS de gestión de proyectos para equipos remotos",
76
+ onSection(section) {
77
+ console.log("New section:", section.label);
78
+ },
79
+ });
43
80
  ```
44
81
 
82
+ When `openaiApiKey` is provided:
83
+ - **Generation** uses GPT-4o (or custom `model`)
84
+ - **Refinement** uses GPT-4o-mini (GPT-4o when `referenceImage` is provided)
85
+ - **Images** use DALL-E 3 with Pexels as fallback
86
+
45
87
  ### Refine a section
46
88
 
47
89
  ```ts
48
90
  import { refineLanding } from "@easybits.cloud/html-tailwind-generator/refine";
49
91
 
50
92
  const html = await refineLanding({
51
- anthropicApiKey: "sk-ant-...",
52
93
  currentHtml: sections[0].html,
53
94
  instruction: "Make it more minimal with more whitespace",
54
95
  onChunk(accumulated) {
@@ -132,14 +173,16 @@ const url = await deployToS3({
132
173
 
133
174
  ## Exports
134
175
 
135
- | Path | Description |
136
- |------|-------------|
137
- | `@easybits.cloud/html-tailwind-generator` | Everything (types, themes, builders, generate, refine, deploy, images, components) |
138
- | `@easybits.cloud/html-tailwind-generator/generate` | `generateLanding`, `extractJsonObjects`, `SYSTEM_PROMPT` |
139
- | `@easybits.cloud/html-tailwind-generator/refine` | `refineLanding`, `REFINE_SYSTEM` |
140
- | `@easybits.cloud/html-tailwind-generator/deploy` | `deployToEasyBits`, `deployToS3` |
141
- | `@easybits.cloud/html-tailwind-generator/images` | `searchImage`, `enrichImages`, `findImageSlots` |
142
- | `@easybits.cloud/html-tailwind-generator/components` | `Canvas`, `SectionList`, `FloatingToolbar`, `CodeEditor` |
176
+ | Path | Exports |
177
+ |------|---------|
178
+ | `@easybits.cloud/html-tailwind-generator` | Everything below (re-exported for convenience) |
179
+ | `@easybits.cloud/html-tailwind-generator/generate` | `generateLanding`, `extractJsonObjects`, `SYSTEM_PROMPT`, `PROMPT_SUFFIX`, type `GenerateOptions` |
180
+ | `@easybits.cloud/html-tailwind-generator/refine` | `refineLanding`, `REFINE_SYSTEM`, type `RefineOptions` |
181
+ | `@easybits.cloud/html-tailwind-generator/deploy` | `deployToEasyBits`, `deployToS3`, types `DeployToS3Options`, `DeployToEasyBitsOptions` |
182
+ | `@easybits.cloud/html-tailwind-generator/images` | `searchImage`, `enrichImages`, `findImageSlots`, `generateImage`, type `PexelsResult` |
183
+ | `@easybits.cloud/html-tailwind-generator/components` | `Canvas`, `SectionList`, `FloatingToolbar`, `CodeEditor`, type `CanvasHandle` |
184
+
185
+ Types also exported from the root path: `Section3`, `IframeMessage`, `LandingTheme`, `CustomColors`.
143
186
 
144
187
  ## Theme System
145
188
 
@@ -156,18 +199,24 @@ The generator uses a semantic color system with CSS custom properties:
156
199
  **Required:**
157
200
  - `react` >= 18
158
201
  - `ai` >= 4 (Vercel AI SDK)
159
- - `@ai-sdk/anthropic` >= 3
202
+ - `@ai-sdk/anthropic` >= 3 (for Claude)
203
+
204
+ **Optional (for OpenAI support):**
205
+ - `@ai-sdk/openai` >= 1
160
206
 
161
207
  **Optional (for editor components):**
162
208
  - `react-dom`, `react-icons`
163
- - `@codemirror/*` packages (for CodeEditor)
209
+ - CodeMirror packages (required if you use `CodeEditor`):
210
+ ```bash
211
+ npm install @codemirror/lang-html @codemirror/state @codemirror/theme-one-dark @codemirror/view @codemirror/commands @codemirror/search @codemirror/language @codemirror/autocomplete
212
+ ```
164
213
 
165
214
  ## TODO
166
215
 
167
216
  > These are planned improvements — contributions welcome for noncommercial use.
168
217
 
169
218
  - [ ] **Inline Tailwind CSS build** — Replace CDN `<script src="tailwindcss.com">` with `@tailwindcss/standalone` or PostCSS to generate only used CSS as `<style>`. Faster load, no external dependency, production-ready.
170
- - [ ] **DALL-E image generation** — Add `openaiApiKey` option to generate unique images instead of Pexels stock. Sections with `data-image-query` could use AI-generated images for cases where stock photos don't fit.
219
+ - [x] **DALL-E image generation** — `openaiApiKey` option generates unique images via DALL-E 3 with Pexels fallback.
171
220
  - [ ] **tsup build** — Add a proper build step (ESM + CJS + types) for npm publish. Currently exported as raw TypeScript source, which works for monorepo consumers but not for external npm users.
172
221
  - [ ] **i18n** — Component labels are in Spanish. Add a `locale` prop or i18n system for English and other languages.
173
222
  - [ ] **Tests** — Unit tests for `extractJsonObjects`, `findImageSlots`, `buildDeployHtml`, `buildCustomTheme`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/html-tailwind-generator",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -22,6 +22,7 @@
22
22
  "react-dom": ">=18",
23
23
  "ai": ">=4",
24
24
  "@ai-sdk/anthropic": ">=3",
25
+ "@ai-sdk/openai": ">=1",
25
26
  "@codemirror/lang-html": ">=6",
26
27
  "@codemirror/state": ">=6",
27
28
  "@codemirror/theme-one-dark": ">=6",
@@ -33,16 +34,39 @@
33
34
  "react-icons": ">=5"
34
35
  },
35
36
  "peerDependenciesMeta": {
36
- "@codemirror/lang-html": { "optional": true },
37
- "@codemirror/state": { "optional": true },
38
- "@codemirror/theme-one-dark": { "optional": true },
39
- "@codemirror/view": { "optional": true },
40
- "@codemirror/commands": { "optional": true },
41
- "@codemirror/search": { "optional": true },
42
- "@codemirror/language": { "optional": true },
43
- "@codemirror/autocomplete": { "optional": true },
44
- "react-icons": { "optional": true },
45
- "react-dom": { "optional": true }
37
+ "@codemirror/lang-html": {
38
+ "optional": true
39
+ },
40
+ "@codemirror/state": {
41
+ "optional": true
42
+ },
43
+ "@codemirror/theme-one-dark": {
44
+ "optional": true
45
+ },
46
+ "@codemirror/view": {
47
+ "optional": true
48
+ },
49
+ "@codemirror/commands": {
50
+ "optional": true
51
+ },
52
+ "@codemirror/search": {
53
+ "optional": true
54
+ },
55
+ "@codemirror/language": {
56
+ "optional": true
57
+ },
58
+ "@codemirror/autocomplete": {
59
+ "optional": true
60
+ },
61
+ "react-icons": {
62
+ "optional": true
63
+ },
64
+ "@ai-sdk/openai": {
65
+ "optional": true
66
+ },
67
+ "react-dom": {
68
+ "optional": true
69
+ }
46
70
  },
47
71
  "dependencies": {
48
72
  "nanoid": "^5.1.5"
package/src/generate.ts CHANGED
@@ -3,8 +3,22 @@ import { createAnthropic } from "@ai-sdk/anthropic";
3
3
  import { nanoid } from "nanoid";
4
4
  import { findImageSlots } from "./images/enrichImages";
5
5
  import { searchImage } from "./images/pexels";
6
+ import { generateImage } from "./images/dalleImages";
6
7
  import type { Section3 } from "./types";
7
8
 
9
+ function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {
10
+ const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;
11
+ if (openaiKey) {
12
+ const { createOpenAI } = require("@ai-sdk/openai");
13
+ const openai = createOpenAI({ apiKey: openaiKey });
14
+ return openai(opts.modelId || opts.defaultOpenai);
15
+ }
16
+ const anthropic = opts.anthropicApiKey
17
+ ? createAnthropic({ apiKey: opts.anthropicApiKey })
18
+ : createAnthropic();
19
+ return anthropic(opts.modelId || opts.defaultAnthropic);
20
+ }
21
+
8
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.
9
23
 
10
24
  RULES:
@@ -113,6 +127,8 @@ export function extractJsonObjects(text: string): [any[], string] {
113
127
  export interface GenerateOptions {
114
128
  /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
115
129
  anthropicApiKey?: string;
130
+ /** OpenAI API key. If provided, uses GPT-4o instead of Claude */
131
+ openaiApiKey?: string;
116
132
  /** Landing page description prompt */
117
133
  prompt: string;
118
134
  /** Reference image (base64 data URI) for vision-based generation */
@@ -121,7 +137,7 @@ export interface GenerateOptions {
121
137
  extraInstructions?: string;
122
138
  /** Custom system prompt (overrides default SYSTEM_PROMPT) */
123
139
  systemPrompt?: string;
124
- /** Model ID (default: claude-sonnet-4-6) */
140
+ /** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */
125
141
  model?: string;
126
142
  /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
127
143
  pexelsApiKey?: string;
@@ -142,11 +158,12 @@ export interface GenerateOptions {
142
158
  export async function generateLanding(options: GenerateOptions): Promise<Section3[]> {
143
159
  const {
144
160
  anthropicApiKey,
161
+ openaiApiKey: _openaiApiKey,
145
162
  prompt,
146
163
  referenceImage,
147
164
  extraInstructions,
148
165
  systemPrompt = SYSTEM_PROMPT,
149
- model: modelId = "claude-sonnet-4-6",
166
+ model: modelId,
150
167
  pexelsApiKey,
151
168
  onSection,
152
169
  onImageUpdate,
@@ -154,11 +171,8 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
154
171
  onError,
155
172
  } = options;
156
173
 
157
- const anthropic = anthropicApiKey
158
- ? createAnthropic({ apiKey: anthropicApiKey })
159
- : createAnthropic();
160
-
161
- const model = anthropic(modelId);
174
+ const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;
175
+ const model = resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: "gpt-4o", defaultAnthropic: "claude-sonnet-4-6" });
162
176
 
163
177
  // Build prompt content (supports multimodal with reference image)
164
178
  const extra = extraInstructions ? `\nAdditional instructions: ${extraInstructions}` : "";
@@ -207,7 +221,7 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
207
221
  allSections.push(section);
208
222
  onSection?.(section);
209
223
 
210
- // Enrich images with Pexels (non-blocking per section)
224
+ // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)
211
225
  const slots = findImageSlots(section.html);
212
226
  if (slots.length > 0) {
213
227
  const sectionRef = section;
@@ -216,8 +230,15 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
216
230
  (async () => {
217
231
  const results = await Promise.allSettled(
218
232
  slotsSnapshot.map(async (slot) => {
219
- const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
220
- const url = img?.url || `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
233
+ let url: string | null = null;
234
+ if (openaiApiKey) {
235
+ url = await generateImage(slot.query, openaiApiKey).catch(() => null);
236
+ }
237
+ if (!url) {
238
+ const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
239
+ url = img?.url || null;
240
+ }
241
+ url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
221
242
  return { slot, url };
222
243
  })
223
244
  );
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Generate an image using DALL-E 3 API.
3
+ */
4
+ export async function generateImage(
5
+ query: string,
6
+ openaiApiKey: string
7
+ ): Promise<string> {
8
+ const res = await fetch("https://api.openai.com/v1/images/generations", {
9
+ method: "POST",
10
+ headers: {
11
+ Authorization: `Bearer ${openaiApiKey}`,
12
+ "Content-Type": "application/json",
13
+ },
14
+ body: JSON.stringify({
15
+ model: "dall-e-3",
16
+ prompt: query,
17
+ n: 1,
18
+ size: "1792x1024",
19
+ }),
20
+ });
21
+
22
+ if (!res.ok) {
23
+ const err = await res.text().catch(() => "Unknown error");
24
+ throw new Error(`DALL-E API error ${res.status}: ${err}`);
25
+ }
26
+
27
+ const data = await res.json();
28
+ return data.data[0].url;
29
+ }
@@ -1,4 +1,5 @@
1
1
  import { searchImage } from "./pexels";
2
+ import { generateImage } from "./dalleImages";
2
3
 
3
4
  interface ImageMatch {
4
5
  query: string;
@@ -102,14 +103,21 @@ export function findImageSlots(html: string): ImageMatch[] {
102
103
  /**
103
104
  * Enrich all images in an HTML string with Pexels photos.
104
105
  */
105
- export async function enrichImages(html: string, pexelsApiKey?: string): Promise<string> {
106
+ export async function enrichImages(html: string, pexelsApiKey?: string, openaiApiKey?: string): Promise<string> {
106
107
  const slots = findImageSlots(html);
107
108
  if (slots.length === 0) return html;
108
109
 
109
110
  let result = html;
110
111
  const promises = slots.map(async (slot) => {
111
- const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
112
- const url = img?.url || `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
112
+ let url: string | null = null;
113
+ if (openaiApiKey) {
114
+ url = await generateImage(slot.query, openaiApiKey).catch(() => null);
115
+ }
116
+ if (!url) {
117
+ const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
118
+ url = img?.url || null;
119
+ }
120
+ url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
113
121
  const replacement = slot.replaceStr.replace("{url}", url);
114
122
  result = result.replaceAll(slot.searchStr, replacement);
115
123
  });
@@ -1,2 +1,3 @@
1
1
  export { searchImage, type PexelsResult } from "./pexels";
2
2
  export { enrichImages, findImageSlots } from "./enrichImages";
3
+ export { generateImage } from "./dalleImages";
package/src/index.ts CHANGED
@@ -44,6 +44,7 @@ export {
44
44
  searchImage,
45
45
  enrichImages,
46
46
  findImageSlots,
47
+ generateImage,
47
48
  type PexelsResult,
48
49
  } from "./images/index";
49
50
 
package/src/refine.ts CHANGED
@@ -2,6 +2,19 @@ import { streamText } from "ai";
2
2
  import { createAnthropic } from "@ai-sdk/anthropic";
3
3
  import { enrichImages } from "./images/enrichImages";
4
4
 
5
+ function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {
6
+ const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;
7
+ if (openaiKey) {
8
+ const { createOpenAI } = require("@ai-sdk/openai");
9
+ const openai = createOpenAI({ apiKey: openaiKey });
10
+ return openai(opts.modelId || opts.defaultOpenai);
11
+ }
12
+ const anthropic = opts.anthropicApiKey
13
+ ? createAnthropic({ apiKey: opts.anthropicApiKey })
14
+ : createAnthropic();
15
+ return anthropic(opts.modelId || opts.defaultAnthropic);
16
+ }
17
+
5
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.
6
19
 
7
20
  RULES:
@@ -26,6 +39,8 @@ TAILWIND v3 NOTES:
26
39
  export interface RefineOptions {
27
40
  /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
28
41
  anthropicApiKey?: string;
42
+ /** OpenAI API key. If provided, uses GPT-4o-mini instead of Claude */
43
+ openaiApiKey?: string;
29
44
  /** Current HTML of the section being refined */
30
45
  currentHtml: string;
31
46
  /** User instruction for refinement */
@@ -34,7 +49,7 @@ export interface RefineOptions {
34
49
  referenceImage?: string;
35
50
  /** Custom system prompt (overrides default REFINE_SYSTEM) */
36
51
  systemPrompt?: string;
37
- /** Model ID (default: claude-haiku-4-5-20251001, claude-sonnet-4-6 when referenceImage is provided) */
52
+ /** Model ID (default: gpt-4o-mini/gpt-4o for OpenAI, claude-haiku/claude-sonnet for Anthropic) */
38
53
  model?: string;
39
54
  /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
40
55
  pexelsApiKey?: string;
@@ -53,6 +68,7 @@ export interface RefineOptions {
53
68
  export async function refineLanding(options: RefineOptions): Promise<string> {
54
69
  const {
55
70
  anthropicApiKey,
71
+ openaiApiKey: _openaiApiKey,
56
72
  currentHtml,
57
73
  instruction,
58
74
  referenceImage,
@@ -64,13 +80,10 @@ export async function refineLanding(options: RefineOptions): Promise<string> {
64
80
  onError,
65
81
  } = options;
66
82
 
67
- const anthropic = anthropicApiKey
68
- ? createAnthropic({ apiKey: anthropicApiKey })
69
- : createAnthropic();
70
-
71
- // Use Haiku for speed, Sonnet for vision
72
- const defaultModel = referenceImage ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001";
73
- const model = anthropic(modelId || defaultModel);
83
+ const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;
84
+ const defaultOpenai = referenceImage ? "gpt-4o" : "gpt-4o-mini";
85
+ const defaultAnthropic = referenceImage ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001";
86
+ const model = resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai, defaultAnthropic });
74
87
 
75
88
  // Build content (supports multimodal with reference image)
76
89
  const content: any[] = [];
@@ -102,8 +115,8 @@ export async function refineLanding(options: RefineOptions): Promise<string> {
102
115
  html = html.replace(/^```(?:html|xml)?\s*/, "").replace(/\s*```$/, "");
103
116
  }
104
117
 
105
- // Enrich images
106
- html = await enrichImages(html, pexelsApiKey);
118
+ // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)
119
+ html = await enrichImages(html, pexelsApiKey, openaiApiKey);
107
120
 
108
121
  onDone?.(html);
109
122
  return html;