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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibespot",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
5
5
  "type": "module",
6
6
  "bin": {
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
- // Render markdown-lite (code blocks, inline code, paragraphs)
235
- streamingMsgEl.innerHTML = renderMarkdown(streamBuffer);
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
- streamingMsgEl.innerHTML = renderMarkdown(streamBuffer);
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
- // Hide vibespot-modules JSON blocks (they're data, not display)
348
- text = text.replace(/```vibespot-modules[\s\S]*?```/g, "");
349
-
350
- // Code blocks: ```lang\n...\n```
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 res = await fetch("/api/history");
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.startsWith("Rollback to:");
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(commit.message)}</div>
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 ok = await vibeConfirm("Restore this version?", "Your current files will be replaced, but chat history is preserved.", { confirmLabel: "Restore", confirmClass: "btn--primary" });
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({ hash }),
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">v</div>
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">v</div>
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
- <div class="topbar__brand">
261
- <div class="topbar__logo-icon">v</div>
262
- <span class="topbar__brand-name">vibeSpot</span>
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-20250514", label: "Claude Sonnet 4 (default)" },
823
- { id: "claude-opus-4-20250514", label: "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.0-flash", label: "Gemini 2.0 Flash (default)" },
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.5-flash", label: "Gemini 2.5 Flash" },
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-20250514";
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();