vibespot 0.7.0 → 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 +293 -6781
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/ui/chat.js +215 -10
- package/ui/dashboard.js +12 -17
- package/ui/dialog.js +2 -3
- package/ui/favicon.ico +0 -0
- package/ui/field-editor.js +1 -0
- 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 +115 -39
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
|
@@ -15,6 +15,8 @@ let streamTimerInterval = null;
|
|
|
15
15
|
let lastStreamStatus = "";
|
|
16
16
|
let currentSessionId = "";
|
|
17
17
|
let currentTemplateId = "";
|
|
18
|
+
let renderScheduled = false;
|
|
19
|
+
let scrollScheduled = false;
|
|
18
20
|
|
|
19
21
|
const messagesEl = document.getElementById("chat-messages");
|
|
20
22
|
const inputEl = document.getElementById("chat-input");
|
|
@@ -47,11 +49,15 @@ function connectWebSocket() {
|
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
ws.onclose = () => {
|
|
52
|
+
stopStreamTimer();
|
|
53
|
+
if (isStreaming) finishStreaming();
|
|
50
54
|
setStatus("Disconnected — reconnecting...");
|
|
51
55
|
setTimeout(connectWebSocket, 2000);
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
ws.onerror = () => {
|
|
59
|
+
stopStreamTimer();
|
|
60
|
+
if (isStreaming) finishStreaming();
|
|
55
61
|
setStatus("Connection error");
|
|
56
62
|
};
|
|
57
63
|
}
|
|
@@ -147,25 +153,191 @@ function handleWsMessage(msg) {
|
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
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
|
+
|
|
150
297
|
// ---------------------------------------------------------------------------
|
|
151
298
|
// Sending messages
|
|
152
299
|
// ---------------------------------------------------------------------------
|
|
153
300
|
|
|
154
|
-
function sendMessage(text) {
|
|
155
|
-
|
|
301
|
+
async function sendMessage(text) {
|
|
302
|
+
const hasFiles = pendingFiles.length > 0;
|
|
303
|
+
if ((!text.trim() && !hasFiles) || isStreaming || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
156
304
|
|
|
157
305
|
// Remove welcome screen
|
|
158
306
|
const welcome = messagesEl.querySelector(".chat__welcome");
|
|
159
307
|
if (welcome) welcome.remove();
|
|
160
308
|
|
|
161
|
-
//
|
|
162
|
-
|
|
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);
|
|
163
331
|
|
|
164
332
|
// Start streaming indicator
|
|
165
333
|
startStreaming();
|
|
166
334
|
|
|
167
|
-
// Send via WebSocket
|
|
168
|
-
|
|
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));
|
|
169
341
|
|
|
170
342
|
// Clear input
|
|
171
343
|
inputEl.value = "";
|
|
@@ -183,10 +355,11 @@ function formatMessageTime(ts) {
|
|
|
183
355
|
return `${d.getHours()}:${d.getMinutes().toString().padStart(2, "0")}`;
|
|
184
356
|
}
|
|
185
357
|
|
|
186
|
-
function appendUserMessage(text, timestamp) {
|
|
358
|
+
function appendUserMessage(text, timestamp, files) {
|
|
187
359
|
const time = formatMessageTime(timestamp || Date.now());
|
|
188
360
|
const div = document.createElement("div");
|
|
189
361
|
div.className = "chat-msg chat-msg--user";
|
|
362
|
+
const fileChipsHtml = renderFileChipsInMessage(files);
|
|
190
363
|
div.innerHTML = `
|
|
191
364
|
<div class="chat-msg__avatar chat-msg__avatar--user">Y</div>
|
|
192
365
|
<div class="chat-msg__content">
|
|
@@ -194,6 +367,7 @@ function appendUserMessage(text, timestamp) {
|
|
|
194
367
|
<span class="chat-msg__sender">You</span>
|
|
195
368
|
<span class="chat-msg__time">${time}</span>
|
|
196
369
|
</div>
|
|
370
|
+
${fileChipsHtml}
|
|
197
371
|
<div class="chat-msg__bubble">${escapeHtml(text)}</div>
|
|
198
372
|
</div>`;
|
|
199
373
|
messagesEl.appendChild(div);
|
|
@@ -236,6 +410,16 @@ function handleStreamChunk(text) {
|
|
|
236
410
|
if (!streamingMsgEl) return;
|
|
237
411
|
streamBuffer += text;
|
|
238
412
|
|
|
413
|
+
if (!renderScheduled) {
|
|
414
|
+
renderScheduled = true;
|
|
415
|
+
requestAnimationFrame(flushStreamRender);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function flushStreamRender() {
|
|
420
|
+
renderScheduled = false;
|
|
421
|
+
if (!streamingMsgEl) return;
|
|
422
|
+
|
|
239
423
|
// Hide incomplete code fences (AI is writing module code)
|
|
240
424
|
let display = streamBuffer;
|
|
241
425
|
const fenceCount = (display.match(/```/g) || []).length;
|
|
@@ -410,7 +594,16 @@ function escapeHtml(str) {
|
|
|
410
594
|
}
|
|
411
595
|
|
|
412
596
|
function scrollToBottom() {
|
|
413
|
-
|
|
597
|
+
if (scrollScheduled) return;
|
|
598
|
+
scrollScheduled = true;
|
|
599
|
+
requestAnimationFrame(() => {
|
|
600
|
+
scrollScheduled = false;
|
|
601
|
+
// Only auto-scroll if user is near the bottom (within 150px)
|
|
602
|
+
const gap = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
|
|
603
|
+
if (gap < 150) {
|
|
604
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
605
|
+
}
|
|
606
|
+
});
|
|
414
607
|
}
|
|
415
608
|
|
|
416
609
|
function setStatus(text) {
|
|
@@ -488,7 +681,11 @@ async function refreshHistoryPanel() {
|
|
|
488
681
|
}
|
|
489
682
|
|
|
490
683
|
list.innerHTML = toggleHtml;
|
|
491
|
-
|
|
684
|
+
const HISTORY_LIMIT = 50;
|
|
685
|
+
const commits = data.commits.slice(0, HISTORY_LIMIT);
|
|
686
|
+
const frag = document.createDocumentFragment();
|
|
687
|
+
|
|
688
|
+
for (const commit of commits) {
|
|
492
689
|
const isInitial = commit.message.startsWith("Initial ");
|
|
493
690
|
const isRollback = commit.message.includes("Rollback to:");
|
|
494
691
|
|
|
@@ -507,7 +704,15 @@ async function refreshHistoryPanel() {
|
|
|
507
704
|
<div class="history-item__msg">${escapeHtml(displayMsg)}</div>
|
|
508
705
|
${!isInitial ? `<button class="history-item__rollback" data-hash="${escapeHtml(commit.fullHash)}">Restore</button>` : ""}
|
|
509
706
|
`;
|
|
510
|
-
|
|
707
|
+
frag.appendChild(item);
|
|
708
|
+
}
|
|
709
|
+
list.appendChild(frag);
|
|
710
|
+
|
|
711
|
+
if (data.commits.length > HISTORY_LIMIT) {
|
|
712
|
+
const more = document.createElement("div");
|
|
713
|
+
more.className = "history__show-more";
|
|
714
|
+
more.textContent = `Showing ${HISTORY_LIMIT} of ${data.commits.length} versions`;
|
|
715
|
+
list.appendChild(more);
|
|
511
716
|
}
|
|
512
717
|
|
|
513
718
|
list.querySelectorAll(".history-item__rollback").forEach((btn) => {
|
package/ui/dashboard.js
CHANGED
|
@@ -115,25 +115,20 @@ function renderTemplateList(templates) {
|
|
|
115
115
|
list.appendChild(item);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
//
|
|
119
|
-
list.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
// Event delegation — single listener handles all template actions
|
|
119
|
+
list.onclick = (e) => {
|
|
120
|
+
const openBtn = e.target.closest(".dashboard__template-open");
|
|
121
|
+
if (openBtn) return openTemplate(openBtn.dataset.id);
|
|
122
|
+
const delBtn = e.target.closest(".dashboard__template-delete");
|
|
123
|
+
if (delBtn) return confirmDeleteTemplate(delBtn.dataset.id);
|
|
124
|
+
};
|
|
125
|
+
list.ondblclick = (e) => {
|
|
126
|
+
const labelEl = e.target.closest(".dashboard__template-label");
|
|
127
|
+
if (!labelEl) return;
|
|
128
128
|
const item = labelEl.closest(".dashboard__template-item");
|
|
129
129
|
const templateId = item?.querySelector(".dashboard__template-open")?.dataset.id;
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
labelEl.addEventListener("dblclick", (e) => {
|
|
133
|
-
e.stopPropagation();
|
|
134
|
-
startTemplateRename(labelEl, templateId);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
130
|
+
if (templateId) startTemplateRename(labelEl, templateId);
|
|
131
|
+
};
|
|
137
132
|
}
|
|
138
133
|
|
|
139
134
|
function startTemplateRename(labelEl, templateId) {
|
package/ui/dialog.js
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
// HTML-escape helper (standalone so dialog.js has no load-order dependency)
|
|
7
7
|
if (typeof esc === "undefined") {
|
|
8
8
|
// eslint-disable-next-line no-var
|
|
9
|
+
var _escMap = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
9
10
|
var esc = function (str) {
|
|
10
|
-
|
|
11
|
-
el.textContent = String(str);
|
|
12
|
-
return el.innerHTML;
|
|
11
|
+
return String(str).replace(/[&<>"']/g, function (c) { return _escMap[c]; });
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
14
|
|
package/ui/favicon.ico
CHANGED
|
Binary file
|
package/ui/field-editor.js
CHANGED
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">
|