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

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,40 @@ 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
+ | `ANTHROPIC_API_KEY` | `generateLanding`, `refineLanding` | Anthropic API key (auto-read by `@ai-sdk/anthropic`) |
30
+ | `PEXELS_API_KEY` | `enrichImages`, `searchImage` | Pexels stock photo API key |
31
+
32
+ OpenAI keys must be passed explicitly via `openaiApiKey` — there is no env var fallback (to avoid accidentally mixing providers).
33
+
23
34
  ## Quick Start
24
35
 
25
36
  ### Generate a landing page (server-side)
26
37
 
38
+ The simplest usage — just set `ANTHROPIC_API_KEY` and `PEXELS_API_KEY` in your `.env`:
39
+
27
40
  ```ts
28
41
  import { generateLanding } from "@easybits.cloud/html-tailwind-generator/generate";
29
42
 
43
+ const sections = await generateLanding({
44
+ prompt: "SaaS de gestión de proyectos para equipos remotos",
45
+ onSection(section) {
46
+ console.log("New section:", section.label);
47
+ },
48
+ });
49
+ ```
50
+
51
+ You can also pass keys explicitly if you prefer:
52
+
53
+ ```ts
30
54
  const sections = await generateLanding({
31
55
  anthropicApiKey: "sk-ant-...",
32
- pexelsApiKey: "...", // optional, for stock photos
56
+ pexelsApiKey: "...",
33
57
  prompt: "SaaS de gestión de proyectos para equipos remotos",
34
58
  onSection(section) {
35
59
  console.log("New section:", section.label);
@@ -38,17 +62,33 @@ const sections = await generateLanding({
38
62
  console.log("Images enriched for", id);
39
63
  },
40
64
  });
65
+ ```
66
+
67
+ ### Generate with OpenAI + DALL-E
41
68
 
42
- console.log(`Generated ${sections.length} sections`);
69
+ Pass `openaiApiKey` to use GPT-4o for text and DALL-E 3 for images (one key for everything):
70
+
71
+ ```ts
72
+ const sections = await generateLanding({
73
+ openaiApiKey: "sk-...",
74
+ prompt: "SaaS de gestión de proyectos para equipos remotos",
75
+ onSection(section) {
76
+ console.log("New section:", section.label);
77
+ },
78
+ });
43
79
  ```
44
80
 
81
+ When `openaiApiKey` is provided:
82
+ - **Generation** uses GPT-4o (or custom `model`)
83
+ - **Refinement** uses GPT-4o-mini (GPT-4o when `referenceImage` is provided)
84
+ - **Images** use DALL-E 3 with Pexels as fallback
85
+
45
86
  ### Refine a section
46
87
 
47
88
  ```ts
48
89
  import { refineLanding } from "@easybits.cloud/html-tailwind-generator/refine";
49
90
 
50
91
  const html = await refineLanding({
51
- anthropicApiKey: "sk-ant-...",
52
92
  currentHtml: sections[0].html,
53
93
  instruction: "Make it more minimal with more whitespace",
54
94
  onChunk(accumulated) {
@@ -132,14 +172,16 @@ const url = await deployToS3({
132
172
 
133
173
  ## Exports
134
174
 
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` |
175
+ | Path | Exports |
176
+ |------|---------|
177
+ | `@easybits.cloud/html-tailwind-generator` | Everything below (re-exported for convenience) |
178
+ | `@easybits.cloud/html-tailwind-generator/generate` | `generateLanding`, `extractJsonObjects`, `SYSTEM_PROMPT`, `PROMPT_SUFFIX`, type `GenerateOptions` |
179
+ | `@easybits.cloud/html-tailwind-generator/refine` | `refineLanding`, `REFINE_SYSTEM`, type `RefineOptions` |
180
+ | `@easybits.cloud/html-tailwind-generator/deploy` | `deployToEasyBits`, `deployToS3`, types `DeployToS3Options`, `DeployToEasyBitsOptions` |
181
+ | `@easybits.cloud/html-tailwind-generator/images` | `searchImage`, `enrichImages`, `findImageSlots`, `generateImage`, type `PexelsResult` |
182
+ | `@easybits.cloud/html-tailwind-generator/components` | `Canvas`, `SectionList`, `FloatingToolbar`, `CodeEditor`, type `CanvasHandle` |
183
+
184
+ Types also exported from the root path: `Section3`, `IframeMessage`, `LandingTheme`, `CustomColors`.
143
185
 
144
186
  ## Theme System
145
187
 
@@ -156,18 +198,24 @@ The generator uses a semantic color system with CSS custom properties:
156
198
  **Required:**
157
199
  - `react` >= 18
158
200
  - `ai` >= 4 (Vercel AI SDK)
159
- - `@ai-sdk/anthropic` >= 3
201
+ - `@ai-sdk/anthropic` >= 3 (for Claude)
202
+
203
+ **Optional (for OpenAI support):**
204
+ - `@ai-sdk/openai` >= 1
160
205
 
161
206
  **Optional (for editor components):**
162
207
  - `react-dom`, `react-icons`
163
- - `@codemirror/*` packages (for CodeEditor)
208
+ - CodeMirror packages (required if you use `CodeEditor`):
209
+ ```bash
210
+ npm install @codemirror/lang-html @codemirror/state @codemirror/theme-one-dark @codemirror/view @codemirror/commands @codemirror/search @codemirror/language @codemirror/autocomplete
211
+ ```
164
212
 
165
213
  ## TODO
166
214
 
167
215
  > These are planned improvements — contributions welcome for noncommercial use.
168
216
 
169
217
  - [ ] **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.
218
+ - [x] **DALL-E image generation** — `openaiApiKey` option generates unique images via DALL-E 3 with Pexels fallback.
171
219
  - [ ] **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
220
  - [ ] **i18n** — Component labels are in Spanish. Add a `locale` prop or i18n system for English and other languages.
173
221
  - [ ] **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.1",
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",
@@ -42,6 +43,7 @@
42
43
  "@codemirror/language": { "optional": true },
43
44
  "@codemirror/autocomplete": { "optional": true },
44
45
  "react-icons": { "optional": true },
46
+ "@ai-sdk/openai": { "optional": true },
45
47
  "react-dom": { "optional": true }
46
48
  },
47
49
  "dependencies": {
package/src/generate.ts CHANGED
@@ -3,8 +3,21 @@ 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
+ if (opts.openaiApiKey) {
11
+ const { createOpenAI } = require("@ai-sdk/openai");
12
+ const openai = createOpenAI({ apiKey: opts.openaiApiKey });
13
+ return openai(opts.modelId || opts.defaultOpenai);
14
+ }
15
+ const anthropic = opts.anthropicApiKey
16
+ ? createAnthropic({ apiKey: opts.anthropicApiKey })
17
+ : createAnthropic();
18
+ return anthropic(opts.modelId || opts.defaultAnthropic);
19
+ }
20
+
8
21
  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
22
 
10
23
  RULES:
@@ -113,6 +126,8 @@ export function extractJsonObjects(text: string): [any[], string] {
113
126
  export interface GenerateOptions {
114
127
  /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
115
128
  anthropicApiKey?: string;
129
+ /** OpenAI API key. If provided, uses GPT-4o instead of Claude */
130
+ openaiApiKey?: string;
116
131
  /** Landing page description prompt */
117
132
  prompt: string;
118
133
  /** Reference image (base64 data URI) for vision-based generation */
@@ -121,7 +136,7 @@ export interface GenerateOptions {
121
136
  extraInstructions?: string;
122
137
  /** Custom system prompt (overrides default SYSTEM_PROMPT) */
123
138
  systemPrompt?: string;
124
- /** Model ID (default: claude-sonnet-4-6) */
139
+ /** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */
125
140
  model?: string;
126
141
  /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
127
142
  pexelsApiKey?: string;
@@ -142,11 +157,12 @@ export interface GenerateOptions {
142
157
  export async function generateLanding(options: GenerateOptions): Promise<Section3[]> {
143
158
  const {
144
159
  anthropicApiKey,
160
+ openaiApiKey,
145
161
  prompt,
146
162
  referenceImage,
147
163
  extraInstructions,
148
164
  systemPrompt = SYSTEM_PROMPT,
149
- model: modelId = "claude-sonnet-4-6",
165
+ model: modelId,
150
166
  pexelsApiKey,
151
167
  onSection,
152
168
  onImageUpdate,
@@ -154,11 +170,7 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
154
170
  onError,
155
171
  } = options;
156
172
 
157
- const anthropic = anthropicApiKey
158
- ? createAnthropic({ apiKey: anthropicApiKey })
159
- : createAnthropic();
160
-
161
- const model = anthropic(modelId);
173
+ const model = resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: "gpt-4o", defaultAnthropic: "claude-sonnet-4-6" });
162
174
 
163
175
  // Build prompt content (supports multimodal with reference image)
164
176
  const extra = extraInstructions ? `\nAdditional instructions: ${extraInstructions}` : "";
@@ -207,7 +219,7 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
207
219
  allSections.push(section);
208
220
  onSection?.(section);
209
221
 
210
- // Enrich images with Pexels (non-blocking per section)
222
+ // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)
211
223
  const slots = findImageSlots(section.html);
212
224
  if (slots.length > 0) {
213
225
  const sectionRef = section;
@@ -216,8 +228,15 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
216
228
  (async () => {
217
229
  const results = await Promise.allSettled(
218
230
  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))}`;
231
+ let url: string | null = null;
232
+ if (openaiApiKey) {
233
+ url = await generateImage(slot.query, openaiApiKey).catch(() => null);
234
+ }
235
+ if (!url) {
236
+ const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
237
+ url = img?.url || null;
238
+ }
239
+ url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
221
240
  return { slot, url };
222
241
  })
223
242
  );
@@ -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,18 @@ 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
+ if (opts.openaiApiKey) {
7
+ const { createOpenAI } = require("@ai-sdk/openai");
8
+ const openai = createOpenAI({ apiKey: opts.openaiApiKey });
9
+ return openai(opts.modelId || opts.defaultOpenai);
10
+ }
11
+ const anthropic = opts.anthropicApiKey
12
+ ? createAnthropic({ apiKey: opts.anthropicApiKey })
13
+ : createAnthropic();
14
+ return anthropic(opts.modelId || opts.defaultAnthropic);
15
+ }
16
+
5
17
  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
18
 
7
19
  RULES:
@@ -26,6 +38,8 @@ TAILWIND v3 NOTES:
26
38
  export interface RefineOptions {
27
39
  /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
28
40
  anthropicApiKey?: string;
41
+ /** OpenAI API key. If provided, uses GPT-4o-mini instead of Claude */
42
+ openaiApiKey?: string;
29
43
  /** Current HTML of the section being refined */
30
44
  currentHtml: string;
31
45
  /** User instruction for refinement */
@@ -34,7 +48,7 @@ export interface RefineOptions {
34
48
  referenceImage?: string;
35
49
  /** Custom system prompt (overrides default REFINE_SYSTEM) */
36
50
  systemPrompt?: string;
37
- /** Model ID (default: claude-haiku-4-5-20251001, claude-sonnet-4-6 when referenceImage is provided) */
51
+ /** Model ID (default: gpt-4o-mini/gpt-4o for OpenAI, claude-haiku/claude-sonnet for Anthropic) */
38
52
  model?: string;
39
53
  /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
40
54
  pexelsApiKey?: string;
@@ -53,6 +67,7 @@ export interface RefineOptions {
53
67
  export async function refineLanding(options: RefineOptions): Promise<string> {
54
68
  const {
55
69
  anthropicApiKey,
70
+ openaiApiKey,
56
71
  currentHtml,
57
72
  instruction,
58
73
  referenceImage,
@@ -64,13 +79,9 @@ export async function refineLanding(options: RefineOptions): Promise<string> {
64
79
  onError,
65
80
  } = options;
66
81
 
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);
82
+ const defaultOpenai = referenceImage ? "gpt-4o" : "gpt-4o-mini";
83
+ const defaultAnthropic = referenceImage ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001";
84
+ const model = resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai, defaultAnthropic });
74
85
 
75
86
  // Build content (supports multimodal with reference image)
76
87
  const content: any[] = [];
@@ -102,8 +113,8 @@ export async function refineLanding(options: RefineOptions): Promise<string> {
102
113
  html = html.replace(/^```(?:html|xml)?\s*/, "").replace(/\s*```$/, "");
103
114
  }
104
115
 
105
- // Enrich images
106
- html = await enrichImages(html, pexelsApiKey);
116
+ // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)
117
+ html = await enrichImages(html, pexelsApiKey, openaiApiKey);
107
118
 
108
119
  onDone?.(html);
109
120
  return html;