@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 +65 -16
- package/package.json +35 -11
- package/src/generate.ts +31 -10
- package/src/images/dalleImages.ts +29 -0
- package/src/images/enrichImages.ts +11 -3
- package/src/images/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/refine.ts +23 -10
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
|
|
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: "...",
|
|
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
|
-
|
|
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 |
|
|
136
|
-
|
|
137
|
-
| `@easybits.cloud/html-tailwind-generator` | Everything (
|
|
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
|
-
-
|
|
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
|
-
- [
|
|
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.
|
|
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": {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"@codemirror/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"@codemirror/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
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
|
|
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
|
|
158
|
-
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
});
|
package/src/images/index.ts
CHANGED
package/src/index.ts
CHANGED
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:
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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;
|