vibespot 0.6.0 → 0.7.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/dist/index.js +242 -6230
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/chat.js +192 -21
- package/ui/dashboard.js +190 -6
- package/ui/dialog.js +2 -3
- package/ui/favicon.ico +0 -0
- package/ui/field-editor.js +1 -0
- package/ui/index.html +16 -9
- package/ui/settings.js +10 -7
- package/ui/setup.js +101 -11
- package/ui/styles.css +111 -24
- package/ui/upload-panel.js +16 -4
package/package.json
CHANGED
package/ui/chat.js
CHANGED
|
@@ -12,6 +12,11 @@ let streamingMsgEl = null;
|
|
|
12
12
|
let streamBuffer = "";
|
|
13
13
|
let streamStartTime = 0;
|
|
14
14
|
let streamTimerInterval = null;
|
|
15
|
+
let lastStreamStatus = "";
|
|
16
|
+
let currentSessionId = "";
|
|
17
|
+
let currentTemplateId = "";
|
|
18
|
+
let renderScheduled = false;
|
|
19
|
+
let scrollScheduled = false;
|
|
15
20
|
|
|
16
21
|
const messagesEl = document.getElementById("chat-messages");
|
|
17
22
|
const inputEl = document.getElementById("chat-input");
|
|
@@ -44,11 +49,15 @@ function connectWebSocket() {
|
|
|
44
49
|
};
|
|
45
50
|
|
|
46
51
|
ws.onclose = () => {
|
|
52
|
+
stopStreamTimer();
|
|
53
|
+
if (isStreaming) finishStreaming();
|
|
47
54
|
setStatus("Disconnected — reconnecting...");
|
|
48
55
|
setTimeout(connectWebSocket, 2000);
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
ws.onerror = () => {
|
|
59
|
+
stopStreamTimer();
|
|
60
|
+
if (isStreaming) finishStreaming();
|
|
52
61
|
setStatus("Connection error");
|
|
53
62
|
};
|
|
54
63
|
}
|
|
@@ -64,6 +73,8 @@ function handleWsMessage(msg) {
|
|
|
64
73
|
|
|
65
74
|
switch (msg.type) {
|
|
66
75
|
case "init":
|
|
76
|
+
currentSessionId = msg.sessionId || "";
|
|
77
|
+
currentTemplateId = msg.templateId || "";
|
|
67
78
|
document.getElementById("theme-name").textContent = msg.themeName || "—";
|
|
68
79
|
|
|
69
80
|
// Clear previous project's chat and module list
|
|
@@ -104,7 +115,6 @@ function handleWsMessage(msg) {
|
|
|
104
115
|
break;
|
|
105
116
|
|
|
106
117
|
case "stream":
|
|
107
|
-
clearStreamStatus();
|
|
108
118
|
handleStreamChunk(msg.content);
|
|
109
119
|
break;
|
|
110
120
|
|
|
@@ -199,6 +209,7 @@ function appendUserMessage(text, timestamp) {
|
|
|
199
209
|
function startStreaming() {
|
|
200
210
|
isStreaming = true;
|
|
201
211
|
streamBuffer = "";
|
|
212
|
+
lastStreamStatus = "";
|
|
202
213
|
sendBtn.disabled = true;
|
|
203
214
|
streamStartTime = Date.now();
|
|
204
215
|
|
|
@@ -231,14 +242,42 @@ function handleStreamChunk(text) {
|
|
|
231
242
|
if (!streamingMsgEl) return;
|
|
232
243
|
streamBuffer += text;
|
|
233
244
|
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
if (!renderScheduled) {
|
|
246
|
+
renderScheduled = true;
|
|
247
|
+
requestAnimationFrame(flushStreamRender);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function flushStreamRender() {
|
|
252
|
+
renderScheduled = false;
|
|
253
|
+
if (!streamingMsgEl) return;
|
|
254
|
+
|
|
255
|
+
// Hide incomplete code fences (AI is writing module code)
|
|
256
|
+
let display = streamBuffer;
|
|
257
|
+
const fenceCount = (display.match(/```/g) || []).length;
|
|
258
|
+
if (fenceCount % 2 !== 0) {
|
|
259
|
+
const lastFence = display.lastIndexOf("```");
|
|
260
|
+
display = display.substring(0, lastFence);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const rendered = renderMarkdown(display);
|
|
264
|
+
const visibleText = rendered.replace(/<[^>]*>/g, "").trim();
|
|
265
|
+
|
|
266
|
+
if (visibleText) {
|
|
267
|
+
// Preserve the stream-status spinner while updating text
|
|
268
|
+
const statusEl = streamingMsgEl.querySelector(".stream-status");
|
|
269
|
+
streamingMsgEl.innerHTML = rendered;
|
|
270
|
+
if (statusEl) streamingMsgEl.appendChild(statusEl);
|
|
271
|
+
}
|
|
272
|
+
// No visible text — leave the spinner (.stream-status) untouched in the DOM
|
|
236
273
|
scrollToBottom();
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
function handleStreamStatus(status) {
|
|
240
277
|
if (!streamingMsgEl) startStreaming();
|
|
241
278
|
|
|
279
|
+
lastStreamStatus = status;
|
|
280
|
+
|
|
242
281
|
// Find or create the status element inside the streaming bubble
|
|
243
282
|
let statusEl = streamingMsgEl.querySelector(".stream-status");
|
|
244
283
|
if (!statusEl) {
|
|
@@ -311,7 +350,9 @@ function finishStreaming() {
|
|
|
311
350
|
|
|
312
351
|
// Final render of the full response
|
|
313
352
|
if (streamingMsgEl && streamBuffer) {
|
|
314
|
-
|
|
353
|
+
const rendered = renderMarkdown(streamBuffer);
|
|
354
|
+
const visibleText = rendered.replace(/<[^>]*>/g, "").trim();
|
|
355
|
+
streamingMsgEl.innerHTML = visibleText ? rendered : "<em>Modules applied.</em>";
|
|
315
356
|
}
|
|
316
357
|
|
|
317
358
|
streamingMsgEl = null;
|
|
@@ -344,13 +385,10 @@ function appendAssistantError(message) {
|
|
|
344
385
|
// ---------------------------------------------------------------------------
|
|
345
386
|
|
|
346
387
|
function renderMarkdown(text) {
|
|
347
|
-
//
|
|
348
|
-
text = text.replace(/```
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
352
|
-
return `<pre><code>${escapeHtml(code.trim())}</code></pre>`;
|
|
353
|
-
});
|
|
388
|
+
// Strip all code blocks — module code is applied via JSON, not displayed in chat
|
|
389
|
+
text = text.replace(/```[\s\S]*?```/g, "");
|
|
390
|
+
// Also strip unclosed code fences (truncated responses)
|
|
391
|
+
text = text.replace(/```[\s\S]*$/g, "");
|
|
354
392
|
|
|
355
393
|
// Inline code: `...`
|
|
356
394
|
text = text.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
@@ -388,7 +426,16 @@ function escapeHtml(str) {
|
|
|
388
426
|
}
|
|
389
427
|
|
|
390
428
|
function scrollToBottom() {
|
|
391
|
-
|
|
429
|
+
if (scrollScheduled) return;
|
|
430
|
+
scrollScheduled = true;
|
|
431
|
+
requestAnimationFrame(() => {
|
|
432
|
+
scrollScheduled = false;
|
|
433
|
+
// Only auto-scroll if user is near the bottom (within 150px)
|
|
434
|
+
const gap = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
|
|
435
|
+
if (gap < 150) {
|
|
436
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
392
439
|
}
|
|
393
440
|
|
|
394
441
|
function setStatus(text) {
|
|
@@ -434,28 +481,50 @@ function toggleHistoryPanel() {
|
|
|
434
481
|
if (historyPanelOpen) refreshHistoryPanel();
|
|
435
482
|
}
|
|
436
483
|
|
|
484
|
+
let historyShowAll = false;
|
|
485
|
+
|
|
437
486
|
async function refreshHistoryPanel() {
|
|
438
487
|
const list = document.getElementById("history-list");
|
|
439
488
|
if (!list) return;
|
|
440
489
|
list.innerHTML = '<div class="history__loading">Loading...</div>';
|
|
441
490
|
|
|
442
491
|
try {
|
|
443
|
-
const
|
|
492
|
+
const useFilter = currentTemplateId && !historyShowAll;
|
|
493
|
+
const url = useFilter
|
|
494
|
+
? `/api/history?templateId=${encodeURIComponent(currentTemplateId)}`
|
|
495
|
+
: "/api/history";
|
|
496
|
+
const res = await fetch(url);
|
|
444
497
|
const data = await res.json();
|
|
445
498
|
|
|
446
499
|
if (!data.available) {
|
|
447
500
|
list.innerHTML = '<div class="history__empty">Git not available</div>';
|
|
448
501
|
return;
|
|
449
502
|
}
|
|
503
|
+
|
|
504
|
+
// Show all / filter toggle
|
|
505
|
+
const toggleHtml = currentTemplateId
|
|
506
|
+
? `<div class="history__toggle"><button class="history__toggle-btn" id="history-toggle-filter">${historyShowAll ? "This template" : "Show all"}</button></div>`
|
|
507
|
+
: "";
|
|
508
|
+
|
|
450
509
|
if (data.commits.length === 0) {
|
|
451
|
-
list.innerHTML = '<div class="history__empty">No versions yet</div>';
|
|
510
|
+
list.innerHTML = toggleHtml + '<div class="history__empty">No versions yet</div>';
|
|
511
|
+
attachHistoryToggle();
|
|
452
512
|
return;
|
|
453
513
|
}
|
|
454
514
|
|
|
455
|
-
list.innerHTML =
|
|
456
|
-
|
|
515
|
+
list.innerHTML = toggleHtml;
|
|
516
|
+
const HISTORY_LIMIT = 50;
|
|
517
|
+
const commits = data.commits.slice(0, HISTORY_LIMIT);
|
|
518
|
+
const frag = document.createDocumentFragment();
|
|
519
|
+
|
|
520
|
+
for (const commit of commits) {
|
|
457
521
|
const isInitial = commit.message.startsWith("Initial ");
|
|
458
|
-
const isRollback = commit.message.
|
|
522
|
+
const isRollback = commit.message.includes("Rollback to:");
|
|
523
|
+
|
|
524
|
+
// Strip [templateId] prefix from display
|
|
525
|
+
let displayMsg = commit.message;
|
|
526
|
+
const prefixMatch = displayMsg.match(/^\[[^\]]+\]\s*/);
|
|
527
|
+
if (prefixMatch) displayMsg = displayMsg.slice(prefixMatch[0].length);
|
|
459
528
|
|
|
460
529
|
const item = document.createElement("div");
|
|
461
530
|
item.className = "history-item" + (isRollback ? " history-item--rollback" : "");
|
|
@@ -464,30 +533,55 @@ async function refreshHistoryPanel() {
|
|
|
464
533
|
<span class="history-item__hash">${escapeHtml(commit.hash)}</span>
|
|
465
534
|
<span class="history-item__date">${timeAgoShort(commit.timestamp)}</span>
|
|
466
535
|
</div>
|
|
467
|
-
<div class="history-item__msg">${escapeHtml(
|
|
536
|
+
<div class="history-item__msg">${escapeHtml(displayMsg)}</div>
|
|
468
537
|
${!isInitial ? `<button class="history-item__rollback" data-hash="${escapeHtml(commit.fullHash)}">Restore</button>` : ""}
|
|
469
538
|
`;
|
|
470
|
-
|
|
539
|
+
frag.appendChild(item);
|
|
540
|
+
}
|
|
541
|
+
list.appendChild(frag);
|
|
542
|
+
|
|
543
|
+
if (data.commits.length > HISTORY_LIMIT) {
|
|
544
|
+
const more = document.createElement("div");
|
|
545
|
+
more.className = "history__show-more";
|
|
546
|
+
more.textContent = `Showing ${HISTORY_LIMIT} of ${data.commits.length} versions`;
|
|
547
|
+
list.appendChild(more);
|
|
471
548
|
}
|
|
472
549
|
|
|
473
550
|
list.querySelectorAll(".history-item__rollback").forEach((btn) => {
|
|
474
551
|
btn.addEventListener("click", () => doRollback(btn.dataset.hash));
|
|
475
552
|
});
|
|
553
|
+
attachHistoryToggle();
|
|
476
554
|
} catch {
|
|
477
555
|
list.innerHTML = '<div class="history__empty">Error loading history</div>';
|
|
478
556
|
}
|
|
479
557
|
}
|
|
480
558
|
|
|
559
|
+
function attachHistoryToggle() {
|
|
560
|
+
const btn = document.getElementById("history-toggle-filter");
|
|
561
|
+
if (btn) {
|
|
562
|
+
btn.addEventListener("click", () => {
|
|
563
|
+
historyShowAll = !historyShowAll;
|
|
564
|
+
refreshHistoryPanel();
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
481
569
|
async function doRollback(hash) {
|
|
482
|
-
const
|
|
570
|
+
const scoped = currentTemplateId && !historyShowAll;
|
|
571
|
+
const msg = scoped
|
|
572
|
+
? "This template's modules will be restored to the selected version. Other templates are not affected."
|
|
573
|
+
: "All theme files will be replaced, but chat history is preserved.";
|
|
574
|
+
const ok = await vibeConfirm("Restore this version?", msg, { confirmLabel: "Restore", confirmClass: "btn--primary" });
|
|
483
575
|
if (!ok) return;
|
|
484
576
|
setStatus("Rolling back...");
|
|
485
577
|
|
|
486
578
|
try {
|
|
579
|
+
const payload = { hash };
|
|
580
|
+
if (scoped) payload.templateId = currentTemplateId;
|
|
487
581
|
const res = await fetch("/api/rollback", {
|
|
488
582
|
method: "POST",
|
|
489
583
|
headers: { "Content-Type": "application/json" },
|
|
490
|
-
body: JSON.stringify(
|
|
584
|
+
body: JSON.stringify(payload),
|
|
491
585
|
});
|
|
492
586
|
const data = await res.json();
|
|
493
587
|
|
|
@@ -991,6 +1085,83 @@ async function fetchHsAccountStatus() {
|
|
|
991
1085
|
}
|
|
992
1086
|
}
|
|
993
1087
|
|
|
1088
|
+
// ---------------------------------------------------------------------------
|
|
1089
|
+
// Topbar theme-name rename (double-click)
|
|
1090
|
+
// ---------------------------------------------------------------------------
|
|
1091
|
+
|
|
1092
|
+
document.getElementById("theme-name")?.addEventListener("dblclick", () => {
|
|
1093
|
+
const el = document.getElementById("theme-name");
|
|
1094
|
+
if (!el || !currentSessionId) return;
|
|
1095
|
+
if (el.contentEditable === "true") return;
|
|
1096
|
+
|
|
1097
|
+
const oldName = el.textContent.trim();
|
|
1098
|
+
el.contentEditable = "true";
|
|
1099
|
+
el.classList.add("topbar__project-pill--editing");
|
|
1100
|
+
el.focus();
|
|
1101
|
+
|
|
1102
|
+
const range = document.createRange();
|
|
1103
|
+
range.selectNodeContents(el);
|
|
1104
|
+
const sel = window.getSelection();
|
|
1105
|
+
sel.removeAllRanges();
|
|
1106
|
+
sel.addRange(range);
|
|
1107
|
+
|
|
1108
|
+
function commit() {
|
|
1109
|
+
el.contentEditable = "false";
|
|
1110
|
+
el.classList.remove("topbar__project-pill--editing");
|
|
1111
|
+
|
|
1112
|
+
const newName = el.textContent.trim();
|
|
1113
|
+
if (!newName || newName === oldName) {
|
|
1114
|
+
el.textContent = oldName;
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
fetch("/api/themes/rename", {
|
|
1119
|
+
method: "POST",
|
|
1120
|
+
headers: { "Content-Type": "application/json" },
|
|
1121
|
+
body: JSON.stringify({ sessionId: currentSessionId, newName }),
|
|
1122
|
+
})
|
|
1123
|
+
.then((r) => r.json())
|
|
1124
|
+
.then((data) => {
|
|
1125
|
+
if (data.ok) {
|
|
1126
|
+
el.textContent = data.newName;
|
|
1127
|
+
if (typeof currentAppTheme !== "undefined") currentAppTheme = data.newName;
|
|
1128
|
+
window.location.hash = "#/app/" + encodeURIComponent(data.newName);
|
|
1129
|
+
// Update rail item
|
|
1130
|
+
const railItem = document.querySelector(`.project-rail__item[data-name="${oldName}"]`);
|
|
1131
|
+
if (railItem) {
|
|
1132
|
+
railItem.dataset.name = data.newName;
|
|
1133
|
+
const nameSpan = railItem.querySelector(".project-rail__item-name");
|
|
1134
|
+
if (nameSpan) nameSpan.textContent = data.newName;
|
|
1135
|
+
const bubble = railItem.querySelector(".project-rail__item-bubble");
|
|
1136
|
+
if (bubble) bubble.textContent = data.newName.charAt(0).toUpperCase();
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof updateRailActive === "function") updateRailActive();
|
|
1139
|
+
} else {
|
|
1140
|
+
el.textContent = oldName;
|
|
1141
|
+
showError(data.error || "Rename failed");
|
|
1142
|
+
}
|
|
1143
|
+
})
|
|
1144
|
+
.catch(() => {
|
|
1145
|
+
el.textContent = oldName;
|
|
1146
|
+
showError("Rename failed");
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
el.addEventListener("blur", commit, { once: true });
|
|
1151
|
+
el.addEventListener("keydown", function handler(e) {
|
|
1152
|
+
if (e.key === "Enter") {
|
|
1153
|
+
e.preventDefault();
|
|
1154
|
+
el.removeEventListener("keydown", handler);
|
|
1155
|
+
el.blur();
|
|
1156
|
+
}
|
|
1157
|
+
if (e.key === "Escape") {
|
|
1158
|
+
el.textContent = oldName;
|
|
1159
|
+
el.removeEventListener("keydown", handler);
|
|
1160
|
+
el.blur();
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
|
|
994
1165
|
// ---------------------------------------------------------------------------
|
|
995
1166
|
// Initialize
|
|
996
1167
|
// ---------------------------------------------------------------------------
|
package/ui/dashboard.js
CHANGED
|
@@ -25,6 +25,7 @@ const PAGE_TYPE_FULL_LABELS = {
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
|
|
27
27
|
let currentDashboardTheme = "";
|
|
28
|
+
let currentDashboardSessionId = "";
|
|
28
29
|
|
|
29
30
|
async function showDashboard(themeName) {
|
|
30
31
|
currentDashboardTheme = themeName;
|
|
@@ -37,6 +38,15 @@ async function showDashboard(themeName) {
|
|
|
37
38
|
dashboardScreen.classList.remove("hidden");
|
|
38
39
|
|
|
39
40
|
document.getElementById("dashboard-theme-name").textContent = themeName;
|
|
41
|
+
document.getElementById("dashboard-theme-heading").textContent = themeName;
|
|
42
|
+
document.getElementById("dashboard-theme-path-text").textContent = "";
|
|
43
|
+
|
|
44
|
+
// Get sessionId for the active theme
|
|
45
|
+
try {
|
|
46
|
+
const themesRes = await fetch("/api/themes");
|
|
47
|
+
const themesData = await themesRes.json();
|
|
48
|
+
currentDashboardSessionId = themesData.activeTheme?.id || "";
|
|
49
|
+
} catch { currentDashboardSessionId = ""; }
|
|
40
50
|
|
|
41
51
|
// Update URL
|
|
42
52
|
const target = "#/dashboard/" + encodeURIComponent(themeName);
|
|
@@ -69,6 +79,9 @@ async function refreshDashboard() {
|
|
|
69
79
|
renderTemplateList(data.templates || []);
|
|
70
80
|
renderModuleLibrary(data.moduleLibrary || []);
|
|
71
81
|
renderBrandAssets(data.brandAssets || {});
|
|
82
|
+
if (data.themePath) {
|
|
83
|
+
document.getElementById("dashboard-theme-path-text").textContent = data.themePath;
|
|
84
|
+
}
|
|
72
85
|
} catch (err) {
|
|
73
86
|
console.error("Failed to load dashboard:", err);
|
|
74
87
|
}
|
|
@@ -102,12 +115,78 @@ function renderTemplateList(templates) {
|
|
|
102
115
|
list.appendChild(item);
|
|
103
116
|
}
|
|
104
117
|
|
|
105
|
-
//
|
|
106
|
-
list.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
const item = labelEl.closest(".dashboard__template-item");
|
|
129
|
+
const templateId = item?.querySelector(".dashboard__template-open")?.dataset.id;
|
|
130
|
+
if (templateId) startTemplateRename(labelEl, templateId);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function startTemplateRename(labelEl, templateId) {
|
|
135
|
+
if (labelEl.contentEditable === "true") return;
|
|
136
|
+
|
|
137
|
+
const oldLabel = labelEl.textContent.trim();
|
|
138
|
+
labelEl.contentEditable = "true";
|
|
139
|
+
labelEl.classList.add("dashboard__template-label--editing");
|
|
140
|
+
labelEl.focus();
|
|
141
|
+
|
|
142
|
+
const range = document.createRange();
|
|
143
|
+
range.selectNodeContents(labelEl);
|
|
144
|
+
const sel = window.getSelection();
|
|
145
|
+
sel.removeAllRanges();
|
|
146
|
+
sel.addRange(range);
|
|
147
|
+
|
|
148
|
+
function commit() {
|
|
149
|
+
labelEl.contentEditable = "false";
|
|
150
|
+
labelEl.classList.remove("dashboard__template-label--editing");
|
|
151
|
+
|
|
152
|
+
const newLabel = labelEl.textContent.trim();
|
|
153
|
+
if (!newLabel || newLabel === oldLabel) {
|
|
154
|
+
labelEl.textContent = oldLabel;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fetch("/api/templates/rename", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify({ templateId, newLabel }),
|
|
162
|
+
})
|
|
163
|
+
.then((r) => r.json())
|
|
164
|
+
.then((data) => {
|
|
165
|
+
if (data.ok) {
|
|
166
|
+
labelEl.textContent = data.newLabel;
|
|
167
|
+
} else {
|
|
168
|
+
labelEl.textContent = oldLabel;
|
|
169
|
+
if (typeof showError === "function") showError(data.error || "Rename failed");
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
.catch(() => {
|
|
173
|
+
labelEl.textContent = oldLabel;
|
|
174
|
+
if (typeof showError === "function") showError("Rename failed");
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
labelEl.addEventListener("blur", commit, { once: true });
|
|
179
|
+
labelEl.addEventListener("keydown", function handler(e) {
|
|
180
|
+
if (e.key === "Enter") {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
labelEl.removeEventListener("keydown", handler);
|
|
183
|
+
labelEl.blur();
|
|
184
|
+
}
|
|
185
|
+
if (e.key === "Escape") {
|
|
186
|
+
labelEl.textContent = oldLabel;
|
|
187
|
+
labelEl.removeEventListener("keydown", handler);
|
|
188
|
+
labelEl.blur();
|
|
189
|
+
}
|
|
111
190
|
});
|
|
112
191
|
}
|
|
113
192
|
|
|
@@ -389,6 +468,111 @@ document.getElementById("brand-upload-brandvoice").querySelector("input").addEve
|
|
|
389
468
|
if (e.target.files[0]) handleBrandFileSelected("brandvoice", e.target.files[0]);
|
|
390
469
|
});
|
|
391
470
|
|
|
471
|
+
// Dashboard theme heading — double-click to rename
|
|
472
|
+
document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick", () => {
|
|
473
|
+
const el = document.getElementById("dashboard-theme-heading");
|
|
474
|
+
if (!el || !currentDashboardSessionId) return;
|
|
475
|
+
if (el.contentEditable === "true") return;
|
|
476
|
+
|
|
477
|
+
const oldName = el.textContent.trim();
|
|
478
|
+
el.contentEditable = "true";
|
|
479
|
+
el.classList.add("dashboard__theme-heading--editing");
|
|
480
|
+
el.focus();
|
|
481
|
+
|
|
482
|
+
const range = document.createRange();
|
|
483
|
+
range.selectNodeContents(el);
|
|
484
|
+
const sel = window.getSelection();
|
|
485
|
+
sel.removeAllRanges();
|
|
486
|
+
sel.addRange(range);
|
|
487
|
+
|
|
488
|
+
function commit() {
|
|
489
|
+
el.contentEditable = "false";
|
|
490
|
+
el.classList.remove("dashboard__theme-heading--editing");
|
|
491
|
+
|
|
492
|
+
const newName = el.textContent.trim();
|
|
493
|
+
if (!newName || newName === oldName) {
|
|
494
|
+
el.textContent = oldName;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fetch("/api/themes/rename", {
|
|
499
|
+
method: "POST",
|
|
500
|
+
headers: { "Content-Type": "application/json" },
|
|
501
|
+
body: JSON.stringify({ sessionId: currentDashboardSessionId, newName }),
|
|
502
|
+
})
|
|
503
|
+
.then((r) => r.json())
|
|
504
|
+
.then((data) => {
|
|
505
|
+
if (data.ok) {
|
|
506
|
+
el.textContent = data.newName;
|
|
507
|
+
currentDashboardTheme = data.newName;
|
|
508
|
+
document.getElementById("dashboard-theme-name").textContent = data.newName;
|
|
509
|
+
window.location.hash = "#/dashboard/" + encodeURIComponent(data.newName);
|
|
510
|
+
// Update rail
|
|
511
|
+
const railItem = document.querySelector(`.project-rail__item[data-name="${oldName}"]`);
|
|
512
|
+
if (railItem) {
|
|
513
|
+
railItem.dataset.name = data.newName;
|
|
514
|
+
const nameSpan = railItem.querySelector(".project-rail__item-name");
|
|
515
|
+
if (nameSpan) nameSpan.textContent = data.newName;
|
|
516
|
+
const bubble = railItem.querySelector(".project-rail__item-bubble");
|
|
517
|
+
if (bubble) bubble.textContent = data.newName.charAt(0).toUpperCase();
|
|
518
|
+
}
|
|
519
|
+
if (typeof updateRailActive === "function") updateRailActive();
|
|
520
|
+
} else {
|
|
521
|
+
el.textContent = oldName;
|
|
522
|
+
if (typeof showError === "function") showError(data.error || "Rename failed");
|
|
523
|
+
}
|
|
524
|
+
})
|
|
525
|
+
.catch(() => {
|
|
526
|
+
el.textContent = oldName;
|
|
527
|
+
if (typeof showError === "function") showError("Rename failed");
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
el.addEventListener("blur", commit, { once: true });
|
|
532
|
+
el.addEventListener("keydown", function handler(e) {
|
|
533
|
+
if (e.key === "Enter") {
|
|
534
|
+
e.preventDefault();
|
|
535
|
+
el.removeEventListener("keydown", handler);
|
|
536
|
+
el.blur();
|
|
537
|
+
}
|
|
538
|
+
if (e.key === "Escape") {
|
|
539
|
+
el.textContent = oldName;
|
|
540
|
+
el.removeEventListener("keydown", handler);
|
|
541
|
+
el.blur();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Download ZIP button
|
|
547
|
+
document.getElementById("dashboard-download-zip").addEventListener("click", async () => {
|
|
548
|
+
const btn = document.getElementById("dashboard-download-zip");
|
|
549
|
+
const origHTML = btn.innerHTML;
|
|
550
|
+
btn.disabled = true;
|
|
551
|
+
btn.querySelector("span").textContent = "Downloading...";
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
const res = await fetch("/api/download-zip");
|
|
555
|
+
if (!res.ok) {
|
|
556
|
+
const err = await res.json().catch(() => ({ error: "Download failed" }));
|
|
557
|
+
throw new Error(err.error || "Download failed");
|
|
558
|
+
}
|
|
559
|
+
const blob = await res.blob();
|
|
560
|
+
const url = URL.createObjectURL(blob);
|
|
561
|
+
const a = document.createElement("a");
|
|
562
|
+
a.href = url;
|
|
563
|
+
a.download = (currentDashboardTheme || "theme") + ".zip";
|
|
564
|
+
document.body.appendChild(a);
|
|
565
|
+
a.click();
|
|
566
|
+
a.remove();
|
|
567
|
+
URL.revokeObjectURL(url);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
if (typeof vibeAlert === "function") vibeAlert(err.message, "Error");
|
|
570
|
+
} finally {
|
|
571
|
+
btn.disabled = false;
|
|
572
|
+
btn.innerHTML = origHTML;
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
392
576
|
// Humanify toggle
|
|
393
577
|
const humanifyCheckbox = document.getElementById("humanify-checkbox");
|
|
394
578
|
if (humanifyCheckbox) {
|
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
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
<!-- ============================================================ -->
|
|
42
42
|
<div class="setup-topbar" id="setup-topbar">
|
|
43
43
|
<div class="topbar__brand">
|
|
44
|
-
<div class="topbar__logo-icon"
|
|
44
|
+
<div class="topbar__logo-icon"><svg viewBox="0 0 512 512" width="18" height="18"><path d="M256 76 Q280 220 436 256 Q280 292 256 436 Q232 292 76 256 Q232 220 256 76 Z" fill="currentColor"/></svg></div>
|
|
45
45
|
<span class="topbar__brand-name">vibeSpot</span>
|
|
46
46
|
</div>
|
|
47
47
|
<div style="margin-left:auto">
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
|
141
141
|
</button>
|
|
142
142
|
<div class="topbar__brand">
|
|
143
|
-
<div class="topbar__logo-icon"
|
|
143
|
+
<div class="topbar__logo-icon"><svg viewBox="0 0 512 512" width="18" height="18"><path d="M256 76 Q280 220 436 256 Q280 292 256 436 Q232 292 76 256 Q232 220 256 76 Z" fill="currentColor"/></svg></div>
|
|
144
144
|
<span class="topbar__brand-name">vibeSpot</span>
|
|
145
145
|
</div>
|
|
146
146
|
<span class="topbar__project-pill" id="dashboard-theme-name"></span>
|
|
@@ -163,6 +163,17 @@
|
|
|
163
163
|
<div class="dashboard__body">
|
|
164
164
|
<div class="dashboard__container">
|
|
165
165
|
|
|
166
|
+
<h1 class="dashboard__theme-heading" id="dashboard-theme-heading"></h1>
|
|
167
|
+
|
|
168
|
+
<div class="dashboard__theme-path" id="dashboard-theme-path">
|
|
169
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="flex-shrink:0"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
170
|
+
<span class="dashboard__theme-path-text" id="dashboard-theme-path-text"></span>
|
|
171
|
+
<button class="dashboard__download-btn" id="dashboard-download-zip" title="Download theme as ZIP">
|
|
172
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
173
|
+
<span>Download ZIP</span>
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
166
177
|
<!-- Brand Assets -->
|
|
167
178
|
<section class="dashboard__section">
|
|
168
179
|
<div class="dashboard__section-header">
|
|
@@ -267,10 +278,9 @@
|
|
|
267
278
|
<div class="app hidden" id="app-screen">
|
|
268
279
|
<header class="topbar">
|
|
269
280
|
<div class="topbar__left">
|
|
270
|
-
<
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
</div>
|
|
281
|
+
<button class="topbar__back-btn" id="app-back" title="Back to theme overview">
|
|
282
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
|
283
|
+
</button>
|
|
274
284
|
<span class="topbar__project-pill" id="theme-name"></span>
|
|
275
285
|
</div>
|
|
276
286
|
<div class="topbar__center">
|
|
@@ -294,9 +304,6 @@
|
|
|
294
304
|
<button class="topbar__icon-btn" id="btn-history" title="Version History" style="display:none">
|
|
295
305
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
296
306
|
</button>
|
|
297
|
-
<button class="topbar__icon-btn" id="btn-settings" title="Settings">
|
|
298
|
-
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M7.5 1.5h3l.4 2.1a5.5 5.5 0 0 1 1.3.7l2-.8 1.5 2.6-1.6 1.3a5.5 5.5 0 0 1 0 1.5l1.6 1.3-1.5 2.6-2-.8a5.5 5.5 0 0 1-1.3.7l-.4 2.1h-3l-.4-2.1a5.5 5.5 0 0 1-1.3-.7l-2 .8-1.5-2.6 1.6-1.3a5.5 5.5 0 0 1 0-1.5L2.3 6.1l1.5-2.6 2 .8a5.5 5.5 0 0 1 1.3-.7L7.5 1.5Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="9" cy="9" r="2" stroke="currentColor" stroke-width="1.5"/></svg>
|
|
299
|
-
</button>
|
|
300
307
|
<button class="btn btn--primary" id="btn-upload" title="Deploy theme to HubSpot">
|
|
301
308
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px;vertical-align:-2px"><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>
|
|
302
309
|
Deploy
|