vibespot 0.5.2 → 0.7.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 +779 -180
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/chat.js +152 -18
- package/ui/dashboard.js +189 -0
- package/ui/index.html +30 -9
- package/ui/settings.js +10 -7
- package/ui/setup.js +116 -11
- package/ui/styles.css +208 -75
package/package.json
CHANGED
package/ui/chat.js
CHANGED
|
@@ -12,6 +12,9 @@ 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 = "";
|
|
15
18
|
|
|
16
19
|
const messagesEl = document.getElementById("chat-messages");
|
|
17
20
|
const inputEl = document.getElementById("chat-input");
|
|
@@ -64,6 +67,8 @@ function handleWsMessage(msg) {
|
|
|
64
67
|
|
|
65
68
|
switch (msg.type) {
|
|
66
69
|
case "init":
|
|
70
|
+
currentSessionId = msg.sessionId || "";
|
|
71
|
+
currentTemplateId = msg.templateId || "";
|
|
67
72
|
document.getElementById("theme-name").textContent = msg.themeName || "—";
|
|
68
73
|
|
|
69
74
|
// Clear previous project's chat and module list
|
|
@@ -104,7 +109,6 @@ function handleWsMessage(msg) {
|
|
|
104
109
|
break;
|
|
105
110
|
|
|
106
111
|
case "stream":
|
|
107
|
-
clearStreamStatus();
|
|
108
112
|
handleStreamChunk(msg.content);
|
|
109
113
|
break;
|
|
110
114
|
|
|
@@ -199,6 +203,7 @@ function appendUserMessage(text, timestamp) {
|
|
|
199
203
|
function startStreaming() {
|
|
200
204
|
isStreaming = true;
|
|
201
205
|
streamBuffer = "";
|
|
206
|
+
lastStreamStatus = "";
|
|
202
207
|
sendBtn.disabled = true;
|
|
203
208
|
streamStartTime = Date.now();
|
|
204
209
|
|
|
@@ -231,14 +236,32 @@ function handleStreamChunk(text) {
|
|
|
231
236
|
if (!streamingMsgEl) return;
|
|
232
237
|
streamBuffer += text;
|
|
233
238
|
|
|
234
|
-
//
|
|
235
|
-
|
|
239
|
+
// Hide incomplete code fences (AI is writing module code)
|
|
240
|
+
let display = streamBuffer;
|
|
241
|
+
const fenceCount = (display.match(/```/g) || []).length;
|
|
242
|
+
if (fenceCount % 2 !== 0) {
|
|
243
|
+
const lastFence = display.lastIndexOf("```");
|
|
244
|
+
display = display.substring(0, lastFence);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const rendered = renderMarkdown(display);
|
|
248
|
+
const visibleText = rendered.replace(/<[^>]*>/g, "").trim();
|
|
249
|
+
|
|
250
|
+
if (visibleText) {
|
|
251
|
+
// Preserve the stream-status spinner while updating text
|
|
252
|
+
const statusEl = streamingMsgEl.querySelector(".stream-status");
|
|
253
|
+
streamingMsgEl.innerHTML = rendered;
|
|
254
|
+
if (statusEl) streamingMsgEl.appendChild(statusEl);
|
|
255
|
+
}
|
|
256
|
+
// No visible text — leave the spinner (.stream-status) untouched in the DOM
|
|
236
257
|
scrollToBottom();
|
|
237
258
|
}
|
|
238
259
|
|
|
239
260
|
function handleStreamStatus(status) {
|
|
240
261
|
if (!streamingMsgEl) startStreaming();
|
|
241
262
|
|
|
263
|
+
lastStreamStatus = status;
|
|
264
|
+
|
|
242
265
|
// Find or create the status element inside the streaming bubble
|
|
243
266
|
let statusEl = streamingMsgEl.querySelector(".stream-status");
|
|
244
267
|
if (!statusEl) {
|
|
@@ -311,7 +334,9 @@ function finishStreaming() {
|
|
|
311
334
|
|
|
312
335
|
// Final render of the full response
|
|
313
336
|
if (streamingMsgEl && streamBuffer) {
|
|
314
|
-
|
|
337
|
+
const rendered = renderMarkdown(streamBuffer);
|
|
338
|
+
const visibleText = rendered.replace(/<[^>]*>/g, "").trim();
|
|
339
|
+
streamingMsgEl.innerHTML = visibleText ? rendered : "<em>Modules applied.</em>";
|
|
315
340
|
}
|
|
316
341
|
|
|
317
342
|
streamingMsgEl = null;
|
|
@@ -344,13 +369,10 @@ function appendAssistantError(message) {
|
|
|
344
369
|
// ---------------------------------------------------------------------------
|
|
345
370
|
|
|
346
371
|
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
|
-
});
|
|
372
|
+
// Strip all code blocks — module code is applied via JSON, not displayed in chat
|
|
373
|
+
text = text.replace(/```[\s\S]*?```/g, "");
|
|
374
|
+
// Also strip unclosed code fences (truncated responses)
|
|
375
|
+
text = text.replace(/```[\s\S]*$/g, "");
|
|
354
376
|
|
|
355
377
|
// Inline code: `...`
|
|
356
378
|
text = text.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
@@ -434,28 +456,46 @@ function toggleHistoryPanel() {
|
|
|
434
456
|
if (historyPanelOpen) refreshHistoryPanel();
|
|
435
457
|
}
|
|
436
458
|
|
|
459
|
+
let historyShowAll = false;
|
|
460
|
+
|
|
437
461
|
async function refreshHistoryPanel() {
|
|
438
462
|
const list = document.getElementById("history-list");
|
|
439
463
|
if (!list) return;
|
|
440
464
|
list.innerHTML = '<div class="history__loading">Loading...</div>';
|
|
441
465
|
|
|
442
466
|
try {
|
|
443
|
-
const
|
|
467
|
+
const useFilter = currentTemplateId && !historyShowAll;
|
|
468
|
+
const url = useFilter
|
|
469
|
+
? `/api/history?templateId=${encodeURIComponent(currentTemplateId)}`
|
|
470
|
+
: "/api/history";
|
|
471
|
+
const res = await fetch(url);
|
|
444
472
|
const data = await res.json();
|
|
445
473
|
|
|
446
474
|
if (!data.available) {
|
|
447
475
|
list.innerHTML = '<div class="history__empty">Git not available</div>';
|
|
448
476
|
return;
|
|
449
477
|
}
|
|
478
|
+
|
|
479
|
+
// Show all / filter toggle
|
|
480
|
+
const toggleHtml = currentTemplateId
|
|
481
|
+
? `<div class="history__toggle"><button class="history__toggle-btn" id="history-toggle-filter">${historyShowAll ? "This template" : "Show all"}</button></div>`
|
|
482
|
+
: "";
|
|
483
|
+
|
|
450
484
|
if (data.commits.length === 0) {
|
|
451
|
-
list.innerHTML = '<div class="history__empty">No versions yet</div>';
|
|
485
|
+
list.innerHTML = toggleHtml + '<div class="history__empty">No versions yet</div>';
|
|
486
|
+
attachHistoryToggle();
|
|
452
487
|
return;
|
|
453
488
|
}
|
|
454
489
|
|
|
455
|
-
list.innerHTML =
|
|
490
|
+
list.innerHTML = toggleHtml;
|
|
456
491
|
for (const commit of data.commits) {
|
|
457
492
|
const isInitial = commit.message.startsWith("Initial ");
|
|
458
|
-
const isRollback = commit.message.
|
|
493
|
+
const isRollback = commit.message.includes("Rollback to:");
|
|
494
|
+
|
|
495
|
+
// Strip [templateId] prefix from display
|
|
496
|
+
let displayMsg = commit.message;
|
|
497
|
+
const prefixMatch = displayMsg.match(/^\[[^\]]+\]\s*/);
|
|
498
|
+
if (prefixMatch) displayMsg = displayMsg.slice(prefixMatch[0].length);
|
|
459
499
|
|
|
460
500
|
const item = document.createElement("div");
|
|
461
501
|
item.className = "history-item" + (isRollback ? " history-item--rollback" : "");
|
|
@@ -464,7 +504,7 @@ async function refreshHistoryPanel() {
|
|
|
464
504
|
<span class="history-item__hash">${escapeHtml(commit.hash)}</span>
|
|
465
505
|
<span class="history-item__date">${timeAgoShort(commit.timestamp)}</span>
|
|
466
506
|
</div>
|
|
467
|
-
<div class="history-item__msg">${escapeHtml(
|
|
507
|
+
<div class="history-item__msg">${escapeHtml(displayMsg)}</div>
|
|
468
508
|
${!isInitial ? `<button class="history-item__rollback" data-hash="${escapeHtml(commit.fullHash)}">Restore</button>` : ""}
|
|
469
509
|
`;
|
|
470
510
|
list.appendChild(item);
|
|
@@ -473,21 +513,38 @@ async function refreshHistoryPanel() {
|
|
|
473
513
|
list.querySelectorAll(".history-item__rollback").forEach((btn) => {
|
|
474
514
|
btn.addEventListener("click", () => doRollback(btn.dataset.hash));
|
|
475
515
|
});
|
|
516
|
+
attachHistoryToggle();
|
|
476
517
|
} catch {
|
|
477
518
|
list.innerHTML = '<div class="history__empty">Error loading history</div>';
|
|
478
519
|
}
|
|
479
520
|
}
|
|
480
521
|
|
|
522
|
+
function attachHistoryToggle() {
|
|
523
|
+
const btn = document.getElementById("history-toggle-filter");
|
|
524
|
+
if (btn) {
|
|
525
|
+
btn.addEventListener("click", () => {
|
|
526
|
+
historyShowAll = !historyShowAll;
|
|
527
|
+
refreshHistoryPanel();
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
481
532
|
async function doRollback(hash) {
|
|
482
|
-
const
|
|
533
|
+
const scoped = currentTemplateId && !historyShowAll;
|
|
534
|
+
const msg = scoped
|
|
535
|
+
? "This template's modules will be restored to the selected version. Other templates are not affected."
|
|
536
|
+
: "All theme files will be replaced, but chat history is preserved.";
|
|
537
|
+
const ok = await vibeConfirm("Restore this version?", msg, { confirmLabel: "Restore", confirmClass: "btn--primary" });
|
|
483
538
|
if (!ok) return;
|
|
484
539
|
setStatus("Rolling back...");
|
|
485
540
|
|
|
486
541
|
try {
|
|
542
|
+
const payload = { hash };
|
|
543
|
+
if (scoped) payload.templateId = currentTemplateId;
|
|
487
544
|
const res = await fetch("/api/rollback", {
|
|
488
545
|
method: "POST",
|
|
489
546
|
headers: { "Content-Type": "application/json" },
|
|
490
|
-
body: JSON.stringify(
|
|
547
|
+
body: JSON.stringify(payload),
|
|
491
548
|
});
|
|
492
549
|
const data = await res.json();
|
|
493
550
|
|
|
@@ -991,6 +1048,83 @@ async function fetchHsAccountStatus() {
|
|
|
991
1048
|
}
|
|
992
1049
|
}
|
|
993
1050
|
|
|
1051
|
+
// ---------------------------------------------------------------------------
|
|
1052
|
+
// Topbar theme-name rename (double-click)
|
|
1053
|
+
// ---------------------------------------------------------------------------
|
|
1054
|
+
|
|
1055
|
+
document.getElementById("theme-name")?.addEventListener("dblclick", () => {
|
|
1056
|
+
const el = document.getElementById("theme-name");
|
|
1057
|
+
if (!el || !currentSessionId) return;
|
|
1058
|
+
if (el.contentEditable === "true") return;
|
|
1059
|
+
|
|
1060
|
+
const oldName = el.textContent.trim();
|
|
1061
|
+
el.contentEditable = "true";
|
|
1062
|
+
el.classList.add("topbar__project-pill--editing");
|
|
1063
|
+
el.focus();
|
|
1064
|
+
|
|
1065
|
+
const range = document.createRange();
|
|
1066
|
+
range.selectNodeContents(el);
|
|
1067
|
+
const sel = window.getSelection();
|
|
1068
|
+
sel.removeAllRanges();
|
|
1069
|
+
sel.addRange(range);
|
|
1070
|
+
|
|
1071
|
+
function commit() {
|
|
1072
|
+
el.contentEditable = "false";
|
|
1073
|
+
el.classList.remove("topbar__project-pill--editing");
|
|
1074
|
+
|
|
1075
|
+
const newName = el.textContent.trim();
|
|
1076
|
+
if (!newName || newName === oldName) {
|
|
1077
|
+
el.textContent = oldName;
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
fetch("/api/themes/rename", {
|
|
1082
|
+
method: "POST",
|
|
1083
|
+
headers: { "Content-Type": "application/json" },
|
|
1084
|
+
body: JSON.stringify({ sessionId: currentSessionId, newName }),
|
|
1085
|
+
})
|
|
1086
|
+
.then((r) => r.json())
|
|
1087
|
+
.then((data) => {
|
|
1088
|
+
if (data.ok) {
|
|
1089
|
+
el.textContent = data.newName;
|
|
1090
|
+
if (typeof currentAppTheme !== "undefined") currentAppTheme = data.newName;
|
|
1091
|
+
window.location.hash = "#/app/" + encodeURIComponent(data.newName);
|
|
1092
|
+
// Update rail item
|
|
1093
|
+
const railItem = document.querySelector(`.project-rail__item[data-name="${oldName}"]`);
|
|
1094
|
+
if (railItem) {
|
|
1095
|
+
railItem.dataset.name = data.newName;
|
|
1096
|
+
const nameSpan = railItem.querySelector(".project-rail__item-name");
|
|
1097
|
+
if (nameSpan) nameSpan.textContent = data.newName;
|
|
1098
|
+
const bubble = railItem.querySelector(".project-rail__item-bubble");
|
|
1099
|
+
if (bubble) bubble.textContent = data.newName.charAt(0).toUpperCase();
|
|
1100
|
+
}
|
|
1101
|
+
if (typeof updateRailActive === "function") updateRailActive();
|
|
1102
|
+
} else {
|
|
1103
|
+
el.textContent = oldName;
|
|
1104
|
+
showError(data.error || "Rename failed");
|
|
1105
|
+
}
|
|
1106
|
+
})
|
|
1107
|
+
.catch(() => {
|
|
1108
|
+
el.textContent = oldName;
|
|
1109
|
+
showError("Rename failed");
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
el.addEventListener("blur", commit, { once: true });
|
|
1114
|
+
el.addEventListener("keydown", function handler(e) {
|
|
1115
|
+
if (e.key === "Enter") {
|
|
1116
|
+
e.preventDefault();
|
|
1117
|
+
el.removeEventListener("keydown", handler);
|
|
1118
|
+
el.blur();
|
|
1119
|
+
}
|
|
1120
|
+
if (e.key === "Escape") {
|
|
1121
|
+
el.textContent = oldName;
|
|
1122
|
+
el.removeEventListener("keydown", handler);
|
|
1123
|
+
el.blur();
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
|
|
994
1128
|
// ---------------------------------------------------------------------------
|
|
995
1129
|
// Initialize
|
|
996
1130
|
// ---------------------------------------------------------------------------
|
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
|
}
|
|
@@ -109,6 +122,77 @@ function renderTemplateList(templates) {
|
|
|
109
122
|
list.querySelectorAll(".dashboard__template-delete").forEach((btn) => {
|
|
110
123
|
btn.addEventListener("click", () => confirmDeleteTemplate(btn.dataset.id));
|
|
111
124
|
});
|
|
125
|
+
|
|
126
|
+
// Double-click on label to rename
|
|
127
|
+
list.querySelectorAll(".dashboard__template-label").forEach((labelEl) => {
|
|
128
|
+
const item = labelEl.closest(".dashboard__template-item");
|
|
129
|
+
const templateId = item?.querySelector(".dashboard__template-open")?.dataset.id;
|
|
130
|
+
if (!templateId) return;
|
|
131
|
+
|
|
132
|
+
labelEl.addEventListener("dblclick", (e) => {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
startTemplateRename(labelEl, templateId);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function startTemplateRename(labelEl, templateId) {
|
|
140
|
+
if (labelEl.contentEditable === "true") return;
|
|
141
|
+
|
|
142
|
+
const oldLabel = labelEl.textContent.trim();
|
|
143
|
+
labelEl.contentEditable = "true";
|
|
144
|
+
labelEl.classList.add("dashboard__template-label--editing");
|
|
145
|
+
labelEl.focus();
|
|
146
|
+
|
|
147
|
+
const range = document.createRange();
|
|
148
|
+
range.selectNodeContents(labelEl);
|
|
149
|
+
const sel = window.getSelection();
|
|
150
|
+
sel.removeAllRanges();
|
|
151
|
+
sel.addRange(range);
|
|
152
|
+
|
|
153
|
+
function commit() {
|
|
154
|
+
labelEl.contentEditable = "false";
|
|
155
|
+
labelEl.classList.remove("dashboard__template-label--editing");
|
|
156
|
+
|
|
157
|
+
const newLabel = labelEl.textContent.trim();
|
|
158
|
+
if (!newLabel || newLabel === oldLabel) {
|
|
159
|
+
labelEl.textContent = oldLabel;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fetch("/api/templates/rename", {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: { "Content-Type": "application/json" },
|
|
166
|
+
body: JSON.stringify({ templateId, newLabel }),
|
|
167
|
+
})
|
|
168
|
+
.then((r) => r.json())
|
|
169
|
+
.then((data) => {
|
|
170
|
+
if (data.ok) {
|
|
171
|
+
labelEl.textContent = data.newLabel;
|
|
172
|
+
} else {
|
|
173
|
+
labelEl.textContent = oldLabel;
|
|
174
|
+
if (typeof showError === "function") showError(data.error || "Rename failed");
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
.catch(() => {
|
|
178
|
+
labelEl.textContent = oldLabel;
|
|
179
|
+
if (typeof showError === "function") showError("Rename failed");
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
labelEl.addEventListener("blur", commit, { once: true });
|
|
184
|
+
labelEl.addEventListener("keydown", function handler(e) {
|
|
185
|
+
if (e.key === "Enter") {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
labelEl.removeEventListener("keydown", handler);
|
|
188
|
+
labelEl.blur();
|
|
189
|
+
}
|
|
190
|
+
if (e.key === "Escape") {
|
|
191
|
+
labelEl.textContent = oldLabel;
|
|
192
|
+
labelEl.removeEventListener("keydown", handler);
|
|
193
|
+
labelEl.blur();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
112
196
|
}
|
|
113
197
|
|
|
114
198
|
// ---------------------------------------------------------------------------
|
|
@@ -389,6 +473,111 @@ document.getElementById("brand-upload-brandvoice").querySelector("input").addEve
|
|
|
389
473
|
if (e.target.files[0]) handleBrandFileSelected("brandvoice", e.target.files[0]);
|
|
390
474
|
});
|
|
391
475
|
|
|
476
|
+
// Dashboard theme heading — double-click to rename
|
|
477
|
+
document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick", () => {
|
|
478
|
+
const el = document.getElementById("dashboard-theme-heading");
|
|
479
|
+
if (!el || !currentDashboardSessionId) return;
|
|
480
|
+
if (el.contentEditable === "true") return;
|
|
481
|
+
|
|
482
|
+
const oldName = el.textContent.trim();
|
|
483
|
+
el.contentEditable = "true";
|
|
484
|
+
el.classList.add("dashboard__theme-heading--editing");
|
|
485
|
+
el.focus();
|
|
486
|
+
|
|
487
|
+
const range = document.createRange();
|
|
488
|
+
range.selectNodeContents(el);
|
|
489
|
+
const sel = window.getSelection();
|
|
490
|
+
sel.removeAllRanges();
|
|
491
|
+
sel.addRange(range);
|
|
492
|
+
|
|
493
|
+
function commit() {
|
|
494
|
+
el.contentEditable = "false";
|
|
495
|
+
el.classList.remove("dashboard__theme-heading--editing");
|
|
496
|
+
|
|
497
|
+
const newName = el.textContent.trim();
|
|
498
|
+
if (!newName || newName === oldName) {
|
|
499
|
+
el.textContent = oldName;
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
fetch("/api/themes/rename", {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: { "Content-Type": "application/json" },
|
|
506
|
+
body: JSON.stringify({ sessionId: currentDashboardSessionId, newName }),
|
|
507
|
+
})
|
|
508
|
+
.then((r) => r.json())
|
|
509
|
+
.then((data) => {
|
|
510
|
+
if (data.ok) {
|
|
511
|
+
el.textContent = data.newName;
|
|
512
|
+
currentDashboardTheme = data.newName;
|
|
513
|
+
document.getElementById("dashboard-theme-name").textContent = data.newName;
|
|
514
|
+
window.location.hash = "#/dashboard/" + encodeURIComponent(data.newName);
|
|
515
|
+
// Update rail
|
|
516
|
+
const railItem = document.querySelector(`.project-rail__item[data-name="${oldName}"]`);
|
|
517
|
+
if (railItem) {
|
|
518
|
+
railItem.dataset.name = data.newName;
|
|
519
|
+
const nameSpan = railItem.querySelector(".project-rail__item-name");
|
|
520
|
+
if (nameSpan) nameSpan.textContent = data.newName;
|
|
521
|
+
const bubble = railItem.querySelector(".project-rail__item-bubble");
|
|
522
|
+
if (bubble) bubble.textContent = data.newName.charAt(0).toUpperCase();
|
|
523
|
+
}
|
|
524
|
+
if (typeof updateRailActive === "function") updateRailActive();
|
|
525
|
+
} else {
|
|
526
|
+
el.textContent = oldName;
|
|
527
|
+
if (typeof showError === "function") showError(data.error || "Rename failed");
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
.catch(() => {
|
|
531
|
+
el.textContent = oldName;
|
|
532
|
+
if (typeof showError === "function") showError("Rename failed");
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
el.addEventListener("blur", commit, { once: true });
|
|
537
|
+
el.addEventListener("keydown", function handler(e) {
|
|
538
|
+
if (e.key === "Enter") {
|
|
539
|
+
e.preventDefault();
|
|
540
|
+
el.removeEventListener("keydown", handler);
|
|
541
|
+
el.blur();
|
|
542
|
+
}
|
|
543
|
+
if (e.key === "Escape") {
|
|
544
|
+
el.textContent = oldName;
|
|
545
|
+
el.removeEventListener("keydown", handler);
|
|
546
|
+
el.blur();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Download ZIP button
|
|
552
|
+
document.getElementById("dashboard-download-zip").addEventListener("click", async () => {
|
|
553
|
+
const btn = document.getElementById("dashboard-download-zip");
|
|
554
|
+
const origHTML = btn.innerHTML;
|
|
555
|
+
btn.disabled = true;
|
|
556
|
+
btn.querySelector("span").textContent = "Downloading...";
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const res = await fetch("/api/download-zip");
|
|
560
|
+
if (!res.ok) {
|
|
561
|
+
const err = await res.json().catch(() => ({ error: "Download failed" }));
|
|
562
|
+
throw new Error(err.error || "Download failed");
|
|
563
|
+
}
|
|
564
|
+
const blob = await res.blob();
|
|
565
|
+
const url = URL.createObjectURL(blob);
|
|
566
|
+
const a = document.createElement("a");
|
|
567
|
+
a.href = url;
|
|
568
|
+
a.download = (currentDashboardTheme || "theme") + ".zip";
|
|
569
|
+
document.body.appendChild(a);
|
|
570
|
+
a.click();
|
|
571
|
+
a.remove();
|
|
572
|
+
URL.revokeObjectURL(url);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
if (typeof vibeAlert === "function") vibeAlert(err.message, "Error");
|
|
575
|
+
} finally {
|
|
576
|
+
btn.disabled = false;
|
|
577
|
+
btn.innerHTML = origHTML;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
392
581
|
// Humanify toggle
|
|
393
582
|
const humanifyCheckbox = document.getElementById("humanify-checkbox");
|
|
394
583
|
if (humanifyCheckbox) {
|
package/ui/index.html
CHANGED
|
@@ -41,9 +41,15 @@
|
|
|
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
|
+
<div style="margin-left:auto">
|
|
48
|
+
<button class="topbar__icon-btn theme-toggle" title="Toggle light/dark mode" onclick="toggleTheme()">
|
|
49
|
+
<svg class="theme-toggle__icon--dark" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
|
50
|
+
<svg class="theme-toggle__icon--light" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
47
53
|
</div>
|
|
48
54
|
<div class="setup" id="setup-screen">
|
|
49
55
|
<div class="setup__main">
|
|
@@ -134,12 +140,16 @@
|
|
|
134
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>
|
|
135
141
|
</button>
|
|
136
142
|
<div class="topbar__brand">
|
|
137
|
-
<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>
|
|
138
144
|
<span class="topbar__brand-name">vibeSpot</span>
|
|
139
145
|
</div>
|
|
140
146
|
<span class="topbar__project-pill" id="dashboard-theme-name"></span>
|
|
141
147
|
</div>
|
|
142
148
|
<div class="topbar__right">
|
|
149
|
+
<button class="topbar__icon-btn theme-toggle" title="Toggle light/dark mode" onclick="toggleTheme()">
|
|
150
|
+
<svg class="theme-toggle__icon--dark" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
|
151
|
+
<svg class="theme-toggle__icon--light" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
152
|
+
</button>
|
|
143
153
|
<button class="topbar__icon-btn" id="dashboard-settings-btn" title="Settings">
|
|
144
154
|
<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>
|
|
145
155
|
</button>
|
|
@@ -153,6 +163,17 @@
|
|
|
153
163
|
<div class="dashboard__body">
|
|
154
164
|
<div class="dashboard__container">
|
|
155
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
|
+
|
|
156
177
|
<!-- Brand Assets -->
|
|
157
178
|
<section class="dashboard__section">
|
|
158
179
|
<div class="dashboard__section-header">
|
|
@@ -257,10 +278,9 @@
|
|
|
257
278
|
<div class="app hidden" id="app-screen">
|
|
258
279
|
<header class="topbar">
|
|
259
280
|
<div class="topbar__left">
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
</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>
|
|
264
284
|
<span class="topbar__project-pill" id="theme-name"></span>
|
|
265
285
|
</div>
|
|
266
286
|
<div class="topbar__center">
|
|
@@ -277,12 +297,13 @@
|
|
|
277
297
|
</div>
|
|
278
298
|
</div>
|
|
279
299
|
<div class="topbar__right">
|
|
300
|
+
<button class="topbar__icon-btn theme-toggle" title="Toggle light/dark mode" onclick="toggleTheme()">
|
|
301
|
+
<svg class="theme-toggle__icon--dark" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
|
302
|
+
<svg class="theme-toggle__icon--light" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
303
|
+
</button>
|
|
280
304
|
<button class="topbar__icon-btn" id="btn-history" title="Version History" style="display:none">
|
|
281
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>
|
|
282
306
|
</button>
|
|
283
|
-
<button class="topbar__icon-btn" id="btn-settings" title="Settings">
|
|
284
|
-
<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>
|
|
285
|
-
</button>
|
|
286
307
|
<button class="btn btn--primary" id="btn-upload" title="Deploy theme to HubSpot">
|
|
287
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>
|
|
288
309
|
Deploy
|
package/ui/settings.js
CHANGED
|
@@ -810,6 +810,11 @@ async function authCLI(cli, btn, apiKey) {
|
|
|
810
810
|
// ---------------------------------------------------------------------------
|
|
811
811
|
|
|
812
812
|
function getModelsForEngine(engine) {
|
|
813
|
+
// Use server-provided model catalog if available
|
|
814
|
+
if (settingsData && settingsData.models && settingsData.models[engine]) {
|
|
815
|
+
return settingsData.models[engine];
|
|
816
|
+
}
|
|
817
|
+
// Fallback to hardcoded defaults
|
|
813
818
|
switch (engine) {
|
|
814
819
|
case "claude-code":
|
|
815
820
|
return [
|
|
@@ -819,10 +824,9 @@ function getModelsForEngine(engine) {
|
|
|
819
824
|
];
|
|
820
825
|
case "anthropic-api":
|
|
821
826
|
return [
|
|
822
|
-
{ id: "claude-sonnet-4-
|
|
823
|
-
{ id: "claude-opus-4-
|
|
827
|
+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
|
828
|
+
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
824
829
|
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
|
|
825
|
-
{ id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
|
|
826
830
|
];
|
|
827
831
|
case "openai-api":
|
|
828
832
|
return [
|
|
@@ -834,9 +838,9 @@ function getModelsForEngine(engine) {
|
|
|
834
838
|
case "gemini-cli":
|
|
835
839
|
case "gemini-api":
|
|
836
840
|
return [
|
|
837
|
-
{ id: "gemini-2.
|
|
841
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash (default)" },
|
|
838
842
|
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
|
839
|
-
{ id: "gemini-2.
|
|
843
|
+
{ id: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
|
|
840
844
|
];
|
|
841
845
|
case "codex-cli":
|
|
842
846
|
return [
|
|
@@ -852,7 +856,7 @@ function getModelsForEngine(engine) {
|
|
|
852
856
|
function getCurrentModel(engine, config) {
|
|
853
857
|
switch (engine) {
|
|
854
858
|
case "claude-code": return config.claudeCodeModel || "sonnet";
|
|
855
|
-
case "anthropic-api": return config.anthropicApiModel || "claude-sonnet-4-
|
|
859
|
+
case "anthropic-api": return config.anthropicApiModel || "claude-sonnet-4-6";
|
|
856
860
|
case "openai-api": return config.openaiApiModel || "gpt-4o";
|
|
857
861
|
default: return null;
|
|
858
862
|
}
|
|
@@ -910,7 +914,6 @@ function escSettings(str) {
|
|
|
910
914
|
// Event listeners
|
|
911
915
|
// ---------------------------------------------------------------------------
|
|
912
916
|
|
|
913
|
-
document.getElementById("btn-settings").addEventListener("click", openSettings);
|
|
914
917
|
document.getElementById("settings-close").addEventListener("click", closeSettings);
|
|
915
918
|
document.getElementById("settings-overlay").addEventListener("click", (e) => {
|
|
916
919
|
if (e.target.id === "settings-overlay") closeSettings();
|