vibespot 1.2.0 → 1.3.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/README.md +44 -5
- package/assets/blog-rules.md +251 -0
- package/assets/email-rules.md +390 -0
- package/assets/humanify-guide.md +300 -101
- package/assets/plan-templates/blog-content-hub.md +18 -9
- package/assets/plan-templates/email-announcement.md +41 -0
- package/assets/plan-templates/email-event-invite.md +43 -0
- package/assets/plan-templates/email-newsletter.md +41 -0
- package/assets/plan-templates/email-re-engagement.md +42 -0
- package/assets/plan-templates/email-welcome.md +41 -0
- package/dist/index.js +1460 -387
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/starters/06-blog-content-hub.json +75 -0
- package/starters/06-email-welcome.json +60 -0
- package/starters/07-email-announcement.json +60 -0
- package/starters/08-email-newsletter.json +52 -0
- package/ui/chat.js +777 -63
- package/ui/code-editor.js +49 -7
- package/ui/dashboard.js +379 -93
- package/ui/docs/docs.css +29 -0
- package/ui/docs/index.html +186 -108
- package/ui/docs/screenshots/brand-kit-preview.png +0 -0
- package/ui/docs/screenshots/content-type-dropdown.png +0 -0
- package/ui/docs/screenshots/editor-full-layout.png +0 -0
- package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
- package/ui/docs/screenshots/multi-page-tree.png +0 -0
- package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
- package/ui/docs/screenshots/split-pane-view.png +0 -0
- package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
- package/ui/docs/screenshots/workspace-tabs.png +0 -0
- package/ui/email-preview.js +109 -0
- package/ui/field-editor.js +72 -1
- package/ui/icons.js +120 -0
- package/ui/index.html +877 -629
- package/ui/inline-edit.js +710 -0
- package/ui/plan.js +0 -0
- package/ui/preview.js +101 -198
- package/ui/section-controls.js +628 -0
- package/ui/settings.js +58 -16
- package/ui/setup.js +750 -140
- package/ui/styles.css +3430 -952
- package/ui/upload-panel.js +47 -20
package/ui/chat.js
CHANGED
|
@@ -34,6 +34,10 @@ const inputEl = document.getElementById("chat-input");
|
|
|
34
34
|
const sendBtn = document.getElementById("chat-send");
|
|
35
35
|
const statusText = document.getElementById("status-text");
|
|
36
36
|
const statusEngine = document.getElementById("status-engine");
|
|
37
|
+
const statusTheme = document.getElementById("status-theme");
|
|
38
|
+
const statusLastSaved = document.getElementById("status-last-saved");
|
|
39
|
+
|
|
40
|
+
let lastSavedAt = 0;
|
|
37
41
|
|
|
38
42
|
// Snapshot the welcome section before any init can destroy it
|
|
39
43
|
const _welcomeHtml = document.getElementById("chat-welcome")?.outerHTML || "";
|
|
@@ -41,15 +45,480 @@ const _welcomeHtml = document.getElementById("chat-welcome")?.outerHTML || "";
|
|
|
41
45
|
function restoreWelcome() {
|
|
42
46
|
if (!_welcomeHtml || messagesEl.querySelector(".chat__welcome")) return;
|
|
43
47
|
messagesEl.insertAdjacentHTML("afterbegin", _welcomeHtml);
|
|
44
|
-
const el = messagesEl.querySelector("#
|
|
45
|
-
if (el)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const el = messagesEl.querySelector("#conversation-starters");
|
|
49
|
+
if (el) el.addEventListener("click", handleConversationStarterClick);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleConversationStarterClick(e) {
|
|
53
|
+
const btn = e.target.closest(".starter-btn");
|
|
54
|
+
if (!btn) return;
|
|
55
|
+
const action = btn.dataset.action;
|
|
56
|
+
if (action === "describe") {
|
|
57
|
+
inputEl?.focus();
|
|
58
|
+
} else if (action === "figma") {
|
|
59
|
+
document.getElementById("btn-attach-file")?.click();
|
|
60
|
+
} else if (action === "hubspot-import") {
|
|
61
|
+
if (inputEl) {
|
|
62
|
+
inputEl.value = "Import this HubSpot page and adapt it: ";
|
|
63
|
+
inputEl.style.height = "auto";
|
|
64
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + "px";
|
|
65
|
+
inputEl.focus();
|
|
66
|
+
const end = inputEl.value.length;
|
|
67
|
+
inputEl.setSelectionRange(end, end);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Page tabs (multi-page template switcher)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
let currentTemplates = [];
|
|
77
|
+
let pageDragSourceId = null;
|
|
78
|
+
|
|
79
|
+
const PAGE_TYPE_BADGES = {
|
|
80
|
+
landing_page: "LP",
|
|
81
|
+
blog_post: "Blog",
|
|
82
|
+
website_page: "Web",
|
|
83
|
+
email: "Email",
|
|
84
|
+
module_only: "Sec",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Inline SVG icons keyed by page type. Stroke-only so they inherit currentColor.
|
|
88
|
+
const PAGE_TYPE_ICONS = {
|
|
89
|
+
landing_page: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="4" y1="9" x2="20" y2="9"/><line x1="9" y1="13" x2="15" y2="13"/></svg>',
|
|
90
|
+
website_page: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><circle cx="6" cy="7" r="0.6" fill="currentColor"/><circle cx="8.4" cy="7" r="0.6" fill="currentColor"/></svg>',
|
|
91
|
+
blog_post: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 4h11l3 3v13H5z"/><line x1="8" y1="11" x2="16" y2="11"/><line x1="8" y1="15" x2="14" y2="15"/></svg>',
|
|
92
|
+
email: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><polyline points="3,7 12,13 21,7"/></svg>',
|
|
93
|
+
module_only: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="6" width="16" height="4" rx="1"/><rect x="4" y="14" width="10" height="4" rx="1"/></svg>',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const PAGE_TYPE_LABEL_LONG = {
|
|
97
|
+
landing_page: "Landing page",
|
|
98
|
+
website_page: "Website page",
|
|
99
|
+
blog_post: "Blog post",
|
|
100
|
+
email: "Email",
|
|
101
|
+
module_only: "Section",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function renderPageTabs(templates, activeTemplateId) {
|
|
105
|
+
const list = document.getElementById("page-tabs-list");
|
|
106
|
+
if (!list) return;
|
|
107
|
+
|
|
108
|
+
currentTemplates = templates || [];
|
|
109
|
+
|
|
110
|
+
list.innerHTML = currentTemplates.map((t) => {
|
|
111
|
+
const isActive = t.id === activeTemplateId;
|
|
112
|
+
const badge = PAGE_TYPE_BADGES[t.pageType] || "";
|
|
113
|
+
const icon = PAGE_TYPE_ICONS[t.pageType] || PAGE_TYPE_ICONS.website_page;
|
|
114
|
+
const typeLabel = PAGE_TYPE_LABEL_LONG[t.pageType] || t.pageType;
|
|
115
|
+
const titleAttr = `${t.label} — ${typeLabel} (${t.moduleCount} modules)`;
|
|
116
|
+
return `<div class="page-tabs__row${isActive ? " page-tabs__row--active" : ""}" role="treeitem" aria-selected="${isActive ? "true" : "false"}" data-template-id="${t.id}" draggable="true" title="${escapeHtml(titleAttr)}">
|
|
117
|
+
<span class="page-tabs__handle" aria-hidden="true" title="Drag to reorder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="18" r="1"/></svg></span>
|
|
118
|
+
<button class="page-tabs__tab" type="button" data-template-id="${t.id}" data-action="activate">
|
|
119
|
+
<span class="page-tabs__icon" aria-hidden="true">${icon}</span>
|
|
120
|
+
${badge ? `<span class="page-tabs__tab-badge">${badge}</span>` : ""}
|
|
121
|
+
<span class="page-tabs__tab-label">${escapeHtml(t.label)}</span>
|
|
122
|
+
<span class="page-tabs__tab-count">${t.moduleCount}</span>
|
|
123
|
+
</button>
|
|
124
|
+
<button class="page-tabs__menu-btn" type="button" data-template-id="${t.id}" data-action="menu" title="Page actions" aria-label="Page actions" aria-haspopup="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="6" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="18" r="1"/></svg></button>
|
|
125
|
+
</div>`;
|
|
126
|
+
}).join("");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function refreshPageTabs() {
|
|
130
|
+
return fetch("/api/templates")
|
|
131
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
132
|
+
.then((data) => {
|
|
133
|
+
if (!data) return;
|
|
134
|
+
currentTemplateId = data.activeTemplateId || currentTemplateId;
|
|
135
|
+
renderPageTabs(data.templates || [], currentTemplateId);
|
|
136
|
+
})
|
|
137
|
+
.catch(() => {});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handlePageTabClick(e) {
|
|
141
|
+
const menuBtn = e.target.closest(".page-tabs__menu-btn");
|
|
142
|
+
if (menuBtn) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
openPageContextMenu(menuBtn.dataset.templateId, menuBtn);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const tab = e.target.closest(".page-tabs__tab");
|
|
149
|
+
if (!tab) return;
|
|
150
|
+
const templateId = tab.dataset.templateId;
|
|
151
|
+
if (!templateId || templateId === currentTemplateId) return;
|
|
152
|
+
if (isStreaming) return;
|
|
153
|
+
|
|
154
|
+
tab.style.opacity = "0.5";
|
|
155
|
+
fetch("/api/templates/activate", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify({ templateId }),
|
|
159
|
+
}).then((res) => {
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
tab.style.opacity = "";
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
connectWebSocket();
|
|
165
|
+
}).catch(() => {
|
|
166
|
+
tab.style.opacity = "";
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Page tree context menu (right-click / kebab) — Edit, Duplicate, Delete, Move
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
function handlePageTabContextMenu(e) {
|
|
175
|
+
const row = e.target.closest(".page-tabs__row");
|
|
176
|
+
if (!row) return;
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
openPageContextMenu(row.dataset.templateId, e);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function openPageContextMenu(templateId, anchor) {
|
|
182
|
+
closePageContextMenu();
|
|
183
|
+
if (!templateId) return;
|
|
184
|
+
const template = currentTemplates.find((t) => t.id === templateId);
|
|
185
|
+
if (!template) return;
|
|
186
|
+
|
|
187
|
+
const idx = currentTemplates.findIndex((t) => t.id === templateId);
|
|
188
|
+
const canMoveUp = idx > 0;
|
|
189
|
+
const canMoveDown = idx >= 0 && idx < currentTemplates.length - 1;
|
|
190
|
+
|
|
191
|
+
const menu = document.createElement("div");
|
|
192
|
+
menu.className = "page-tree__context-menu";
|
|
193
|
+
menu.id = "page-context-menu";
|
|
194
|
+
menu.setAttribute("role", "menu");
|
|
195
|
+
menu.innerHTML = [
|
|
196
|
+
`<button type="button" role="menuitem" data-action="edit">Edit name…</button>`,
|
|
197
|
+
`<button type="button" role="menuitem" data-action="duplicate">Duplicate</button>`,
|
|
198
|
+
`<div class="page-tree__context-sep" role="separator"></div>`,
|
|
199
|
+
`<button type="button" role="menuitem" data-action="move-up"${canMoveUp ? "" : " disabled"}>Move up</button>`,
|
|
200
|
+
`<button type="button" role="menuitem" data-action="move-down"${canMoveDown ? "" : " disabled"}>Move down</button>`,
|
|
201
|
+
`<div class="page-tree__context-sep" role="separator"></div>`,
|
|
202
|
+
`<button type="button" role="menuitem" data-action="delete" class="page-tree__context-menu-danger">Delete…</button>`,
|
|
203
|
+
].join("");
|
|
204
|
+
document.body.appendChild(menu);
|
|
205
|
+
|
|
206
|
+
// Position the menu. Anchor can be a MouseEvent or an HTMLElement.
|
|
207
|
+
let x = 0;
|
|
208
|
+
let y = 0;
|
|
209
|
+
if (anchor instanceof Event) {
|
|
210
|
+
x = anchor.clientX;
|
|
211
|
+
y = anchor.clientY;
|
|
212
|
+
} else if (anchor && anchor.getBoundingClientRect) {
|
|
213
|
+
const r = anchor.getBoundingClientRect();
|
|
214
|
+
x = r.right;
|
|
215
|
+
y = r.bottom;
|
|
216
|
+
}
|
|
217
|
+
// Constrain to viewport.
|
|
218
|
+
const menuRect = menu.getBoundingClientRect();
|
|
219
|
+
if (x + menuRect.width > window.innerWidth - 8) x = Math.max(8, window.innerWidth - menuRect.width - 8);
|
|
220
|
+
if (y + menuRect.height > window.innerHeight - 8) y = Math.max(8, window.innerHeight - menuRect.height - 8);
|
|
221
|
+
menu.style.left = `${x}px`;
|
|
222
|
+
menu.style.top = `${y}px`;
|
|
223
|
+
|
|
224
|
+
menu.addEventListener("click", (e) => {
|
|
225
|
+
const btn = e.target.closest("button[data-action]");
|
|
226
|
+
if (!btn || btn.disabled) return;
|
|
227
|
+
const action = btn.dataset.action;
|
|
228
|
+
closePageContextMenu();
|
|
229
|
+
runPageAction(templateId, action);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Dismiss on outside click / scroll / Escape.
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
document.addEventListener("click", closePageContextMenu, { once: true });
|
|
235
|
+
document.addEventListener("contextmenu", closePageContextMenu, { once: true });
|
|
236
|
+
window.addEventListener("scroll", closePageContextMenu, { capture: true, once: true });
|
|
237
|
+
}, 0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function closePageContextMenu() {
|
|
241
|
+
const menu = document.getElementById("page-context-menu");
|
|
242
|
+
if (menu) menu.remove();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
document.addEventListener("keydown", (e) => {
|
|
246
|
+
if (e.key === "Escape") closePageContextMenu();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
function runPageAction(templateId, action) {
|
|
250
|
+
if (isStreaming) return;
|
|
251
|
+
switch (action) {
|
|
252
|
+
case "edit": return renamePageAction(templateId);
|
|
253
|
+
case "duplicate": return duplicatePageAction(templateId);
|
|
254
|
+
case "delete": return deletePageAction(templateId);
|
|
255
|
+
case "move-up": return movePageAction(templateId, -1);
|
|
256
|
+
case "move-down": return movePageAction(templateId, 1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renamePageAction(templateId) {
|
|
261
|
+
const tpl = currentTemplates.find((t) => t.id === templateId);
|
|
262
|
+
if (!tpl) return;
|
|
263
|
+
// Inline rename: turn the label cell into a contenteditable.
|
|
264
|
+
const row = document.querySelector(`.page-tabs__row[data-template-id="${CSS.escape(templateId)}"]`);
|
|
265
|
+
const labelEl = row && row.querySelector(".page-tabs__tab-label");
|
|
266
|
+
if (!labelEl) {
|
|
267
|
+
// Fallback: native prompt.
|
|
268
|
+
const next = prompt("Rename page", tpl.label);
|
|
269
|
+
if (!next || next.trim() === tpl.label) return;
|
|
270
|
+
sendRename(templateId, next.trim());
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const oldLabel = tpl.label;
|
|
274
|
+
labelEl.contentEditable = "true";
|
|
275
|
+
labelEl.classList.add("page-tabs__tab-label--editing");
|
|
276
|
+
labelEl.focus();
|
|
277
|
+
const range = document.createRange();
|
|
278
|
+
range.selectNodeContents(labelEl);
|
|
279
|
+
const sel = window.getSelection();
|
|
280
|
+
sel.removeAllRanges();
|
|
281
|
+
sel.addRange(range);
|
|
282
|
+
|
|
283
|
+
let committed = false;
|
|
284
|
+
function commit() {
|
|
285
|
+
if (committed) return;
|
|
286
|
+
committed = true;
|
|
287
|
+
labelEl.contentEditable = "false";
|
|
288
|
+
labelEl.classList.remove("page-tabs__tab-label--editing");
|
|
289
|
+
const next = labelEl.textContent.trim();
|
|
290
|
+
if (!next || next === oldLabel) {
|
|
291
|
+
labelEl.textContent = oldLabel;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
sendRename(templateId, next);
|
|
295
|
+
}
|
|
296
|
+
labelEl.addEventListener("blur", commit, { once: true });
|
|
297
|
+
labelEl.addEventListener("keydown", function handler(e) {
|
|
298
|
+
if (e.key === "Enter") {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
labelEl.removeEventListener("keydown", handler);
|
|
301
|
+
commit();
|
|
302
|
+
} else if (e.key === "Escape") {
|
|
303
|
+
labelEl.removeEventListener("keydown", handler);
|
|
304
|
+
committed = true;
|
|
305
|
+
labelEl.textContent = oldLabel;
|
|
306
|
+
labelEl.contentEditable = "false";
|
|
307
|
+
labelEl.classList.remove("page-tabs__tab-label--editing");
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function sendRename(templateId, newLabel) {
|
|
313
|
+
fetch("/api/templates/rename", {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers: { "Content-Type": "application/json" },
|
|
316
|
+
body: JSON.stringify({ templateId, newLabel }),
|
|
317
|
+
}).then((r) => r.json()).then((data) => {
|
|
318
|
+
if (data && data.ok) refreshPageTabs();
|
|
319
|
+
}).catch(() => {});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function duplicatePageAction(templateId) {
|
|
323
|
+
const tpl = currentTemplates.find((t) => t.id === templateId);
|
|
324
|
+
if (!tpl) return;
|
|
325
|
+
const label = prompt("Name for the duplicated page:", `${tpl.label} (Copy)`);
|
|
326
|
+
if (!label || !label.trim()) return;
|
|
327
|
+
fetch("/api/templates/clone", {
|
|
328
|
+
method: "POST",
|
|
329
|
+
headers: { "Content-Type": "application/json" },
|
|
330
|
+
body: JSON.stringify({ templateId, label: label.trim() }),
|
|
331
|
+
}).then((r) => r.json()).then((data) => {
|
|
332
|
+
if (data && data.ok && data.template && data.template.id) {
|
|
333
|
+
// Activate the clone so the user lands on it (mirrors createPage flow).
|
|
334
|
+
return fetch("/api/templates/activate", {
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: { "Content-Type": "application/json" },
|
|
337
|
+
body: JSON.stringify({ templateId: data.template.id }),
|
|
338
|
+
}).then(() => { refreshPageTabs(); connectWebSocket(); });
|
|
339
|
+
}
|
|
340
|
+
}).catch(() => {});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function deletePageAction(templateId) {
|
|
344
|
+
const tpl = currentTemplates.find((t) => t.id === templateId);
|
|
345
|
+
if (!tpl) return;
|
|
346
|
+
const msg = `Delete "${tpl.label}"? This removes the page and its template file. Sections (modules) are kept in the library.`;
|
|
347
|
+
if (!confirm(msg)) return;
|
|
348
|
+
fetch("/api/templates", {
|
|
349
|
+
method: "DELETE",
|
|
350
|
+
headers: { "Content-Type": "application/json" },
|
|
351
|
+
body: JSON.stringify({ templateId, deleteModules: false }),
|
|
352
|
+
}).then((r) => r.json()).then((data) => {
|
|
353
|
+
if (data && data.ok) {
|
|
354
|
+
refreshPageTabs();
|
|
355
|
+
// If we deleted the active page, the server picks a new active — reconnect.
|
|
356
|
+
if (templateId === currentTemplateId) connectWebSocket();
|
|
357
|
+
}
|
|
358
|
+
}).catch(() => {});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function movePageAction(templateId, delta) {
|
|
362
|
+
const ids = currentTemplates.map((t) => t.id);
|
|
363
|
+
const idx = ids.indexOf(templateId);
|
|
364
|
+
if (idx < 0) return;
|
|
365
|
+
const target = idx + delta;
|
|
366
|
+
if (target < 0 || target >= ids.length) return;
|
|
367
|
+
ids.splice(idx, 1);
|
|
368
|
+
ids.splice(target, 0, templateId);
|
|
369
|
+
sendReorder(ids);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function sendReorder(templateIds) {
|
|
373
|
+
// Optimistic re-render so the move feels instant.
|
|
374
|
+
const map = new Map(currentTemplates.map((t) => [t.id, t]));
|
|
375
|
+
const next = templateIds.map((id) => map.get(id)).filter(Boolean);
|
|
376
|
+
if (next.length === currentTemplates.length) {
|
|
377
|
+
renderPageTabs(next, currentTemplateId);
|
|
378
|
+
}
|
|
379
|
+
fetch("/api/templates/reorder", {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: { "Content-Type": "application/json" },
|
|
382
|
+
body: JSON.stringify({ templateIds }),
|
|
383
|
+
}).then((r) => r.json()).then((data) => {
|
|
384
|
+
if (!data || !data.ok) refreshPageTabs();
|
|
385
|
+
}).catch(() => refreshPageTabs());
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Drag and drop reordering (HTML5)
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
function handlePageTabDragStart(e) {
|
|
393
|
+
const row = e.target.closest(".page-tabs__row");
|
|
394
|
+
if (!row) return;
|
|
395
|
+
pageDragSourceId = row.dataset.templateId || null;
|
|
396
|
+
row.classList.add("page-tabs__row--dragging");
|
|
397
|
+
if (e.dataTransfer) {
|
|
398
|
+
e.dataTransfer.effectAllowed = "move";
|
|
399
|
+
// Some browsers need data set to start the drag.
|
|
400
|
+
try { e.dataTransfer.setData("text/plain", pageDragSourceId || ""); } catch (_) {}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function handlePageTabDragEnd(e) {
|
|
405
|
+
const row = e.target.closest(".page-tabs__row");
|
|
406
|
+
if (row) row.classList.remove("page-tabs__row--dragging");
|
|
407
|
+
pageDragSourceId = null;
|
|
408
|
+
document.querySelectorAll(".page-tabs__row--drop-before, .page-tabs__row--drop-after")
|
|
409
|
+
.forEach((el) => el.classList.remove("page-tabs__row--drop-before", "page-tabs__row--drop-after"));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function handlePageTabDragOver(e) {
|
|
413
|
+
if (!pageDragSourceId) return;
|
|
414
|
+
const row = e.target.closest(".page-tabs__row");
|
|
415
|
+
if (!row || row.dataset.templateId === pageDragSourceId) return;
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
418
|
+
const rect = row.getBoundingClientRect();
|
|
419
|
+
const before = e.clientY - rect.top < rect.height / 2;
|
|
420
|
+
document.querySelectorAll(".page-tabs__row--drop-before, .page-tabs__row--drop-after")
|
|
421
|
+
.forEach((el) => el.classList.remove("page-tabs__row--drop-before", "page-tabs__row--drop-after"));
|
|
422
|
+
row.classList.add(before ? "page-tabs__row--drop-before" : "page-tabs__row--drop-after");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function handlePageTabDrop(e) {
|
|
426
|
+
if (!pageDragSourceId) return;
|
|
427
|
+
const row = e.target.closest(".page-tabs__row");
|
|
428
|
+
if (!row) return;
|
|
429
|
+
const targetId = row.dataset.templateId;
|
|
430
|
+
if (!targetId || targetId === pageDragSourceId) return;
|
|
431
|
+
e.preventDefault();
|
|
432
|
+
const ids = currentTemplates.map((t) => t.id);
|
|
433
|
+
const from = ids.indexOf(pageDragSourceId);
|
|
434
|
+
const to = ids.indexOf(targetId);
|
|
435
|
+
if (from < 0 || to < 0) return;
|
|
436
|
+
const rect = row.getBoundingClientRect();
|
|
437
|
+
const before = e.clientY - rect.top < rect.height / 2;
|
|
438
|
+
ids.splice(from, 1);
|
|
439
|
+
// After removing the source, the target index may have shifted by one.
|
|
440
|
+
const adjustedTo = to > from ? to - 1 : to;
|
|
441
|
+
const insertAt = before ? adjustedTo : adjustedTo + 1;
|
|
442
|
+
ids.splice(insertAt, 0, pageDragSourceId);
|
|
443
|
+
sendReorder(ids);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function handleAddPage() {
|
|
447
|
+
if (isStreaming) return;
|
|
448
|
+
const inlineForm = document.getElementById("page-tree-add-inline");
|
|
449
|
+
if (inlineForm) {
|
|
450
|
+
inlineForm.classList.toggle("hidden");
|
|
451
|
+
if (!inlineForm.classList.contains("hidden")) {
|
|
452
|
+
document.getElementById("page-tree-name")?.focus();
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
50
455
|
}
|
|
456
|
+
const label = prompt("Page name:");
|
|
457
|
+
if (!label || !label.trim()) return;
|
|
458
|
+
createPage("website_page", label.trim());
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function handleInlineCreate() {
|
|
462
|
+
if (isStreaming) return;
|
|
463
|
+
const nameInput = document.getElementById("page-tree-name");
|
|
464
|
+
const typeSelect = document.getElementById("page-tree-type");
|
|
465
|
+
const label = nameInput?.value?.trim();
|
|
466
|
+
if (!label) { nameInput?.focus(); return; }
|
|
467
|
+
const pageType = typeSelect?.value || "website_page";
|
|
468
|
+
createPage(pageType, label);
|
|
469
|
+
nameInput.value = "";
|
|
470
|
+
document.getElementById("page-tree-add-inline")?.classList.add("hidden");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function createPage(pageType, label) {
|
|
474
|
+
fetch("/api/templates", {
|
|
475
|
+
method: "POST",
|
|
476
|
+
headers: { "Content-Type": "application/json" },
|
|
477
|
+
body: JSON.stringify({ pageType, label }),
|
|
478
|
+
}).then((res) => {
|
|
479
|
+
if (!res.ok) return null;
|
|
480
|
+
return res.json();
|
|
481
|
+
}).then((data) => {
|
|
482
|
+
const newId = data && data.template && data.template.id;
|
|
483
|
+
if (!newId) return;
|
|
484
|
+
// Server's addTemplate already marks the new template as active, but we
|
|
485
|
+
// call activate to sync the flat-field cache and let the WS reconnect
|
|
486
|
+
// re-broadcast the full session (modules + templates + active id).
|
|
487
|
+
return fetch("/api/templates/activate", {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: { "Content-Type": "application/json" },
|
|
490
|
+
body: JSON.stringify({ templateId: newId }),
|
|
491
|
+
}).then((res) => {
|
|
492
|
+
if (!res || !res.ok) return;
|
|
493
|
+
// Re-render the tree immediately from the canonical list so the new
|
|
494
|
+
// page shows up even before the WS init message arrives.
|
|
495
|
+
refreshPageTabs();
|
|
496
|
+
connectWebSocket();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
51
499
|
}
|
|
52
500
|
|
|
501
|
+
(function initPageTree() {
|
|
502
|
+
const list = document.getElementById("page-tabs-list");
|
|
503
|
+
const addBtn = document.getElementById("btn-add-page");
|
|
504
|
+
const createBtn = document.getElementById("page-tree-create");
|
|
505
|
+
const nameInput = document.getElementById("page-tree-name");
|
|
506
|
+
if (list) {
|
|
507
|
+
list.addEventListener("click", handlePageTabClick);
|
|
508
|
+
list.addEventListener("contextmenu", handlePageTabContextMenu);
|
|
509
|
+
list.addEventListener("dragstart", handlePageTabDragStart);
|
|
510
|
+
list.addEventListener("dragend", handlePageTabDragEnd);
|
|
511
|
+
list.addEventListener("dragover", handlePageTabDragOver);
|
|
512
|
+
list.addEventListener("drop", handlePageTabDrop);
|
|
513
|
+
}
|
|
514
|
+
if (addBtn) addBtn.addEventListener("click", handleAddPage);
|
|
515
|
+
if (createBtn) createBtn.addEventListener("click", handleInlineCreate);
|
|
516
|
+
if (nameInput) nameInput.addEventListener("keydown", (e) => {
|
|
517
|
+
if (e.key === "Enter") handleInlineCreate();
|
|
518
|
+
if (e.key === "Escape") document.getElementById("page-tree-add-inline")?.classList.add("hidden");
|
|
519
|
+
});
|
|
520
|
+
})();
|
|
521
|
+
|
|
53
522
|
// ---------------------------------------------------------------------------
|
|
54
523
|
// WebSocket connection
|
|
55
524
|
// ---------------------------------------------------------------------------
|
|
@@ -103,11 +572,15 @@ function handleWsMessage(msg) {
|
|
|
103
572
|
currentSessionId = msg.sessionId || "";
|
|
104
573
|
currentTemplateId = msg.templateId || "";
|
|
105
574
|
document.getElementById("theme-name").textContent = msg.themeName || "—";
|
|
575
|
+
setStatusTheme(msg.themeName);
|
|
576
|
+
lastSavedAt = typeof msg.updatedAt === "number" ? msg.updatedAt : 0;
|
|
577
|
+
renderLastSaved();
|
|
578
|
+
if (msg.engine) statusEngine.textContent = msg.engine;
|
|
106
579
|
|
|
107
580
|
// Clear previous project's chat and module list
|
|
108
581
|
messagesEl.innerHTML = "";
|
|
109
|
-
document.getElementById("module-items").innerHTML = "";
|
|
110
|
-
document.getElementById("module-count").textContent = "0";
|
|
582
|
+
const _mi = document.getElementById("module-items"); if (_mi) _mi.innerHTML = "";
|
|
583
|
+
const _mc = document.getElementById("module-count") || document.getElementById("slideout-module-count"); if (_mc) _mc.textContent = "0";
|
|
111
584
|
hideChatSuggestions();
|
|
112
585
|
// Reset pipeline state — DOM nodes were detached by innerHTML clear
|
|
113
586
|
resetPipelineState();
|
|
@@ -118,8 +591,13 @@ function handleWsMessage(msg) {
|
|
|
118
591
|
updateModuleList(msg.modules);
|
|
119
592
|
refreshPreview();
|
|
120
593
|
}
|
|
594
|
+
|
|
595
|
+
// Render page tabs for multi-page projects
|
|
596
|
+
renderPageTabs(msg.templates, currentTemplateId);
|
|
597
|
+
|
|
121
598
|
statusEngine.textContent = msg.engine || "";
|
|
122
599
|
fetchHsAccountStatus();
|
|
600
|
+
if (typeof refreshPortalIndicator === "function") refreshPortalIndicator();
|
|
123
601
|
|
|
124
602
|
// Populate chat header
|
|
125
603
|
const chatHeaderTitle = document.getElementById("chat-header-title");
|
|
@@ -172,6 +650,9 @@ function handleWsMessage(msg) {
|
|
|
172
650
|
if (typeof window.setSelectModeDisabled === "function") {
|
|
173
651
|
window.setSelectModeDisabled(true);
|
|
174
652
|
}
|
|
653
|
+
if (typeof window.setEditModeDisabled === "function") {
|
|
654
|
+
window.setEditModeDisabled(true);
|
|
655
|
+
}
|
|
175
656
|
}
|
|
176
657
|
|
|
177
658
|
// If setup handed us an initial prompt (describe-it path), send it now
|
|
@@ -201,18 +682,25 @@ function handleWsMessage(msg) {
|
|
|
201
682
|
// The next `modules_updated` is the terminal one for this run — let it
|
|
202
683
|
// fire the change-highlight pass on the preview iframe.
|
|
203
684
|
highlightOnNextModulesUpdated = true;
|
|
685
|
+
// Refresh the Library tab's module list so it stays in sync
|
|
686
|
+
if (typeof refreshDashboard === "function") refreshDashboard();
|
|
204
687
|
break;
|
|
205
688
|
|
|
206
689
|
case "modules_updated":
|
|
207
690
|
if (msg.modules) {
|
|
208
691
|
updateModuleList(msg.modules);
|
|
209
692
|
}
|
|
693
|
+
if (msg.templates) {
|
|
694
|
+
if (msg.templateId) currentTemplateId = msg.templateId;
|
|
695
|
+
renderPageTabs(msg.templates, currentTemplateId);
|
|
696
|
+
}
|
|
210
697
|
if (highlightOnNextModulesUpdated) {
|
|
211
698
|
highlightOnNextModulesUpdated = false;
|
|
212
699
|
flushChangeHighlights(msg.modules || []);
|
|
213
700
|
} else {
|
|
214
701
|
refreshPreview();
|
|
215
702
|
}
|
|
703
|
+
markSaved(msg.updatedAt);
|
|
216
704
|
break;
|
|
217
705
|
|
|
218
706
|
case "version_created":
|
|
@@ -220,10 +708,11 @@ function handleWsMessage(msg) {
|
|
|
220
708
|
// New generation: reset timeline cursor to head and refresh.
|
|
221
709
|
historyTimelineCursor = 0;
|
|
222
710
|
refreshHistoryTimeline();
|
|
711
|
+
markSaved(msg.updatedAt);
|
|
223
712
|
break;
|
|
224
713
|
|
|
225
714
|
case "parse_warning":
|
|
226
|
-
appendSystemMessage(msg.message || "
|
|
715
|
+
appendSystemMessage(msg.message || "Module changes could not be applied.");
|
|
227
716
|
break;
|
|
228
717
|
|
|
229
718
|
case "error":
|
|
@@ -279,8 +768,8 @@ function handleWsMessage(msg) {
|
|
|
279
768
|
case "needs_setup":
|
|
280
769
|
// Clear stale UI if shown
|
|
281
770
|
messagesEl.innerHTML = "";
|
|
282
|
-
document.getElementById("module-items").innerHTML = "";
|
|
283
|
-
document.getElementById("module-count").textContent = "0";
|
|
771
|
+
{ const _mi = document.getElementById("module-items"); if (_mi) _mi.innerHTML = ""; }
|
|
772
|
+
{ const _mc = document.getElementById("module-count") || document.getElementById("slideout-module-count"); if (_mc) _mc.textContent = "0"; }
|
|
284
773
|
break;
|
|
285
774
|
|
|
286
775
|
case "plan_updated":
|
|
@@ -556,9 +1045,136 @@ function handleModuleProgress(msg) {
|
|
|
556
1045
|
changedModulesInRun.add(msg.module);
|
|
557
1046
|
}
|
|
558
1047
|
|
|
1048
|
+
// Render code snippet preview when a module finishes generating, and update
|
|
1049
|
+
// the global "Valid HubL" indicator based on a quick syntax check.
|
|
1050
|
+
if (msg.status === "complete" && msg.moduleFiles) {
|
|
1051
|
+
appendModuleCardSnippet(card, msg.module, msg.moduleFiles);
|
|
1052
|
+
if (typeof window.reportHublCheck === "function") {
|
|
1053
|
+
const issues = quickCheckHubl(msg.moduleFiles);
|
|
1054
|
+
window.reportHublCheck(msg.module, issues);
|
|
1055
|
+
}
|
|
1056
|
+
} else if (msg.status === "failed" && typeof window.reportHublCheck === "function") {
|
|
1057
|
+
window.reportHublCheck(msg.module, [{ kind: "generation_failed", message: "Module generation failed" }]);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
559
1060
|
scrollToBottom();
|
|
560
1061
|
}
|
|
561
1062
|
|
|
1063
|
+
// ---------------------------------------------------------------------------
|
|
1064
|
+
// Code snippet preview inside a pipeline module card
|
|
1065
|
+
// ---------------------------------------------------------------------------
|
|
1066
|
+
|
|
1067
|
+
const HUBL_PREVIEW_MAX_LINES = 12;
|
|
1068
|
+
const HUBL_PREVIEW_MAX_CHARS = 800;
|
|
1069
|
+
|
|
1070
|
+
function appendModuleCardSnippet(card, moduleName, files) {
|
|
1071
|
+
if (card.querySelector(".pipeline-module-card__snippet")) return;
|
|
1072
|
+
const snippet = buildHublSnippet(files.moduleHtml || "");
|
|
1073
|
+
if (!snippet) return;
|
|
1074
|
+
|
|
1075
|
+
const wrap = document.createElement("div");
|
|
1076
|
+
wrap.className = "pipeline-module-card__snippet";
|
|
1077
|
+
|
|
1078
|
+
const header = document.createElement("div");
|
|
1079
|
+
header.className = "pipeline-module-card__snippet-head";
|
|
1080
|
+
header.innerHTML = `
|
|
1081
|
+
<span class="pipeline-module-card__snippet-label">module.html</span>
|
|
1082
|
+
<button class="pipeline-module-card__snippet-toggle" type="button" aria-expanded="false">
|
|
1083
|
+
<svg class="icon icon--sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
|
|
1084
|
+
<span class="pipeline-module-card__snippet-toggle-label">Show code</span>
|
|
1085
|
+
</button>`;
|
|
1086
|
+
|
|
1087
|
+
const codeWrap = document.createElement("div");
|
|
1088
|
+
codeWrap.className = "pipeline-module-card__snippet-code hidden";
|
|
1089
|
+
const pre = document.createElement("pre");
|
|
1090
|
+
const codeEl = document.createElement("code");
|
|
1091
|
+
codeEl.className = "language-hubl";
|
|
1092
|
+
codeEl.textContent = snippet.text;
|
|
1093
|
+
pre.appendChild(codeEl);
|
|
1094
|
+
codeWrap.appendChild(pre);
|
|
1095
|
+
|
|
1096
|
+
if (snippet.truncated) {
|
|
1097
|
+
const more = document.createElement("button");
|
|
1098
|
+
more.className = "pipeline-module-card__snippet-open";
|
|
1099
|
+
more.type = "button";
|
|
1100
|
+
more.textContent = "Open in code view →";
|
|
1101
|
+
more.addEventListener("click", () => openCodeViewForModule(moduleName));
|
|
1102
|
+
codeWrap.appendChild(more);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
header.querySelector("button").addEventListener("click", () => {
|
|
1106
|
+
const expanded = !codeWrap.classList.contains("hidden");
|
|
1107
|
+
codeWrap.classList.toggle("hidden", expanded);
|
|
1108
|
+
const toggle = header.querySelector(".pipeline-module-card__snippet-toggle");
|
|
1109
|
+
toggle.setAttribute("aria-expanded", expanded ? "false" : "true");
|
|
1110
|
+
const lbl = toggle.querySelector(".pipeline-module-card__snippet-toggle-label");
|
|
1111
|
+
if (lbl) lbl.textContent = expanded ? "Show code" : "Hide code";
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
wrap.appendChild(header);
|
|
1115
|
+
wrap.appendChild(codeWrap);
|
|
1116
|
+
card.appendChild(wrap);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function buildHublSnippet(html) {
|
|
1120
|
+
if (!html) return null;
|
|
1121
|
+
const trimmed = html.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
1122
|
+
const lines = trimmed.split("\n");
|
|
1123
|
+
let out = lines.slice(0, HUBL_PREVIEW_MAX_LINES).join("\n");
|
|
1124
|
+
let truncated = lines.length > HUBL_PREVIEW_MAX_LINES;
|
|
1125
|
+
if (out.length > HUBL_PREVIEW_MAX_CHARS) {
|
|
1126
|
+
out = out.slice(0, HUBL_PREVIEW_MAX_CHARS);
|
|
1127
|
+
truncated = true;
|
|
1128
|
+
}
|
|
1129
|
+
return { text: out, truncated };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function openCodeViewForModule(moduleName) {
|
|
1133
|
+
const codeBtn = document.querySelector('.view-toggle__btn[data-view="code"]');
|
|
1134
|
+
if (codeBtn) codeBtn.click();
|
|
1135
|
+
// Best-effort selection of the module's html file if the file list is ready.
|
|
1136
|
+
setTimeout(() => {
|
|
1137
|
+
const list = document.getElementById("code-file-list");
|
|
1138
|
+
if (!list) return;
|
|
1139
|
+
const target = Array.from(list.querySelectorAll("[data-file-path]")).find((el) => {
|
|
1140
|
+
const p = el.getAttribute("data-file-path") || "";
|
|
1141
|
+
return p.includes(`/${moduleName}.module/module.html`) || p.endsWith(`${moduleName}.module/module.html`);
|
|
1142
|
+
});
|
|
1143
|
+
if (target) target.click();
|
|
1144
|
+
}, 80);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Lightweight HubL validity check — flags the same balance issues the
|
|
1148
|
+
// server-side validator would auto-fix, plus reserved field/ deprecated type
|
|
1149
|
+
// markers that the server already strips. The badge is a hint, not a gate.
|
|
1150
|
+
function quickCheckHubl(files) {
|
|
1151
|
+
const issues = [];
|
|
1152
|
+
const html = files && typeof files.moduleHtml === "string" ? files.moduleHtml : "";
|
|
1153
|
+
if (!html) return issues;
|
|
1154
|
+
|
|
1155
|
+
const blockOpeners = ["if", "for", "block", "with", "macro", "filter", "raw", "scope_css"];
|
|
1156
|
+
const stack = [];
|
|
1157
|
+
const tagRe = /\{%-?\s*(end)?(\w+)/g;
|
|
1158
|
+
let match;
|
|
1159
|
+
while ((match = tagRe.exec(html))) {
|
|
1160
|
+
const isEnd = !!match[1];
|
|
1161
|
+
const name = match[2];
|
|
1162
|
+
if (isEnd) {
|
|
1163
|
+
if (!stack.length || stack[stack.length - 1] !== name) {
|
|
1164
|
+
issues.push({ kind: "unbalanced_close", tag: name, message: `Stray {% end${name} %}` });
|
|
1165
|
+
} else {
|
|
1166
|
+
stack.pop();
|
|
1167
|
+
}
|
|
1168
|
+
} else if (blockOpeners.includes(name)) {
|
|
1169
|
+
stack.push(name);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
for (const open of stack) {
|
|
1173
|
+
issues.push({ kind: "unbalanced_open", tag: open, message: `Missing {% end${open} %}` });
|
|
1174
|
+
}
|
|
1175
|
+
return issues;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
562
1178
|
function startPipelineEstimate() {
|
|
563
1179
|
stopPipelineEstimate();
|
|
564
1180
|
if (!pipelineEstimateEl) return;
|
|
@@ -634,24 +1250,26 @@ function handlePipelineComplete(msg) {
|
|
|
634
1250
|
answerEl.className = "pipeline-answer";
|
|
635
1251
|
answerEl.textContent = msg.answer;
|
|
636
1252
|
bubble.appendChild(answerEl);
|
|
1253
|
+
} else if (msg.assistantMessage) {
|
|
1254
|
+
streamBuffer = msg.assistantMessage;
|
|
637
1255
|
}
|
|
638
1256
|
|
|
1257
|
+
clearStreamStatus();
|
|
1258
|
+
finishStreaming();
|
|
1259
|
+
|
|
639
1260
|
const stats = document.createElement("div");
|
|
640
1261
|
stats.className = "pipeline-stats";
|
|
641
1262
|
const duration = formatDuration(msg.durationMs);
|
|
642
1263
|
if (msg.answer) {
|
|
643
1264
|
stats.textContent = `Answered in ${duration}`;
|
|
644
1265
|
} else {
|
|
645
|
-
stats.textContent = `Generated ${msg.modulesGenerated}
|
|
1266
|
+
stats.textContent = `Generated ${msg.modulesGenerated} module${msg.modulesGenerated === 1 ? "" : "s"} in ${duration}`;
|
|
646
1267
|
if (msg.modulesUnchanged > 0) {
|
|
647
1268
|
stats.textContent += ` (${msg.modulesUnchanged} unchanged)`;
|
|
648
1269
|
}
|
|
649
1270
|
}
|
|
650
1271
|
bubble.appendChild(stats);
|
|
651
1272
|
|
|
652
|
-
clearStreamStatus();
|
|
653
|
-
finishStreaming();
|
|
654
|
-
|
|
655
1273
|
resetPipelineState();
|
|
656
1274
|
}
|
|
657
1275
|
|
|
@@ -671,15 +1289,15 @@ function handlePipelinePartial(msg) {
|
|
|
671
1289
|
|
|
672
1290
|
const bubble = streamingMsgEl || pipelineBubbleEl;
|
|
673
1291
|
|
|
1292
|
+
clearStreamStatus();
|
|
1293
|
+
finishStreaming();
|
|
1294
|
+
|
|
674
1295
|
const stats = document.createElement("div");
|
|
675
1296
|
stats.className = "pipeline-stats pipeline-stats--partial";
|
|
676
1297
|
const duration = formatDuration(msg.durationMs);
|
|
677
|
-
stats.textContent = `${msg.succeeded.length}
|
|
1298
|
+
stats.textContent = `${msg.succeeded.length} modules succeeded, ${msg.failed.length} failed in ${duration}`;
|
|
678
1299
|
bubble.appendChild(stats);
|
|
679
1300
|
|
|
680
|
-
clearStreamStatus();
|
|
681
|
-
finishStreaming();
|
|
682
|
-
|
|
683
1301
|
resetPipelineState();
|
|
684
1302
|
}
|
|
685
1303
|
|
|
@@ -710,19 +1328,19 @@ function pickContextualSuggestions(moduleNames) {
|
|
|
710
1328
|
const additive = [];
|
|
711
1329
|
|
|
712
1330
|
if (!has("testimonial") && !has("review") && !has("quote")) {
|
|
713
|
-
additive.push("Add a testimonials
|
|
1331
|
+
additive.push("Add a testimonials module");
|
|
714
1332
|
}
|
|
715
1333
|
if (!has("pricing") && !has("plan") && !has("tier")) {
|
|
716
1334
|
additive.push("Add a pricing table");
|
|
717
1335
|
}
|
|
718
1336
|
if (!has("faq") && !has("question")) {
|
|
719
|
-
additive.push("Add an FAQ
|
|
1337
|
+
additive.push("Add an FAQ module");
|
|
720
1338
|
}
|
|
721
1339
|
if (!has("contact") && !has("form")) {
|
|
722
1340
|
additive.push("Add a contact form");
|
|
723
1341
|
}
|
|
724
1342
|
if (!has("hero") && !has("banner")) {
|
|
725
|
-
additive.push("Add a hero
|
|
1343
|
+
additive.push("Add a hero module");
|
|
726
1344
|
}
|
|
727
1345
|
if (!has("feature") && !has("benefit")) {
|
|
728
1346
|
additive.push("Add a features grid with icons");
|
|
@@ -731,10 +1349,10 @@ function pickContextualSuggestions(moduleNames) {
|
|
|
731
1349
|
additive.push("Add a footer with social links");
|
|
732
1350
|
}
|
|
733
1351
|
if (!has("stat") && !has("metric") && !has("number")) {
|
|
734
|
-
additive.push("Add a stats
|
|
1352
|
+
additive.push("Add a stats module with key numbers");
|
|
735
1353
|
}
|
|
736
1354
|
if (!has("cta") && !has("call")) {
|
|
737
|
-
additive.push("Add a call-to-action
|
|
1355
|
+
additive.push("Add a call-to-action module");
|
|
738
1356
|
}
|
|
739
1357
|
|
|
740
1358
|
const refinements = [
|
|
@@ -803,12 +1421,12 @@ async function handleAgenticPrompt() {
|
|
|
803
1421
|
"vibeSpot can decompose AI generation into specialized agents:\n\n" +
|
|
804
1422
|
"• Intent Analyzer — classifies your request\n" +
|
|
805
1423
|
"• Page Architect — designs the page structure\n" +
|
|
806
|
-
"•
|
|
1424
|
+
"• Module Developer — generates each module in parallel\n" +
|
|
807
1425
|
"• Validator — checks and auto-fixes errors\n\n" +
|
|
808
1426
|
"Tradeoffs:\n" +
|
|
809
1427
|
"✓ Better quality — each agent is focused on one task\n" +
|
|
810
1428
|
"✓ Structured output — eliminates JSON parsing failures\n" +
|
|
811
|
-
"✓ Only changed
|
|
1429
|
+
"✓ Only changed modules regenerated on edits\n" +
|
|
812
1430
|
"✗ Uses more calls per request (API calls or CLI subprocess calls)\n\n" +
|
|
813
1431
|
"You can change this anytime in Settings.",
|
|
814
1432
|
"Use Agentic Pipeline",
|
|
@@ -848,8 +1466,11 @@ function handleSuggestBrandExtraction() {
|
|
|
848
1466
|
scrollToBottom();
|
|
849
1467
|
|
|
850
1468
|
el.querySelector("#btn-accept-extraction").addEventListener("click", () => {
|
|
851
|
-
el.querySelector(".
|
|
852
|
-
|
|
1469
|
+
const container = el.querySelector(".chat-msg__system");
|
|
1470
|
+
if (container) {
|
|
1471
|
+
container.innerHTML =
|
|
1472
|
+
'<span class="brand-extraction-prompt__status">Extracting…</span>';
|
|
1473
|
+
}
|
|
853
1474
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
854
1475
|
ws.send(JSON.stringify({ type: "extract_brand_assets" }));
|
|
855
1476
|
}
|
|
@@ -1064,6 +1685,7 @@ async function sendMessage(text) {
|
|
|
1064
1685
|
);
|
|
1065
1686
|
changedModulesInRun = new Set();
|
|
1066
1687
|
clearModuleListChanged();
|
|
1688
|
+
if (typeof window.resetHublCheck === "function") window.resetHublCheck();
|
|
1067
1689
|
|
|
1068
1690
|
// Start streaming indicator
|
|
1069
1691
|
startStreaming();
|
|
@@ -1119,6 +1741,9 @@ function startStreaming() {
|
|
|
1119
1741
|
if (typeof window.setSelectModeDisabled === "function") {
|
|
1120
1742
|
window.setSelectModeDisabled(true);
|
|
1121
1743
|
}
|
|
1744
|
+
if (typeof window.setEditModeDisabled === "function") {
|
|
1745
|
+
window.setEditModeDisabled(true);
|
|
1746
|
+
}
|
|
1122
1747
|
|
|
1123
1748
|
hideChatSuggestions();
|
|
1124
1749
|
if (typeof updateHistoryTimelineNavState === "function") updateHistoryTimelineNavState();
|
|
@@ -1267,6 +1892,9 @@ function finishStreaming() {
|
|
|
1267
1892
|
if (typeof window.setSelectModeDisabled === "function") {
|
|
1268
1893
|
window.setSelectModeDisabled(false);
|
|
1269
1894
|
}
|
|
1895
|
+
if (typeof window.setEditModeDisabled === "function") {
|
|
1896
|
+
window.setEditModeDisabled(false);
|
|
1897
|
+
}
|
|
1270
1898
|
if (typeof updateHistoryTimelineNavState === "function") updateHistoryTimelineNavState();
|
|
1271
1899
|
|
|
1272
1900
|
// Stop the timer and capture duration
|
|
@@ -1293,7 +1921,7 @@ function finishStreaming() {
|
|
|
1293
1921
|
if (streamingMsgEl && streamBuffer) {
|
|
1294
1922
|
const rendered = renderMarkdown(streamBuffer);
|
|
1295
1923
|
const visibleText = rendered.replace(/<[^>]*>/g, "").trim();
|
|
1296
|
-
streamingMsgEl.innerHTML = visibleText ? rendered : "<em>
|
|
1924
|
+
streamingMsgEl.innerHTML = visibleText ? rendered : "<em>Modules applied.</em>";
|
|
1297
1925
|
}
|
|
1298
1926
|
|
|
1299
1927
|
streamingMsgEl = null;
|
|
@@ -1389,6 +2017,46 @@ function setStatus(text) {
|
|
|
1389
2017
|
statusText.textContent = text;
|
|
1390
2018
|
}
|
|
1391
2019
|
|
|
2020
|
+
function setStatusTheme(name) {
|
|
2021
|
+
if (!statusTheme) return;
|
|
2022
|
+
const value = (name || "").trim();
|
|
2023
|
+
statusTheme.textContent = value && value !== "—" ? value : "";
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function formatRelativeSaved(ts) {
|
|
2027
|
+
if (!ts) return "";
|
|
2028
|
+
const diff = Math.max(0, Date.now() - ts);
|
|
2029
|
+
const sec = Math.round(diff / 1000);
|
|
2030
|
+
if (sec < 10) return "just now";
|
|
2031
|
+
if (sec < 60) return `${sec} sec ago`;
|
|
2032
|
+
const min = Math.round(sec / 60);
|
|
2033
|
+
if (min < 60) return `${min} min ago`;
|
|
2034
|
+
const hr = Math.round(min / 60);
|
|
2035
|
+
if (hr < 24) return `${hr} hr ago`;
|
|
2036
|
+
const day = Math.round(hr / 24);
|
|
2037
|
+
return `${day} day${day === 1 ? "" : "s"} ago`;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function renderLastSaved() {
|
|
2041
|
+
if (!statusLastSaved) return;
|
|
2042
|
+
if (!lastSavedAt) {
|
|
2043
|
+
statusLastSaved.textContent = "";
|
|
2044
|
+
statusLastSaved.title = "";
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
statusLastSaved.textContent = formatRelativeSaved(lastSavedAt);
|
|
2048
|
+
try {
|
|
2049
|
+
statusLastSaved.title = new Date(lastSavedAt).toLocaleString();
|
|
2050
|
+
} catch { /* ignore */ }
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
function markSaved(ts) {
|
|
2054
|
+
lastSavedAt = typeof ts === "number" && ts > 0 ? ts : Date.now();
|
|
2055
|
+
renderLastSaved();
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
setInterval(renderLastSaved, 30000);
|
|
2059
|
+
|
|
1392
2060
|
// ---------------------------------------------------------------------------
|
|
1393
2061
|
// Restored / system messages
|
|
1394
2062
|
// ---------------------------------------------------------------------------
|
|
@@ -1436,7 +2104,7 @@ function appendRestoredAssistantMessage(text, timestamp, pipeline) {
|
|
|
1436
2104
|
: "";
|
|
1437
2105
|
|
|
1438
2106
|
const duration = formatDuration(pipeline.stats.durationMs);
|
|
1439
|
-
let statsText = `Generated ${pipeline.stats.modulesGenerated}
|
|
2107
|
+
let statsText = `Generated ${pipeline.stats.modulesGenerated} module${pipeline.stats.modulesGenerated === 1 ? "" : "s"} in ${duration}`;
|
|
1440
2108
|
if (pipeline.stats.modulesUnchanged > 0) {
|
|
1441
2109
|
statsText += ` (${pipeline.stats.modulesUnchanged} unchanged)`;
|
|
1442
2110
|
}
|
|
@@ -1574,7 +2242,7 @@ function attachHistoryToggle() {
|
|
|
1574
2242
|
async function doRollback(hash) {
|
|
1575
2243
|
const scoped = currentTemplateId && !historyShowAll;
|
|
1576
2244
|
const msg = scoped
|
|
1577
|
-
? "This template's
|
|
2245
|
+
? "This template's modules will be restored to the selected version. Other templates are not affected."
|
|
1578
2246
|
: "All theme files will be replaced, but chat history is preserved.";
|
|
1579
2247
|
const ok = await vibeConfirm("Restore this version?", msg, { confirmLabel: "Restore", confirmClass: "btn--primary" });
|
|
1580
2248
|
if (!ok) return;
|
|
@@ -1740,10 +2408,16 @@ async function refreshHistoryTimeline() {
|
|
|
1740
2408
|
function updateHistoryTimelineNavState() {
|
|
1741
2409
|
const undoBtn = document.getElementById("history-timeline-undo");
|
|
1742
2410
|
const redoBtn = document.getElementById("history-timeline-redo");
|
|
2411
|
+
const topbarUndo = document.getElementById("btn-undo");
|
|
2412
|
+
const topbarRedo = document.getElementById("btn-redo");
|
|
1743
2413
|
const max = historyTimelineEntries.length - 1;
|
|
1744
2414
|
const busy = historyTimelineRestoring || (typeof isStreaming !== "undefined" && isStreaming);
|
|
1745
|
-
|
|
1746
|
-
|
|
2415
|
+
const undoDisabled = busy || !historyGitAvailable || historyTimelineCursor >= max;
|
|
2416
|
+
const redoDisabled = busy || !historyGitAvailable || historyTimelineCursor <= 0;
|
|
2417
|
+
if (undoBtn) undoBtn.disabled = undoDisabled;
|
|
2418
|
+
if (redoBtn) redoBtn.disabled = redoDisabled;
|
|
2419
|
+
if (topbarUndo) topbarUndo.disabled = undoDisabled;
|
|
2420
|
+
if (topbarRedo) topbarRedo.disabled = redoDisabled;
|
|
1747
2421
|
}
|
|
1748
2422
|
|
|
1749
2423
|
async function restoreToTimelineIndex(idx) {
|
|
@@ -1809,7 +2483,7 @@ function showTimelineTooltip(entryEl) {
|
|
|
1809
2483
|
const more = (commit.changedModules || []).length - modules.length;
|
|
1810
2484
|
const modulesLine = modules.length
|
|
1811
2485
|
? modules.join(", ") + (more > 0 ? ` +${more} more` : "")
|
|
1812
|
-
: "No
|
|
2486
|
+
: "No module changes";
|
|
1813
2487
|
|
|
1814
2488
|
// Strip [templateId] prefix for display; keep "Rollback to:" prefix because
|
|
1815
2489
|
// that case is filtered out earlier and won't reach here.
|
|
@@ -1875,6 +2549,13 @@ function hideTimelineTooltip() {
|
|
|
1875
2549
|
if (undoBtn) undoBtn.addEventListener("click", timelineUndo);
|
|
1876
2550
|
if (redoBtn) redoBtn.addEventListener("click", timelineRedo);
|
|
1877
2551
|
|
|
2552
|
+
// Topbar undo/redo buttons mirror the timeline strip controls so the action
|
|
2553
|
+
// is discoverable without opening the history panel.
|
|
2554
|
+
const topbarUndo = document.getElementById("btn-undo");
|
|
2555
|
+
const topbarRedo = document.getElementById("btn-redo");
|
|
2556
|
+
if (topbarUndo) topbarUndo.addEventListener("click", timelineUndo);
|
|
2557
|
+
if (topbarRedo) topbarRedo.addEventListener("click", timelineRedo);
|
|
2558
|
+
|
|
1878
2559
|
// Global Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z keyboard shortcuts. Skip when the
|
|
1879
2560
|
// user is editing text so we never hijack native undo in inputs/editors.
|
|
1880
2561
|
document.addEventListener("keydown", (e) => {
|
|
@@ -1913,6 +2594,8 @@ function updateModuleList(moduleNames) {
|
|
|
1913
2594
|
|
|
1914
2595
|
if (barCountEl) barCountEl.textContent = moduleNames.length;
|
|
1915
2596
|
if (slideoutCountEl) slideoutCountEl.textContent = moduleNames.length;
|
|
2597
|
+
const pageTreeCountEl = document.getElementById("page-tree-module-count");
|
|
2598
|
+
if (pageTreeCountEl) pageTreeCountEl.textContent = moduleNames.length;
|
|
1916
2599
|
|
|
1917
2600
|
// Preserve which items were marked as recently changed across re-renders so
|
|
1918
2601
|
// the dots survive the per-module `modules_updated` events that happen
|
|
@@ -1931,7 +2614,7 @@ function updateModuleList(moduleNames) {
|
|
|
1931
2614
|
<span class="module-item__drag">⠿</span>
|
|
1932
2615
|
<span class="module-item__changed-dot" aria-hidden="true"></span>
|
|
1933
2616
|
<span class="module-item__name">${escapeHtml(name)}</span>
|
|
1934
|
-
<span class="module-item__edit" title="Edit fields"
|
|
2617
|
+
<span class="module-item__edit" title="Edit fields">${vsIcon("settings", {size: "sm"})}</span>
|
|
1935
2618
|
<span class="module-item__delete" title="Delete section">×</span>
|
|
1936
2619
|
`;
|
|
1937
2620
|
|
|
@@ -2030,6 +2713,7 @@ function clearModuleListChanged() {
|
|
|
2030
2713
|
|
|
2031
2714
|
function openModuleSlideout() {
|
|
2032
2715
|
const slideout = document.getElementById("module-slideout");
|
|
2716
|
+
slideout.classList.remove("hidden");
|
|
2033
2717
|
document.getElementById("module-list-view").classList.remove("hidden");
|
|
2034
2718
|
document.getElementById("module-editor-view").classList.add("hidden");
|
|
2035
2719
|
slideout.classList.add("open");
|
|
@@ -2040,9 +2724,11 @@ function closeModuleSlideout() {
|
|
|
2040
2724
|
}
|
|
2041
2725
|
|
|
2042
2726
|
function showEditorView(moduleName) {
|
|
2727
|
+
const slideout = document.getElementById("module-slideout");
|
|
2728
|
+
slideout.classList.remove("hidden");
|
|
2043
2729
|
document.getElementById("module-list-view").classList.add("hidden");
|
|
2044
2730
|
document.getElementById("module-editor-view").classList.remove("hidden");
|
|
2045
|
-
|
|
2731
|
+
slideout.classList.add("open");
|
|
2046
2732
|
}
|
|
2047
2733
|
|
|
2048
2734
|
function showModuleListView() {
|
|
@@ -2123,8 +2809,8 @@ async function addModuleFromLibrary(moduleName) {
|
|
|
2123
2809
|
}
|
|
2124
2810
|
}
|
|
2125
2811
|
|
|
2126
|
-
//
|
|
2127
|
-
document.getElementById("btn-modules")?.addEventListener("click", () => {
|
|
2812
|
+
// Toggle module slideout from page-tree header
|
|
2813
|
+
document.getElementById("btn-toggle-modules")?.addEventListener("click", () => {
|
|
2128
2814
|
const slideout = document.getElementById("module-slideout");
|
|
2129
2815
|
if (slideout.classList.contains("open")) {
|
|
2130
2816
|
closeModuleSlideout();
|
|
@@ -2349,10 +3035,15 @@ inputEl.addEventListener("keydown", (e) => {
|
|
|
2349
3035
|
}
|
|
2350
3036
|
});
|
|
2351
3037
|
|
|
2352
|
-
// Auto-grow textarea
|
|
2353
|
-
|
|
3038
|
+
// Auto-grow textarea — 3 rows default, expand up to 6 rows.
|
|
3039
|
+
// Line-height (1.5) × font-size (14px) × 6 rows + ~8px padding = ~134px.
|
|
3040
|
+
const CHAT_INPUT_MAX_HEIGHT = 134;
|
|
3041
|
+
function autoGrowChatInput() {
|
|
2354
3042
|
inputEl.style.height = "auto";
|
|
2355
|
-
inputEl.style.height = Math.min(inputEl.scrollHeight,
|
|
3043
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, CHAT_INPUT_MAX_HEIGHT) + "px";
|
|
3044
|
+
}
|
|
3045
|
+
inputEl.addEventListener("input", () => {
|
|
3046
|
+
autoGrowChatInput();
|
|
2356
3047
|
|
|
2357
3048
|
// Hide suggestion chips as soon as the user starts typing
|
|
2358
3049
|
if (suggestionsVisible && inputEl.value.length > 0) {
|
|
@@ -2360,6 +3051,36 @@ inputEl.addEventListener("input", () => {
|
|
|
2360
3051
|
}
|
|
2361
3052
|
});
|
|
2362
3053
|
|
|
3054
|
+
// Contextual placeholder — adapts to plan mode and whether a page already exists.
|
|
3055
|
+
function updateChatPlaceholder() {
|
|
3056
|
+
if (!inputEl) return;
|
|
3057
|
+
const planActive = !!window.planModeActive;
|
|
3058
|
+
const moduleCountEl = document.getElementById("module-count");
|
|
3059
|
+
const moduleCount = moduleCountEl ? parseInt(moduleCountEl.textContent || "0", 10) || 0 : 0;
|
|
3060
|
+
let placeholder;
|
|
3061
|
+
if (planActive) {
|
|
3062
|
+
placeholder = "Describe what you want to plan...";
|
|
3063
|
+
} else if (moduleCount > 0) {
|
|
3064
|
+
placeholder = "Describe what you want to change...";
|
|
3065
|
+
} else {
|
|
3066
|
+
placeholder = "Describe your landing page...";
|
|
3067
|
+
}
|
|
3068
|
+
inputEl.placeholder = placeholder;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// Re-evaluate placeholder when plan mode changes (plan.js dispatches this) or
|
|
3072
|
+
// when the module list updates.
|
|
3073
|
+
window.addEventListener("plan-mode-changed", updateChatPlaceholder);
|
|
3074
|
+
const _moduleCountEl = document.getElementById("module-count");
|
|
3075
|
+
if (_moduleCountEl && typeof MutationObserver !== "undefined") {
|
|
3076
|
+
new MutationObserver(updateChatPlaceholder).observe(_moduleCountEl, {
|
|
3077
|
+
childList: true,
|
|
3078
|
+
characterData: true,
|
|
3079
|
+
subtree: true,
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
updateChatPlaceholder();
|
|
3083
|
+
|
|
2363
3084
|
// Suggestion chip click — pre-fill input and focus, do not auto-send so the
|
|
2364
3085
|
// user can edit before pressing Enter.
|
|
2365
3086
|
document.getElementById("chat-suggestions")?.addEventListener("click", (e) => {
|
|
@@ -2380,35 +3101,33 @@ window.prefillChatInput = function (text) {
|
|
|
2380
3101
|
const existing = inputEl.value;
|
|
2381
3102
|
const prefix = existing.trim() ? existing.replace(/\s+$/, "") + "\n\n" : "";
|
|
2382
3103
|
inputEl.value = prefix + text;
|
|
2383
|
-
|
|
2384
|
-
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + "px";
|
|
3104
|
+
autoGrowChatInput();
|
|
2385
3105
|
inputEl.focus();
|
|
2386
3106
|
const end = inputEl.value.length;
|
|
2387
3107
|
inputEl.setSelectionRange(end, end);
|
|
2388
3108
|
};
|
|
2389
3109
|
|
|
2390
|
-
//
|
|
2391
|
-
document.getElementById("
|
|
2392
|
-
const btn = e.target.closest(".starter-btn");
|
|
2393
|
-
if (btn) sendMessage(btn.dataset.prompt);
|
|
2394
|
-
});
|
|
3110
|
+
// Conversation starter buttons in chat welcome
|
|
3111
|
+
document.getElementById("conversation-starters")?.addEventListener("click", handleConversationStarterClick);
|
|
2395
3112
|
|
|
2396
|
-
// Templates icon in input area —
|
|
3113
|
+
// Templates icon in input area — switch to Library tab where project assets live
|
|
2397
3114
|
document.getElementById("btn-starter-templates")?.addEventListener("click", () => {
|
|
2398
|
-
|
|
2399
|
-
if (
|
|
2400
|
-
|
|
2401
|
-
|
|
3115
|
+
const libraryTab = document.getElementById("ws-tab-library");
|
|
3116
|
+
if (libraryTab) {
|
|
3117
|
+
libraryTab.click();
|
|
3118
|
+
document.getElementById("library-project-assets")?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
2402
3119
|
}
|
|
2403
|
-
if (welcome) welcome.classList.toggle("hidden");
|
|
2404
3120
|
});
|
|
2405
3121
|
|
|
2406
|
-
// Version history
|
|
3122
|
+
// Version history (bottom panel)
|
|
2407
3123
|
document.getElementById("btn-history")?.addEventListener("click", toggleHistoryPanel);
|
|
2408
3124
|
document.getElementById("history-panel-close")?.addEventListener("click", () => {
|
|
2409
3125
|
historyPanelOpen = false;
|
|
2410
3126
|
document.getElementById("history-panel")?.classList.add("hidden");
|
|
2411
3127
|
});
|
|
3128
|
+
document.getElementById("history-panel-collapse")?.addEventListener("click", () => {
|
|
3129
|
+
document.getElementById("history-panel")?.classList.toggle("collapsed");
|
|
3130
|
+
});
|
|
2412
3131
|
|
|
2413
3132
|
// Import from GitHub is now on the setup screen (setup.js)
|
|
2414
3133
|
|
|
@@ -2456,11 +3175,6 @@ document.getElementById("responsive-toggle").addEventListener("click", (e) => {
|
|
|
2456
3175
|
chrome.style.maxWidth = isFullWidth ? "none" : width;
|
|
2457
3176
|
chrome.style.flex = isFullWidth ? "1" : "none";
|
|
2458
3177
|
chrome.style.width = isFullWidth ? "" : width;
|
|
2459
|
-
|
|
2460
|
-
// Update browser URL bar with theme name
|
|
2461
|
-
const urlEl = document.getElementById("browser-url");
|
|
2462
|
-
const themeName = document.getElementById("theme-name")?.textContent || "vibespot.app";
|
|
2463
|
-
if (urlEl) urlEl.textContent = themeName + ".vibespot.app";
|
|
2464
3178
|
});
|
|
2465
3179
|
|
|
2466
3180
|
// ---------------------------------------------------------------------------
|
|
@@ -2477,11 +3191,10 @@ async function fetchHsAccountStatus() {
|
|
|
2477
3191
|
const hs = data.environment?.tools?.hubspot;
|
|
2478
3192
|
|
|
2479
3193
|
if (hs && hs.authenticated && hs.portalName) {
|
|
2480
|
-
|
|
2481
|
-
pill.
|
|
3194
|
+
const idSuffix = hs.portalId ? ` (${hs.portalId})` : "";
|
|
3195
|
+
pill.innerHTML = `<span class="statusbar__dot statusbar__dot--ok"></span><span class="statusbar__item-text">${escapeHtml(hs.portalName)}${escapeHtml(idSuffix)}</span>`;
|
|
2482
3196
|
} else {
|
|
2483
3197
|
pill.textContent = "";
|
|
2484
|
-
pill.classList.remove("statusbar__pill--visible");
|
|
2485
3198
|
}
|
|
2486
3199
|
} catch {
|
|
2487
3200
|
// Silently ignore
|
|
@@ -2527,6 +3240,7 @@ document.getElementById("theme-name")?.addEventListener("dblclick", () => {
|
|
|
2527
3240
|
.then((data) => {
|
|
2528
3241
|
if (data.ok) {
|
|
2529
3242
|
el.textContent = data.newName;
|
|
3243
|
+
setStatusTheme(data.newName);
|
|
2530
3244
|
if (typeof currentAppTheme !== "undefined") currentAppTheme = data.newName;
|
|
2531
3245
|
window.location.hash = "#/app/" + encodeURIComponent(data.newName);
|
|
2532
3246
|
// Update rail item
|