@easybits.cloud/html-tailwind-generator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +131 -0
- package/README.md +178 -0
- package/package.json +50 -0
- package/src/buildHtml.ts +78 -0
- package/src/components/Canvas.tsx +162 -0
- package/src/components/CodeEditor.tsx +239 -0
- package/src/components/FloatingToolbar.tsx +350 -0
- package/src/components/SectionList.tsx +217 -0
- package/src/components/index.ts +4 -0
- package/src/deploy.ts +73 -0
- package/src/generate.ts +274 -0
- package/src/iframeScript.ts +261 -0
- package/src/images/enrichImages.ts +127 -0
- package/src/images/index.ts +2 -0
- package/src/images/pexels.ts +27 -0
- package/src/index.ts +57 -0
- package/src/refine.ts +115 -0
- package/src/themes.ts +204 -0
- package/src/types.ts +30 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript injected into the landing v3 iframe.
|
|
3
|
+
* Handles hover highlights, click selection, contentEditable text editing,
|
|
4
|
+
* postMessage communication with the parent editor,
|
|
5
|
+
* and incremental section injection from parent.
|
|
6
|
+
*/
|
|
7
|
+
export function getIframeScript(): string {
|
|
8
|
+
return `
|
|
9
|
+
(function() {
|
|
10
|
+
let hoveredEl = null;
|
|
11
|
+
let selectedEl = null;
|
|
12
|
+
const OUTLINE_HOVER = '2px solid #3B82F6';
|
|
13
|
+
const OUTLINE_SELECTED = '2px solid #8B5CF6';
|
|
14
|
+
|
|
15
|
+
function getSectionId(el) {
|
|
16
|
+
let node = el;
|
|
17
|
+
while (node && node !== document.body) {
|
|
18
|
+
if (node.dataset && node.dataset.sectionId) {
|
|
19
|
+
return node.dataset.sectionId;
|
|
20
|
+
}
|
|
21
|
+
node = node.parentElement;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getSectionElement(sectionId) {
|
|
27
|
+
return document.querySelector('[data-section-id="' + sectionId + '"]');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getElementPath(el) {
|
|
31
|
+
const parts = [];
|
|
32
|
+
let node = el;
|
|
33
|
+
while (node && node !== document.body) {
|
|
34
|
+
let tag = node.tagName.toLowerCase();
|
|
35
|
+
if (node.id) { tag += '#' + node.id; }
|
|
36
|
+
const siblings = node.parentElement ? Array.from(node.parentElement.children).filter(function(c) { return c.tagName === node.tagName; }) : [];
|
|
37
|
+
if (siblings.length > 1) { tag += ':nth(' + siblings.indexOf(node) + ')'; }
|
|
38
|
+
parts.unshift(tag);
|
|
39
|
+
node = node.parentElement;
|
|
40
|
+
}
|
|
41
|
+
return parts.join(' > ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isTextElement(el) {
|
|
45
|
+
var textTags = ['H1','H2','H3','H4','H5','H6','P','SPAN','LI','A','BLOCKQUOTE','LABEL','TD','TH','FIGCAPTION','BUTTON'];
|
|
46
|
+
return textTags.indexOf(el.tagName) !== -1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Hover
|
|
50
|
+
document.addEventListener('mouseover', function(e) {
|
|
51
|
+
var el = e.target;
|
|
52
|
+
if (el === document.body || el === document.documentElement) return;
|
|
53
|
+
if (el === selectedEl) return;
|
|
54
|
+
if (hoveredEl && hoveredEl !== selectedEl) {
|
|
55
|
+
hoveredEl.style.outline = '';
|
|
56
|
+
hoveredEl.style.outlineOffset = '';
|
|
57
|
+
}
|
|
58
|
+
hoveredEl = el;
|
|
59
|
+
if (el !== selectedEl) {
|
|
60
|
+
el.style.outline = OUTLINE_HOVER;
|
|
61
|
+
el.style.outlineOffset = '-2px';
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
document.addEventListener('mouseout', function(e) {
|
|
66
|
+
if (hoveredEl && hoveredEl !== selectedEl) {
|
|
67
|
+
hoveredEl.style.outline = '';
|
|
68
|
+
hoveredEl.style.outlineOffset = '';
|
|
69
|
+
}
|
|
70
|
+
hoveredEl = null;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Click — select element
|
|
74
|
+
document.addEventListener('click', function(e) {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
var el = e.target;
|
|
78
|
+
|
|
79
|
+
// Deselect previous
|
|
80
|
+
if (selectedEl) {
|
|
81
|
+
selectedEl.style.outline = '';
|
|
82
|
+
selectedEl.style.outlineOffset = '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (selectedEl === el) {
|
|
86
|
+
selectedEl = null;
|
|
87
|
+
window.parent.postMessage({ type: 'element-deselected' }, '*');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
selectedEl = el;
|
|
92
|
+
|
|
93
|
+
// Clear hover styles BEFORE capturing openTag (so it matches source HTML)
|
|
94
|
+
el.style.outline = '';
|
|
95
|
+
el.style.outlineOffset = '';
|
|
96
|
+
var openTag = el.outerHTML.substring(0, el.outerHTML.indexOf('>') + 1).substring(0, 120);
|
|
97
|
+
|
|
98
|
+
el.style.outline = OUTLINE_SELECTED;
|
|
99
|
+
el.style.outlineOffset = '-2px';
|
|
100
|
+
|
|
101
|
+
var rect = el.getBoundingClientRect();
|
|
102
|
+
var attrs = {};
|
|
103
|
+
if (el.tagName === 'IMG') {
|
|
104
|
+
attrs = { src: el.getAttribute('src') || '', alt: el.getAttribute('alt') || '' };
|
|
105
|
+
}
|
|
106
|
+
if (el.tagName === 'A') {
|
|
107
|
+
attrs = { href: el.getAttribute('href') || '', target: el.getAttribute('target') || '' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
window.parent.postMessage({
|
|
111
|
+
type: 'element-selected',
|
|
112
|
+
sectionId: getSectionId(el),
|
|
113
|
+
tagName: el.tagName,
|
|
114
|
+
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
|
|
115
|
+
text: (el.textContent || '').substring(0, 200),
|
|
116
|
+
openTag: openTag,
|
|
117
|
+
elementPath: getElementPath(el),
|
|
118
|
+
isSectionRoot: el.dataset && el.dataset.sectionId ? true : false,
|
|
119
|
+
attrs: attrs,
|
|
120
|
+
}, '*');
|
|
121
|
+
}, true);
|
|
122
|
+
|
|
123
|
+
// Double-click — contentEditable for text
|
|
124
|
+
document.addEventListener('dblclick', function(e) {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
var el = e.target;
|
|
128
|
+
if (!isTextElement(el)) return;
|
|
129
|
+
|
|
130
|
+
el.contentEditable = 'true';
|
|
131
|
+
el.focus();
|
|
132
|
+
el.style.outline = '2px dashed #F59E0B';
|
|
133
|
+
el.style.outlineOffset = '-2px';
|
|
134
|
+
|
|
135
|
+
function onBlur() {
|
|
136
|
+
el.contentEditable = 'false';
|
|
137
|
+
el.style.outline = '';
|
|
138
|
+
el.style.outlineOffset = '';
|
|
139
|
+
el.removeEventListener('blur', onBlur);
|
|
140
|
+
el.removeEventListener('keydown', onKeydown);
|
|
141
|
+
|
|
142
|
+
var sid = getSectionId(el);
|
|
143
|
+
var sectionEl = sid ? getSectionElement(sid) : null;
|
|
144
|
+
window.parent.postMessage({
|
|
145
|
+
type: 'text-edited',
|
|
146
|
+
sectionId: sid,
|
|
147
|
+
elementPath: getElementPath(el),
|
|
148
|
+
newText: el.innerHTML,
|
|
149
|
+
sectionHtml: sectionEl ? sectionEl.innerHTML : null,
|
|
150
|
+
}, '*');
|
|
151
|
+
|
|
152
|
+
selectedEl = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function onKeydown(ev) {
|
|
156
|
+
if (ev.key === 'Escape') {
|
|
157
|
+
el.blur();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
el.addEventListener('blur', onBlur);
|
|
162
|
+
el.addEventListener('keydown', onKeydown);
|
|
163
|
+
}, true);
|
|
164
|
+
|
|
165
|
+
// Listen for messages FROM parent (incremental section injection)
|
|
166
|
+
window.addEventListener('message', function(e) {
|
|
167
|
+
var msg = e.data;
|
|
168
|
+
if (!msg || !msg.action) return;
|
|
169
|
+
|
|
170
|
+
if (msg.action === 'add-section') {
|
|
171
|
+
var wrapper = document.createElement('div');
|
|
172
|
+
wrapper.setAttribute('data-section-id', msg.id);
|
|
173
|
+
wrapper.innerHTML = msg.html;
|
|
174
|
+
wrapper.style.animation = 'fadeInUp 0.4s ease-out';
|
|
175
|
+
document.body.appendChild(wrapper);
|
|
176
|
+
wrapper.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (msg.action === 'update-section') {
|
|
180
|
+
var el = getSectionElement(msg.id);
|
|
181
|
+
if (el) { el.innerHTML = msg.html; }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (msg.action === 'remove-section') {
|
|
185
|
+
var el = getSectionElement(msg.id);
|
|
186
|
+
if (el) { el.remove(); }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (msg.action === 'reorder-sections') {
|
|
190
|
+
// msg.order = [id1, id2, id3, ...]
|
|
191
|
+
var order = msg.order;
|
|
192
|
+
for (var i = 0; i < order.length; i++) {
|
|
193
|
+
var el = getSectionElement(order[i]);
|
|
194
|
+
if (el) { document.body.appendChild(el); }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (msg.action === 'update-attribute') {
|
|
199
|
+
var sectionEl = getSectionElement(msg.sectionId);
|
|
200
|
+
if (sectionEl) {
|
|
201
|
+
var target = null;
|
|
202
|
+
if (msg.elementPath) {
|
|
203
|
+
// Find element by matching path
|
|
204
|
+
var allEls = sectionEl.querySelectorAll(msg.tagName || '*');
|
|
205
|
+
for (var i = 0; i < allEls.length; i++) {
|
|
206
|
+
if (getElementPath(allEls[i]) === msg.elementPath) {
|
|
207
|
+
target = allEls[i];
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (target) {
|
|
213
|
+
target.setAttribute(msg.attr, msg.value);
|
|
214
|
+
window.parent.postMessage({
|
|
215
|
+
type: 'section-html-updated',
|
|
216
|
+
sectionId: msg.sectionId,
|
|
217
|
+
sectionHtml: sectionEl.innerHTML,
|
|
218
|
+
}, '*');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (msg.action === 'set-theme') {
|
|
224
|
+
if (msg.theme && msg.theme !== 'default') {
|
|
225
|
+
document.documentElement.setAttribute('data-theme', msg.theme);
|
|
226
|
+
} else {
|
|
227
|
+
document.documentElement.removeAttribute('data-theme');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (msg.action === 'set-custom-css') {
|
|
232
|
+
var customStyle = document.getElementById('custom-theme-css');
|
|
233
|
+
if (!customStyle) {
|
|
234
|
+
customStyle = document.createElement('style');
|
|
235
|
+
customStyle.id = 'custom-theme-css';
|
|
236
|
+
document.head.appendChild(customStyle);
|
|
237
|
+
}
|
|
238
|
+
customStyle.textContent = msg.css || '';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (msg.action === 'scroll-to-section') {
|
|
242
|
+
var el = getSectionElement(msg.id);
|
|
243
|
+
if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (msg.action === 'full-rewrite') {
|
|
247
|
+
// Fallback: rewrite everything
|
|
248
|
+
document.body.innerHTML = msg.html;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Inject animation keyframe
|
|
253
|
+
var style = document.createElement('style');
|
|
254
|
+
style.textContent = '@keyframes fadeInUp { from { opacity:0; transform:translateY(20px); } to { opacity:1; transform:translateY(0); } }';
|
|
255
|
+
document.head.appendChild(style);
|
|
256
|
+
|
|
257
|
+
// Notify parent we're ready
|
|
258
|
+
window.parent.postMessage({ type: 'ready' }, '*');
|
|
259
|
+
})();
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { searchImage } from "./pexels";
|
|
2
|
+
|
|
3
|
+
interface ImageMatch {
|
|
4
|
+
query: string;
|
|
5
|
+
searchStr: string;
|
|
6
|
+
replaceStr: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const FAKE_DOMAINS = [
|
|
10
|
+
"images.unsplash.com",
|
|
11
|
+
"unsplash.com",
|
|
12
|
+
"via.placeholder.com",
|
|
13
|
+
"placeholder.com",
|
|
14
|
+
"placehold.co",
|
|
15
|
+
"placehold.it",
|
|
16
|
+
"placekitten.com",
|
|
17
|
+
"picsum.photos",
|
|
18
|
+
"loremflickr.com",
|
|
19
|
+
"source.unsplash.com",
|
|
20
|
+
"dummyimage.com",
|
|
21
|
+
"fakeimg.pl",
|
|
22
|
+
"example.com",
|
|
23
|
+
"img.freepik.com",
|
|
24
|
+
"cdn.pixabay.com",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find all images in HTML that need Pexels enrichment.
|
|
29
|
+
* Two strategies:
|
|
30
|
+
* 1. data-image-query="..." — AI followed instructions
|
|
31
|
+
* 2. <img src="fake-url" — detect fake domains, use alt/class/nearby text as query
|
|
32
|
+
*/
|
|
33
|
+
export function findImageSlots(html: string): ImageMatch[] {
|
|
34
|
+
const matches: ImageMatch[] = [];
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
|
|
37
|
+
// 1. data-image-query="..."
|
|
38
|
+
const diqRegex = /data-image-query="([^"]+)"/g;
|
|
39
|
+
let m: RegExpExecArray | null;
|
|
40
|
+
while ((m = diqRegex.exec(html)) !== null) {
|
|
41
|
+
const query = m[1];
|
|
42
|
+
if (seen.has(query)) continue;
|
|
43
|
+
seen.add(query);
|
|
44
|
+
matches.push({
|
|
45
|
+
query,
|
|
46
|
+
searchStr: `data-image-query="${query}"`,
|
|
47
|
+
replaceStr: `src="{url}" data-enriched="true"`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. <img with fake/non-existent src URLs
|
|
52
|
+
const imgRegex = /<img\s[^>]*src="(https?:\/\/[^"]+)"[^>]*>/gi;
|
|
53
|
+
while ((m = imgRegex.exec(html)) !== null) {
|
|
54
|
+
const fullTag = m[0];
|
|
55
|
+
const srcUrl = m[1];
|
|
56
|
+
|
|
57
|
+
if (fullTag.includes("data-enriched")) continue;
|
|
58
|
+
if (srcUrl.includes("pexels.com")) continue;
|
|
59
|
+
if (seen.has(srcUrl)) continue;
|
|
60
|
+
|
|
61
|
+
// Check if domain is fake
|
|
62
|
+
let isFake = false;
|
|
63
|
+
try {
|
|
64
|
+
const domain = new URL(srcUrl).hostname;
|
|
65
|
+
isFake = FAKE_DOMAINS.some((d) => domain.includes(d));
|
|
66
|
+
} catch {
|
|
67
|
+
isFake = true;
|
|
68
|
+
}
|
|
69
|
+
if (!isFake) continue;
|
|
70
|
+
|
|
71
|
+
// Extract query: try alt, then class context, then URL path words
|
|
72
|
+
const altMatch = fullTag.match(/alt="([^"]*?)"/);
|
|
73
|
+
let query = altMatch?.[1]?.trim() || "";
|
|
74
|
+
|
|
75
|
+
if (!query) {
|
|
76
|
+
// Try to extract meaningful words from the URL path
|
|
77
|
+
try {
|
|
78
|
+
const path = new URL(srcUrl).pathname;
|
|
79
|
+
const words = path
|
|
80
|
+
.replace(/[^a-zA-Z]/g, " ")
|
|
81
|
+
.split(/\s+/)
|
|
82
|
+
.filter((w) => w.length > 2)
|
|
83
|
+
.slice(0, 4)
|
|
84
|
+
.join(" ");
|
|
85
|
+
if (words.length > 3) query = words;
|
|
86
|
+
} catch { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!query) query = "professional website hero image";
|
|
90
|
+
|
|
91
|
+
seen.add(srcUrl);
|
|
92
|
+
matches.push({
|
|
93
|
+
query,
|
|
94
|
+
searchStr: `src="${srcUrl}"`,
|
|
95
|
+
replaceStr: `src="{url}" data-enriched="true"`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return matches;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Enrich all images in an HTML string with Pexels photos.
|
|
104
|
+
*/
|
|
105
|
+
export async function enrichImages(html: string, pexelsApiKey?: string): Promise<string> {
|
|
106
|
+
const slots = findImageSlots(html);
|
|
107
|
+
if (slots.length === 0) return html;
|
|
108
|
+
|
|
109
|
+
let result = html;
|
|
110
|
+
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))}`;
|
|
113
|
+
const replacement = slot.replaceStr.replace("{url}", url);
|
|
114
|
+
result = result.replaceAll(slot.searchStr, replacement);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await Promise.allSettled(promises);
|
|
118
|
+
|
|
119
|
+
// Catch any remaining <img> tags without src (AI didn't follow instructions)
|
|
120
|
+
result = result.replace(/<img\s(?![^>]*\bsrc=)([^>]*?)>/gi, (_match, attrs) => {
|
|
121
|
+
const altMatch = attrs.match(/alt="([^"]*?)"/);
|
|
122
|
+
const query = altMatch?.[1] || "professional image";
|
|
123
|
+
return `<img src="https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}" ${attrs}>`;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface PexelsResult {
|
|
2
|
+
url: string;
|
|
3
|
+
photographer: string;
|
|
4
|
+
alt: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function searchImage(query: string, apiKey?: string): Promise<PexelsResult | null> {
|
|
8
|
+
const key = apiKey || process.env.PEXELS_API_KEY;
|
|
9
|
+
if (!key) return null;
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(
|
|
12
|
+
`https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=1&orientation=landscape`,
|
|
13
|
+
{ headers: { Authorization: key } }
|
|
14
|
+
);
|
|
15
|
+
if (!res.ok) return null;
|
|
16
|
+
const data = await res.json();
|
|
17
|
+
const photo = data.photos?.[0];
|
|
18
|
+
if (!photo) return null;
|
|
19
|
+
return {
|
|
20
|
+
url: photo.src.large,
|
|
21
|
+
photographer: photo.photographer,
|
|
22
|
+
alt: photo.alt || query,
|
|
23
|
+
};
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type { Section3, IframeMessage } from "./types";
|
|
3
|
+
export type { LandingTheme, CustomColors } from "./themes";
|
|
4
|
+
|
|
5
|
+
// Themes
|
|
6
|
+
export {
|
|
7
|
+
LANDING_THEMES,
|
|
8
|
+
buildCustomTheme,
|
|
9
|
+
buildCustomThemeCss,
|
|
10
|
+
buildThemeCss,
|
|
11
|
+
buildSingleThemeCss,
|
|
12
|
+
} from "./themes";
|
|
13
|
+
|
|
14
|
+
// HTML builders
|
|
15
|
+
export { buildPreviewHtml, buildDeployHtml } from "./buildHtml";
|
|
16
|
+
export { getIframeScript } from "./iframeScript";
|
|
17
|
+
|
|
18
|
+
// Generation
|
|
19
|
+
export {
|
|
20
|
+
generateLanding,
|
|
21
|
+
extractJsonObjects,
|
|
22
|
+
SYSTEM_PROMPT,
|
|
23
|
+
PROMPT_SUFFIX,
|
|
24
|
+
type GenerateOptions,
|
|
25
|
+
} from "./generate";
|
|
26
|
+
|
|
27
|
+
// Refinement
|
|
28
|
+
export {
|
|
29
|
+
refineLanding,
|
|
30
|
+
REFINE_SYSTEM,
|
|
31
|
+
type RefineOptions,
|
|
32
|
+
} from "./refine";
|
|
33
|
+
|
|
34
|
+
// Deploy
|
|
35
|
+
export {
|
|
36
|
+
deployToS3,
|
|
37
|
+
deployToEasyBits,
|
|
38
|
+
type DeployToS3Options,
|
|
39
|
+
type DeployToEasyBitsOptions,
|
|
40
|
+
} from "./deploy";
|
|
41
|
+
|
|
42
|
+
// Images
|
|
43
|
+
export {
|
|
44
|
+
searchImage,
|
|
45
|
+
enrichImages,
|
|
46
|
+
findImageSlots,
|
|
47
|
+
type PexelsResult,
|
|
48
|
+
} from "./images/index";
|
|
49
|
+
|
|
50
|
+
// Components (re-exported for convenience)
|
|
51
|
+
export {
|
|
52
|
+
Canvas,
|
|
53
|
+
type CanvasHandle,
|
|
54
|
+
SectionList,
|
|
55
|
+
FloatingToolbar,
|
|
56
|
+
CodeEditor,
|
|
57
|
+
} from "./components/index";
|
package/src/refine.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { streamText } from "ai";
|
|
2
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
|
+
import { enrichImages } from "./images/enrichImages";
|
|
4
|
+
|
|
5
|
+
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
|
+
|
|
7
|
+
RULES:
|
|
8
|
+
- Return ONLY the modified HTML — no full page, no <html>/<head>/<body> tags
|
|
9
|
+
- Use Tailwind CSS classes (CDN loaded)
|
|
10
|
+
- You may use inline styles for specific adjustments
|
|
11
|
+
- Images: use data-image-query="english search query" for new images
|
|
12
|
+
- Keep all text in its original language unless asked to translate
|
|
13
|
+
- Be creative — don't just make minimal changes, improve the design
|
|
14
|
+
- Return raw HTML only — no markdown fences, no explanations
|
|
15
|
+
|
|
16
|
+
COLOR SYSTEM — CRITICAL:
|
|
17
|
+
- Use semantic color classes: bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary, bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted, bg-secondary, text-secondary, bg-accent, text-accent
|
|
18
|
+
- NEVER use hardcoded colors: NO bg-gray-*, bg-black, bg-white, text-gray-*, text-black, text-white, etc.
|
|
19
|
+
- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.
|
|
20
|
+
- CONTRAST RULE: on bg-primary/bg-primary-dark → text-on-primary. On bg-surface/bg-surface-alt → text-on-surface/text-on-surface-muted. Never mismatch.
|
|
21
|
+
|
|
22
|
+
TAILWIND v3 NOTES:
|
|
23
|
+
- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
|
|
24
|
+
- Borders: border + border-gray-200 for visible borders`;
|
|
25
|
+
|
|
26
|
+
export interface RefineOptions {
|
|
27
|
+
/** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
|
|
28
|
+
anthropicApiKey?: string;
|
|
29
|
+
/** Current HTML of the section being refined */
|
|
30
|
+
currentHtml: string;
|
|
31
|
+
/** User instruction for refinement */
|
|
32
|
+
instruction: string;
|
|
33
|
+
/** Reference image (base64 data URI) for vision-based refinement */
|
|
34
|
+
referenceImage?: string;
|
|
35
|
+
/** Custom system prompt (overrides default REFINE_SYSTEM) */
|
|
36
|
+
systemPrompt?: string;
|
|
37
|
+
/** Model ID (default: claude-haiku-4-5-20251001, claude-sonnet-4-6 when referenceImage is provided) */
|
|
38
|
+
model?: string;
|
|
39
|
+
/** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
|
|
40
|
+
pexelsApiKey?: string;
|
|
41
|
+
/** Called with accumulated HTML as it streams */
|
|
42
|
+
onChunk?: (html: string) => void;
|
|
43
|
+
/** Called when refinement is complete with final enriched HTML */
|
|
44
|
+
onDone?: (html: string) => void;
|
|
45
|
+
/** Called on error */
|
|
46
|
+
onError?: (error: Error) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Refine a landing page section with streaming AI.
|
|
51
|
+
* Returns the final enriched HTML.
|
|
52
|
+
*/
|
|
53
|
+
export async function refineLanding(options: RefineOptions): Promise<string> {
|
|
54
|
+
const {
|
|
55
|
+
anthropicApiKey,
|
|
56
|
+
currentHtml,
|
|
57
|
+
instruction,
|
|
58
|
+
referenceImage,
|
|
59
|
+
systemPrompt = REFINE_SYSTEM,
|
|
60
|
+
model: modelId,
|
|
61
|
+
pexelsApiKey,
|
|
62
|
+
onChunk,
|
|
63
|
+
onDone,
|
|
64
|
+
onError,
|
|
65
|
+
} = options;
|
|
66
|
+
|
|
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);
|
|
74
|
+
|
|
75
|
+
// Build content (supports multimodal with reference image)
|
|
76
|
+
const content: any[] = [];
|
|
77
|
+
if (referenceImage) {
|
|
78
|
+
content.push({ type: "image", image: referenceImage });
|
|
79
|
+
}
|
|
80
|
+
content.push({
|
|
81
|
+
type: "text",
|
|
82
|
+
text: `Current HTML:\n${currentHtml}\n\nInstruction: ${instruction}\n\nReturn the updated HTML.`,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = streamText({
|
|
86
|
+
model,
|
|
87
|
+
system: systemPrompt,
|
|
88
|
+
messages: [{ role: "user", content }],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
let accumulated = "";
|
|
93
|
+
|
|
94
|
+
for await (const chunk of result.textStream) {
|
|
95
|
+
accumulated += chunk;
|
|
96
|
+
onChunk?.(accumulated);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Clean up markdown fences if present
|
|
100
|
+
let html = accumulated.trim();
|
|
101
|
+
if (html.startsWith("```")) {
|
|
102
|
+
html = html.replace(/^```(?:html|xml)?\s*/, "").replace(/\s*```$/, "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Enrich images
|
|
106
|
+
html = await enrichImages(html, pexelsApiKey);
|
|
107
|
+
|
|
108
|
+
onDone?.(html);
|
|
109
|
+
return html;
|
|
110
|
+
} catch (err: any) {
|
|
111
|
+
const error = err instanceof Error ? err : new Error(err?.message || "Refine failed");
|
|
112
|
+
onError?.(error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|