vibespot 0.7.1 → 0.9.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/dist/index.js +160 -99
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/ui/chat.js +175 -7
- package/ui/index.html +58 -31
- package/ui/settings.js +456 -238
- package/ui/setup.js +205 -18
- package/ui/styles.css +343 -0
- package/ui/upload-panel.js +99 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibespot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,12 +21,16 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
23
23
|
"@clack/prompts": "^0.9.1",
|
|
24
|
+
"busboy": "^1.6.0",
|
|
24
25
|
"chalk": "^5.4.1",
|
|
25
26
|
"commander": "^13.1.0",
|
|
26
27
|
"execa": "^9.5.2",
|
|
28
|
+
"mammoth": "^1.11.0",
|
|
29
|
+
"pdf-parse": "^2.4.5",
|
|
27
30
|
"ws": "^8.19.0"
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
33
|
+
"@types/busboy": "^1.5.4",
|
|
30
34
|
"@types/node": "^22.0.0",
|
|
31
35
|
"@types/ws": "^8.18.1",
|
|
32
36
|
"tsup": "^8.4.0",
|
package/ui/chat.js
CHANGED
|
@@ -153,25 +153,191 @@ function handleWsMessage(msg) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// File attachments
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
const pendingFiles = [];
|
|
161
|
+
const fileChipsEl = document.getElementById("file-chips");
|
|
162
|
+
const fileInputEl = document.getElementById("file-input");
|
|
163
|
+
const attachBtn = document.getElementById("btn-attach-file");
|
|
164
|
+
const dropOverlay = document.getElementById("drop-overlay");
|
|
165
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
166
|
+
|
|
167
|
+
const IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/svg+xml", "image/webp", "image/gif"]);
|
|
168
|
+
const DOC_TYPES = new Set([
|
|
169
|
+
"application/pdf",
|
|
170
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
171
|
+
"text/markdown", "text/plain",
|
|
172
|
+
]);
|
|
173
|
+
const SUPPORTED_TYPES = new Set([...IMAGE_TYPES, ...DOC_TYPES]);
|
|
174
|
+
|
|
175
|
+
function addPendingFile(file) {
|
|
176
|
+
if (!SUPPORTED_TYPES.has(file.type)) {
|
|
177
|
+
showToast(`Unsupported file type: ${file.name}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
181
|
+
showToast(`File too large (>10MB): ${file.name}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
pendingFiles.push(file);
|
|
185
|
+
renderFileChips();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function removePendingFile(index) {
|
|
189
|
+
pendingFiles.splice(index, 1);
|
|
190
|
+
renderFileChips();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderFileChips() {
|
|
194
|
+
fileChipsEl.innerHTML = "";
|
|
195
|
+
if (pendingFiles.length === 0) {
|
|
196
|
+
fileChipsEl.classList.remove("visible");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
fileChipsEl.classList.add("visible");
|
|
200
|
+
pendingFiles.forEach((file, i) => {
|
|
201
|
+
const isImage = IMAGE_TYPES.has(file.type);
|
|
202
|
+
const chip = document.createElement("div");
|
|
203
|
+
chip.className = `file-chip file-chip--${isImage ? "image" : "doc"}`;
|
|
204
|
+
const sizeKB = Math.round(file.size / 1024);
|
|
205
|
+
const sizeStr = sizeKB > 1024 ? `${(sizeKB / 1024).toFixed(1)} MB` : `${sizeKB} KB`;
|
|
206
|
+
chip.innerHTML = `
|
|
207
|
+
<span class="file-chip__icon">${isImage ? "\u{1F5BC}" : "\u{1F4C4}"}</span>
|
|
208
|
+
<span class="file-chip__name">${escapeHtml(file.name)}</span>
|
|
209
|
+
<span class="file-chip__size">${sizeStr}</span>
|
|
210
|
+
<button class="file-chip__remove" title="Remove">×</button>
|
|
211
|
+
`;
|
|
212
|
+
chip.querySelector(".file-chip__remove").addEventListener("click", () => removePendingFile(i));
|
|
213
|
+
fileChipsEl.appendChild(chip);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function showToast(message) {
|
|
218
|
+
const toast = document.createElement("div");
|
|
219
|
+
toast.className = "toast";
|
|
220
|
+
toast.textContent = message;
|
|
221
|
+
document.body.appendChild(toast);
|
|
222
|
+
setTimeout(() => toast.classList.add("visible"), 10);
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
toast.classList.remove("visible");
|
|
225
|
+
setTimeout(() => toast.remove(), 300);
|
|
226
|
+
}, 3000);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function uploadFiles(files) {
|
|
230
|
+
const formData = new FormData();
|
|
231
|
+
for (const file of files) {
|
|
232
|
+
formData.append("files", file);
|
|
233
|
+
}
|
|
234
|
+
const resp = await fetch("/api/upload-files", { method: "POST", body: formData });
|
|
235
|
+
if (!resp.ok) {
|
|
236
|
+
const err = await resp.json().catch(() => ({ error: "Upload failed" }));
|
|
237
|
+
throw new Error(err.error || "Upload failed");
|
|
238
|
+
}
|
|
239
|
+
return resp.json();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderFileChipsInMessage(files) {
|
|
243
|
+
if (!files || files.length === 0) return "";
|
|
244
|
+
return `<div class="chat-msg__files">${files
|
|
245
|
+
.map((f) => {
|
|
246
|
+
const isImage = f.type === "image";
|
|
247
|
+
return `<span class="file-chip file-chip--${isImage ? "image" : "doc"} file-chip--sent">
|
|
248
|
+
<span class="file-chip__icon">${isImage ? "\u{1F5BC}" : "\u{1F4C4}"}</span>
|
|
249
|
+
<span class="file-chip__name">${escapeHtml(f.originalName || f.name)}</span>
|
|
250
|
+
</span>`;
|
|
251
|
+
})
|
|
252
|
+
.join("")}</div>`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Drag-and-drop
|
|
256
|
+
let dragCounter = 0;
|
|
257
|
+
|
|
258
|
+
inputEl.closest(".chat__input-area").addEventListener("dragenter", (e) => {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
dragCounter++;
|
|
261
|
+
dropOverlay.classList.remove("hidden");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
inputEl.closest(".chat__input-area").addEventListener("dragleave", (e) => {
|
|
265
|
+
e.preventDefault();
|
|
266
|
+
dragCounter--;
|
|
267
|
+
if (dragCounter <= 0) {
|
|
268
|
+
dragCounter = 0;
|
|
269
|
+
dropOverlay.classList.add("hidden");
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
inputEl.closest(".chat__input-area").addEventListener("dragover", (e) => {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
inputEl.closest(".chat__input-area").addEventListener("drop", (e) => {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
dragCounter = 0;
|
|
280
|
+
dropOverlay.classList.add("hidden");
|
|
281
|
+
if (e.dataTransfer?.files) {
|
|
282
|
+
for (const file of e.dataTransfer.files) {
|
|
283
|
+
addPendingFile(file);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Paperclip button
|
|
289
|
+
attachBtn.addEventListener("click", () => fileInputEl.click());
|
|
290
|
+
fileInputEl.addEventListener("change", () => {
|
|
291
|
+
for (const file of fileInputEl.files) {
|
|
292
|
+
addPendingFile(file);
|
|
293
|
+
}
|
|
294
|
+
fileInputEl.value = "";
|
|
295
|
+
});
|
|
296
|
+
|
|
156
297
|
// ---------------------------------------------------------------------------
|
|
157
298
|
// Sending messages
|
|
158
299
|
// ---------------------------------------------------------------------------
|
|
159
300
|
|
|
160
|
-
function sendMessage(text) {
|
|
161
|
-
|
|
301
|
+
async function sendMessage(text) {
|
|
302
|
+
const hasFiles = pendingFiles.length > 0;
|
|
303
|
+
if ((!text.trim() && !hasFiles) || isStreaming || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
162
304
|
|
|
163
305
|
// Remove welcome screen
|
|
164
306
|
const welcome = messagesEl.querySelector(".chat__welcome");
|
|
165
307
|
if (welcome) welcome.remove();
|
|
166
308
|
|
|
167
|
-
//
|
|
168
|
-
|
|
309
|
+
// Upload files first if any
|
|
310
|
+
let uploadedFiles = [];
|
|
311
|
+
const filesToUpload = [...pendingFiles];
|
|
312
|
+
if (hasFiles) {
|
|
313
|
+
pendingFiles.length = 0;
|
|
314
|
+
renderFileChips();
|
|
315
|
+
setStatus("Uploading files...");
|
|
316
|
+
try {
|
|
317
|
+
const result = await uploadFiles(filesToUpload);
|
|
318
|
+
uploadedFiles = result.files || [];
|
|
319
|
+
if (result.errors?.length) {
|
|
320
|
+
result.errors.forEach((e) => showToast(e));
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
showToast(err.message);
|
|
324
|
+
setStatus("Upload failed");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Show user message with file chips
|
|
330
|
+
appendUserMessage(text, null, uploadedFiles);
|
|
169
331
|
|
|
170
332
|
// Start streaming indicator
|
|
171
333
|
startStreaming();
|
|
172
334
|
|
|
173
|
-
// Send via WebSocket
|
|
174
|
-
|
|
335
|
+
// Send via WebSocket with file IDs
|
|
336
|
+
const payload = { type: "chat", message: text || "(files attached)" };
|
|
337
|
+
if (uploadedFiles.length > 0) {
|
|
338
|
+
payload.fileIds = uploadedFiles.map((f) => f.id);
|
|
339
|
+
}
|
|
340
|
+
ws.send(JSON.stringify(payload));
|
|
175
341
|
|
|
176
342
|
// Clear input
|
|
177
343
|
inputEl.value = "";
|
|
@@ -189,10 +355,11 @@ function formatMessageTime(ts) {
|
|
|
189
355
|
return `${d.getHours()}:${d.getMinutes().toString().padStart(2, "0")}`;
|
|
190
356
|
}
|
|
191
357
|
|
|
192
|
-
function appendUserMessage(text, timestamp) {
|
|
358
|
+
function appendUserMessage(text, timestamp, files) {
|
|
193
359
|
const time = formatMessageTime(timestamp || Date.now());
|
|
194
360
|
const div = document.createElement("div");
|
|
195
361
|
div.className = "chat-msg chat-msg--user";
|
|
362
|
+
const fileChipsHtml = renderFileChipsInMessage(files);
|
|
196
363
|
div.innerHTML = `
|
|
197
364
|
<div class="chat-msg__avatar chat-msg__avatar--user">Y</div>
|
|
198
365
|
<div class="chat-msg__content">
|
|
@@ -200,6 +367,7 @@ function appendUserMessage(text, timestamp) {
|
|
|
200
367
|
<span class="chat-msg__sender">You</span>
|
|
201
368
|
<span class="chat-msg__time">${time}</span>
|
|
202
369
|
</div>
|
|
370
|
+
${fileChipsHtml}
|
|
203
371
|
<div class="chat-msg__bubble">${escapeHtml(text)}</div>
|
|
204
372
|
</div>`;
|
|
205
373
|
messagesEl.appendChild(div);
|
package/ui/index.html
CHANGED
|
@@ -70,53 +70,63 @@
|
|
|
70
70
|
<!-- Settings link (hidden — now in sidebar footer) -->
|
|
71
71
|
<div class="setup__settings-link hidden" id="setup-settings-link"></div>
|
|
72
72
|
|
|
73
|
-
<!-- Main
|
|
73
|
+
<!-- Main action buttons -->
|
|
74
74
|
<div class="setup__options" id="setup-options">
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
<div class="setup__actions">
|
|
76
|
+
<button class="setup__action-btn" data-action="new">
|
|
77
|
+
<span class="setup__action-icon">+</span>
|
|
78
|
+
<span>New Theme</span>
|
|
79
|
+
</button>
|
|
80
|
+
<button class="setup__action-btn" data-action="continue">
|
|
81
|
+
<span class="setup__action-icon">▶</span>
|
|
82
|
+
<span>Continue</span>
|
|
83
|
+
</button>
|
|
84
|
+
<button class="setup__action-btn" data-action="download">
|
|
85
|
+
<span class="setup__action-icon">↓</span>
|
|
86
|
+
<span>From HubSpot</span>
|
|
87
|
+
</button>
|
|
88
|
+
<button class="setup__action-btn" data-action="convert">
|
|
89
|
+
<span class="setup__action-icon">↺</span>
|
|
90
|
+
<span>Convert React</span>
|
|
91
|
+
<span class="setup__action-badge">Experimental</span>
|
|
92
|
+
</button>
|
|
83
93
|
</div>
|
|
84
94
|
|
|
85
|
-
<!--
|
|
86
|
-
<div class="
|
|
87
|
-
<p class="setup__label">Start a new project</p>
|
|
95
|
+
<!-- Expandable panels (one visible at a time) -->
|
|
96
|
+
<div class="setup__panel hidden" id="panel-new">
|
|
88
97
|
<div class="setup__row">
|
|
89
98
|
<input type="text" class="setup__input setup__input--wide" id="new-theme-name" placeholder="my-landing-page" />
|
|
90
99
|
<button class="btn btn--primary" id="btn-create-theme">Create</button>
|
|
91
100
|
</div>
|
|
92
101
|
</div>
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<p class="
|
|
97
|
-
<div class="setup__row">
|
|
98
|
-
<input type="text" class="setup__input setup__input--wide" id="fetch-theme-name" placeholder="theme-name-in-hubspot" />
|
|
99
|
-
<button class="btn btn--secondary" id="btn-fetch-theme">Fetch</button>
|
|
100
|
-
</div>
|
|
103
|
+
<div class="setup__panel hidden" id="panel-continue">
|
|
104
|
+
<div class="setup__pills" id="continue-projects"></div>
|
|
105
|
+
<p class="setup__hint setup__hint--empty hidden" id="continue-empty">No themes yet — create one above!</p>
|
|
101
106
|
</div>
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
<div class="setup__panel hidden" id="panel-download">
|
|
109
|
+
<div class="setup__panel-account hidden" id="dl-account">
|
|
110
|
+
<span id="dl-account-name"></span>
|
|
111
|
+
<button class="btn-link" id="dl-account-change">Change</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div id="dl-account-switch" class="hidden" style="margin:0 0 12px"></div>
|
|
114
|
+
<div class="setup__row hidden" id="dl-input-row">
|
|
115
|
+
<input type="text" class="setup__input setup__input--wide" id="dl-theme-name" placeholder="Enter theme folder name on HubSpot" />
|
|
116
|
+
<button class="btn btn--primary" id="btn-fetch-theme">Download</button>
|
|
117
|
+
</div>
|
|
118
|
+
<p class="setup__hint hidden" id="dl-hint">Enter the exact folder name of your theme in the HubSpot Design Manager</p>
|
|
119
|
+
<div class="hidden" id="dl-no-account">
|
|
120
|
+
<p class="setup__hint">Connect a HubSpot account in <a href="#" id="dl-open-settings">Settings</a> to download themes.</p>
|
|
109
121
|
</div>
|
|
110
|
-
<p class="setup__hint">Convert a React/Lovable project to HubSpot</p>
|
|
111
122
|
</div>
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
<div class="setup__section">
|
|
115
|
-
<p class="setup__label">Open from disk</p>
|
|
124
|
+
<div class="setup__panel hidden" id="panel-convert">
|
|
116
125
|
<div class="setup__row">
|
|
117
|
-
<input type="text" class="setup__input setup__input--wide" id="
|
|
118
|
-
<button class="btn btn--
|
|
126
|
+
<input type="text" class="setup__input setup__input--wide" id="import-url" placeholder="https://github.com/user/repo or Lovable URL" />
|
|
127
|
+
<button class="btn btn--primary" id="import-btn">Import</button>
|
|
119
128
|
</div>
|
|
129
|
+
<p class="setup__hint">Convert a React or Lovable project to a HubSpot theme</p>
|
|
120
130
|
</div>
|
|
121
131
|
</div>
|
|
122
132
|
|
|
@@ -361,9 +371,14 @@
|
|
|
361
371
|
</div>
|
|
362
372
|
</div>
|
|
363
373
|
<div class="chat__input-area">
|
|
374
|
+
<div class="chat__file-chips" id="file-chips"></div>
|
|
364
375
|
<div class="chat__input-wrapper">
|
|
365
376
|
<textarea class="chat__input" id="chat-input" placeholder="Describe your landing page..." rows="1"></textarea>
|
|
366
377
|
<div class="chat__input-actions">
|
|
378
|
+
<button class="chat__input-icon" id="btn-attach-file" title="Attach files (images, PDFs, docs)">
|
|
379
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
|
|
380
|
+
</button>
|
|
381
|
+
<input type="file" id="file-input" multiple accept="image/png,image/jpeg,image/svg+xml,image/webp,image/gif,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/markdown,text/plain" style="display:none">
|
|
367
382
|
<button class="chat__input-icon" id="btn-starter-templates" title="Templates">
|
|
368
383
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>
|
|
369
384
|
</button>
|
|
@@ -372,6 +387,12 @@
|
|
|
372
387
|
</button>
|
|
373
388
|
</div>
|
|
374
389
|
</div>
|
|
390
|
+
<div class="chat__drop-overlay hidden" id="drop-overlay">
|
|
391
|
+
<div class="chat__drop-overlay-content">
|
|
392
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
|
393
|
+
<span>Drop files here</span>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
375
396
|
</div>
|
|
376
397
|
</div>
|
|
377
398
|
</aside>
|
|
@@ -430,6 +451,12 @@
|
|
|
430
451
|
<h2 class="settings__title">Settings</h2>
|
|
431
452
|
<button class="settings__close" id="settings-close">×</button>
|
|
432
453
|
</div>
|
|
454
|
+
<div class="settings__tabs" id="settings-tabs">
|
|
455
|
+
<button class="settings__tab active" data-tab="ai">AI</button>
|
|
456
|
+
<button class="settings__tab" data-tab="hubspot">HubSpot</button>
|
|
457
|
+
<button class="settings__tab" data-tab="github">GitHub</button>
|
|
458
|
+
<button class="settings__tab" data-tab="vibespot">vibeSpot</button>
|
|
459
|
+
</div>
|
|
433
460
|
<div class="settings__body" id="settings-body">
|
|
434
461
|
<!-- Populated dynamically by settings.js -->
|
|
435
462
|
<div class="settings__loading">
|