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/setup.js
CHANGED
|
@@ -1,25 +1,106 @@
|
|
|
1
|
-
/* Theme init — runs synchronously before DOM to prevent flash
|
|
1
|
+
/* Theme init — runs synchronously before DOM to prevent flash.
|
|
2
|
+
Light is the default to match HubSpot's light-first ecosystem. */
|
|
3
|
+
const VIBESPOT_THEMES = ["dark", "light", "hubspot"];
|
|
4
|
+
const VIBESPOT_THEME_LABELS = {
|
|
5
|
+
dark: "Dark",
|
|
6
|
+
light: "Light",
|
|
7
|
+
hubspot: "HubSpot Light",
|
|
8
|
+
};
|
|
2
9
|
(function initTheme() {
|
|
3
10
|
const stored = localStorage.getItem("vibespot-theme");
|
|
4
|
-
const
|
|
5
|
-
const theme = stored || (prefersDark ? "dark" : "light");
|
|
11
|
+
const theme = VIBESPOT_THEMES.includes(stored) ? stored : "light";
|
|
6
12
|
document.documentElement.setAttribute("data-theme", theme);
|
|
7
13
|
})();
|
|
8
14
|
|
|
15
|
+
function syncThemeToggleLabel() {
|
|
16
|
+
const btn = document.querySelector(".theme-toggle");
|
|
17
|
+
if (!btn) return;
|
|
18
|
+
const current = document.documentElement.getAttribute("data-theme") || "dark";
|
|
19
|
+
const idx = VIBESPOT_THEMES.indexOf(current);
|
|
20
|
+
const next = VIBESPOT_THEMES[(idx + 1) % VIBESPOT_THEMES.length];
|
|
21
|
+
const label = `Theme: ${VIBESPOT_THEME_LABELS[current] || current} — switch to ${VIBESPOT_THEME_LABELS[next] || next}`;
|
|
22
|
+
btn.setAttribute("title", label);
|
|
23
|
+
btn.setAttribute("aria-label", label);
|
|
24
|
+
}
|
|
25
|
+
document.addEventListener("DOMContentLoaded", syncThemeToggleLabel);
|
|
26
|
+
|
|
9
27
|
function toggleTheme() {
|
|
10
28
|
const current = document.documentElement.getAttribute("data-theme") || "dark";
|
|
11
|
-
const
|
|
29
|
+
const idx = VIBESPOT_THEMES.indexOf(current);
|
|
30
|
+
const next = VIBESPOT_THEMES[((idx === -1 ? 0 : idx) + 1) % VIBESPOT_THEMES.length];
|
|
12
31
|
document.documentElement.setAttribute("data-theme", next);
|
|
13
32
|
localStorage.setItem("vibespot-theme", next);
|
|
33
|
+
syncThemeToggleLabel();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ---------------------------------------------------------------------------
|
|
37
|
+
* HubSpot portal indicator (topbar) — visible in both Project Home and Editor
|
|
38
|
+
* Reads /api/settings/status and reflects the active HubSpot portal's
|
|
39
|
+
* connection state. Clicking opens Settings (HubSpot tab).
|
|
40
|
+
* ------------------------------------------------------------------------- */
|
|
41
|
+
async function refreshPortalIndicator() {
|
|
42
|
+
const link = document.getElementById("topbar-portal-indicator");
|
|
43
|
+
const label = document.getElementById("topbar-portal-label");
|
|
44
|
+
if (!link || !label) return;
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch("/api/settings/status");
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
const hs = data && data.environment && data.environment.tools && data.environment.tools.hubspot;
|
|
49
|
+
if (hs && hs.authenticated && hs.portalName) {
|
|
50
|
+
const portal = hs.portalId ? `${hs.portalName} (${hs.portalId})` : hs.portalName;
|
|
51
|
+
link.classList.add("portal-indicator--connected");
|
|
52
|
+
link.classList.remove("portal-indicator--disconnected");
|
|
53
|
+
link.setAttribute("title", `Connected to HubSpot portal ${portal}`);
|
|
54
|
+
link.setAttribute("aria-label", `Connected to HubSpot portal ${portal}`);
|
|
55
|
+
label.textContent = portal;
|
|
56
|
+
} else {
|
|
57
|
+
link.classList.remove("portal-indicator--connected");
|
|
58
|
+
link.classList.add("portal-indicator--disconnected");
|
|
59
|
+
link.setAttribute("title", "No HubSpot portal connected — open Settings to add one");
|
|
60
|
+
link.setAttribute("aria-label", "No HubSpot portal connected — open Settings to add one");
|
|
61
|
+
label.textContent = "Not connected";
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Server unreachable — leave the disconnected state in place.
|
|
65
|
+
}
|
|
14
66
|
}
|
|
15
67
|
|
|
68
|
+
function bindPortalIndicator() {
|
|
69
|
+
const link = document.getElementById("topbar-portal-indicator");
|
|
70
|
+
if (!link || link.dataset.bound === "1") return;
|
|
71
|
+
link.dataset.bound = "1";
|
|
72
|
+
link.addEventListener("click", (event) => {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
// In Editor mode, the workspace tab "settings" exists; otherwise the
|
|
75
|
+
// Project Home settings button opens the same overlay.
|
|
76
|
+
const editorTab = document.getElementById("ws-tab-settings");
|
|
77
|
+
const setupBtn = document.getElementById("btn-setup-settings");
|
|
78
|
+
const appBody = document.getElementById("app-body");
|
|
79
|
+
const isEditor = appBody && appBody.getAttribute("data-mode") === "editor";
|
|
80
|
+
const target = isEditor ? editorTab : setupBtn;
|
|
81
|
+
if (target && typeof target.click === "function") target.click();
|
|
82
|
+
else if (editorTab) editorTab.click();
|
|
83
|
+
else if (setupBtn) setupBtn.click();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
88
|
+
bindPortalIndicator();
|
|
89
|
+
refreshPortalIndicator();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Re-poll occasionally so a freshly-saved PAT shows up without reload.
|
|
93
|
+
setInterval(() => { refreshPortalIndicator(); }, 30000);
|
|
94
|
+
|
|
16
95
|
/**
|
|
17
96
|
* Setup screen — onboarding flow in the browser.
|
|
18
97
|
* Handles theme creation, fetching, opening, and session resume.
|
|
19
98
|
*/
|
|
20
99
|
|
|
21
100
|
const setupScreen = document.getElementById("setup-screen");
|
|
22
|
-
const appScreen = document.getElementById("
|
|
101
|
+
const appScreen = document.getElementById("editor");
|
|
102
|
+
const appBody = document.getElementById("app-body");
|
|
103
|
+
let _serverContentMode = "page";
|
|
23
104
|
|
|
24
105
|
// ---------------------------------------------------------------------------
|
|
25
106
|
// Load setup info on page load
|
|
@@ -36,11 +117,11 @@ const ENGINE_DISPLAY_NAMES = {
|
|
|
36
117
|
|
|
37
118
|
async function initSetup() {
|
|
38
119
|
try {
|
|
39
|
-
// Show loading spinner in
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
<div class="project-
|
|
120
|
+
// Show loading spinner in switcher while fetching
|
|
121
|
+
const switcherList = document.getElementById("project-switcher-list");
|
|
122
|
+
if (switcherList) {
|
|
123
|
+
switcherList.innerHTML = `
|
|
124
|
+
<div class="project-switcher__loading">
|
|
44
125
|
<div class="setup__spinner"></div>
|
|
45
126
|
<span>Loading projects...</span>
|
|
46
127
|
</div>`;
|
|
@@ -49,7 +130,7 @@ async function initSetup() {
|
|
|
49
130
|
const res = await fetch("/api/setup");
|
|
50
131
|
const info = await res.json();
|
|
51
132
|
|
|
52
|
-
// Populate the project
|
|
133
|
+
// Populate the project switcher with all projects (used in editor mode)
|
|
53
134
|
populateProjectRail(info);
|
|
54
135
|
|
|
55
136
|
// Show "Continue where you left off" cards above the create options
|
|
@@ -96,29 +177,118 @@ async function initSetup() {
|
|
|
96
177
|
}, 0);
|
|
97
178
|
}
|
|
98
179
|
|
|
99
|
-
//
|
|
180
|
+
// First-visit product intro: 3-step walkthrough explaining vibeSpot.
|
|
181
|
+
// Add ?intro to URL to force-show it for testing.
|
|
182
|
+
const params = new URLSearchParams(location.search);
|
|
183
|
+
const introSeen = localStorage.getItem(INTRO_SEEN_KEY) === "1";
|
|
184
|
+
const isFreshUser = info.sessions.length === 0 && info.localThemes.length === 0;
|
|
185
|
+
if (params.has("intro") || (!introSeen && isFreshUser)) {
|
|
186
|
+
showIntroWalkthrough(info);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if we should show the engine-setup walkthrough (fresh environment).
|
|
100
191
|
// Add ?walkthrough to URL to force-show it for testing
|
|
101
|
-
if (
|
|
102
|
-
(!info.aiAvailable &&
|
|
192
|
+
if (params.has("walkthrough") ||
|
|
193
|
+
(!info.aiAvailable && isFreshUser)) {
|
|
103
194
|
showWalkthrough();
|
|
104
195
|
return;
|
|
105
196
|
}
|
|
106
197
|
|
|
198
|
+
// Track server content mode (email vs page)
|
|
199
|
+
_serverContentMode = info.contentMode || "page";
|
|
200
|
+
|
|
107
201
|
// Reset panel state
|
|
108
202
|
remoteThemesLoaded = false;
|
|
109
203
|
|
|
110
204
|
// Reset starter cache so each visit re-fetches from server
|
|
111
205
|
_startersCache = null;
|
|
112
206
|
|
|
113
|
-
//
|
|
207
|
+
// Set warm time-of-day greeting; show asset-type cards as the primary entry.
|
|
208
|
+
initGuidedEntry();
|
|
209
|
+
|
|
210
|
+
// Reset to cards-first state (no panel, prompt hidden).
|
|
114
211
|
activePanel = null;
|
|
115
|
-
|
|
212
|
+
showAssetTypeCards();
|
|
116
213
|
|
|
117
214
|
} catch (err) {
|
|
118
215
|
showError("Could not connect to server. Is vibeSpot running?");
|
|
119
216
|
}
|
|
120
217
|
}
|
|
121
218
|
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Guided entry — time-of-day greeting + asset-type card flow (VIB-255)
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function initGuidedEntry() {
|
|
224
|
+
const textEl = document.getElementById("setup-greeting-text");
|
|
225
|
+
if (!textEl) return;
|
|
226
|
+
const hour = new Date().getHours();
|
|
227
|
+
let greeting = "Welcome";
|
|
228
|
+
if (hour < 5) greeting = "Working late";
|
|
229
|
+
else if (hour < 12) greeting = "Good morning";
|
|
230
|
+
else if (hour < 17) greeting = "Good afternoon";
|
|
231
|
+
else greeting = "Good evening";
|
|
232
|
+
textEl.textContent = greeting;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let _selectedAssetType = null;
|
|
236
|
+
|
|
237
|
+
function showAssetTypeCards() {
|
|
238
|
+
const cards = document.getElementById("setup-type-cards");
|
|
239
|
+
const promptCard = document.getElementById("setup-prompt-card");
|
|
240
|
+
const recent = document.getElementById("setup-recent");
|
|
241
|
+
const promptInput = document.getElementById("setup-prompt-input");
|
|
242
|
+
const question = document.getElementById("setup-question");
|
|
243
|
+
const importPanel = document.getElementById("setup-import-sources");
|
|
244
|
+
if (cards) cards.classList.remove("hidden");
|
|
245
|
+
if (question) question.classList.remove("hidden");
|
|
246
|
+
if (importPanel) importPanel.classList.add("hidden");
|
|
247
|
+
if (promptCard) {
|
|
248
|
+
promptCard.classList.add("hidden");
|
|
249
|
+
promptCard.dataset.assetType = "";
|
|
250
|
+
}
|
|
251
|
+
if (recent && recent.dataset.hasItems === "1") recent.classList.remove("hidden");
|
|
252
|
+
if (promptInput) {
|
|
253
|
+
promptInput.value = "";
|
|
254
|
+
const submit = document.getElementById("setup-prompt-submit");
|
|
255
|
+
if (submit) submit.disabled = true;
|
|
256
|
+
}
|
|
257
|
+
// Close any open advanced panel (e.g. starter grid) when returning to cards.
|
|
258
|
+
document.querySelectorAll(".setup__panel").forEach((p) => p.classList.add("hidden"));
|
|
259
|
+
document.querySelectorAll(".setup__action-btn").forEach((b) => b.classList.remove("active"));
|
|
260
|
+
activePanel = null;
|
|
261
|
+
_selectedAssetType = null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function showScopedPrompt(card) {
|
|
265
|
+
const cards = document.getElementById("setup-type-cards");
|
|
266
|
+
const promptCard = document.getElementById("setup-prompt-card");
|
|
267
|
+
const recent = document.getElementById("setup-recent");
|
|
268
|
+
const eyebrow = document.getElementById("setup-prompt-eyebrow");
|
|
269
|
+
const input = document.getElementById("setup-prompt-input");
|
|
270
|
+
if (!promptCard || !input) return;
|
|
271
|
+
|
|
272
|
+
const assetType = card.dataset.assetType || "landing-page";
|
|
273
|
+
const placeholder = card.dataset.promptPlaceholder || "Describe what you want to build...";
|
|
274
|
+
const label = card.dataset.promptEyebrow || "Project";
|
|
275
|
+
|
|
276
|
+
_selectedAssetType = assetType;
|
|
277
|
+
promptCard.dataset.assetType = assetType;
|
|
278
|
+
if (eyebrow) eyebrow.textContent = label;
|
|
279
|
+
input.placeholder = placeholder;
|
|
280
|
+
input.setAttribute("aria-label", placeholder.replace(/\.\.\.$/, ""));
|
|
281
|
+
|
|
282
|
+
// Stash the selected type so downstream session/chat code can read it later.
|
|
283
|
+
window.__pendingAssetType = assetType;
|
|
284
|
+
|
|
285
|
+
if (cards) cards.classList.add("hidden");
|
|
286
|
+
if (recent) recent.classList.add("hidden");
|
|
287
|
+
promptCard.classList.remove("hidden");
|
|
288
|
+
|
|
289
|
+
setTimeout(() => input.focus(), 60);
|
|
290
|
+
}
|
|
291
|
+
|
|
122
292
|
async function saveAlertApiKey(key) {
|
|
123
293
|
if (!key) return;
|
|
124
294
|
// Detect provider from key prefix
|
|
@@ -169,6 +339,9 @@ function deduplicateProjects(info) {
|
|
|
169
339
|
updatedAt: s.updatedAt,
|
|
170
340
|
moduleCount: s.moduleCount ?? null,
|
|
171
341
|
templateCount: s.templateCount ?? null,
|
|
342
|
+
pageCount: s.pageCount ?? 0,
|
|
343
|
+
emailCount: s.emailCount ?? 0,
|
|
344
|
+
hasBrandAssets: s.hasBrandAssets ?? false,
|
|
172
345
|
});
|
|
173
346
|
}
|
|
174
347
|
}
|
|
@@ -184,6 +357,9 @@ function deduplicateProjects(info) {
|
|
|
184
357
|
updatedAt: null,
|
|
185
358
|
moduleCount: typeof t === "object" ? t.moduleCount ?? null : null,
|
|
186
359
|
templateCount: null,
|
|
360
|
+
pageCount: 0,
|
|
361
|
+
emailCount: 0,
|
|
362
|
+
hasBrandAssets: false,
|
|
187
363
|
});
|
|
188
364
|
}
|
|
189
365
|
}
|
|
@@ -196,6 +372,7 @@ function deduplicateProjects(info) {
|
|
|
196
372
|
// ---------------------------------------------------------------------------
|
|
197
373
|
|
|
198
374
|
const RECENT_PROJECTS_LIMIT = 4;
|
|
375
|
+
let _allProjects = [];
|
|
199
376
|
|
|
200
377
|
function populateRecentProjects(info) {
|
|
201
378
|
const section = document.getElementById("setup-recent");
|
|
@@ -205,15 +382,19 @@ function populateRecentProjects(info) {
|
|
|
205
382
|
|
|
206
383
|
const projects = deduplicateProjects(info);
|
|
207
384
|
if (projects.length === 0) {
|
|
385
|
+
_allProjects = [];
|
|
208
386
|
section.classList.add("hidden");
|
|
387
|
+
section.dataset.hasItems = "0";
|
|
209
388
|
list.innerHTML = "";
|
|
210
389
|
return;
|
|
211
390
|
}
|
|
391
|
+
section.dataset.hasItems = "1";
|
|
212
392
|
|
|
213
393
|
// Most recently updated first; locals (no updatedAt) follow
|
|
214
394
|
const withTime = projects.filter((p) => p.updatedAt).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
215
395
|
const withoutTime = projects.filter((p) => !p.updatedAt);
|
|
216
396
|
const ordered = [...withTime, ...withoutTime];
|
|
397
|
+
_allProjects = ordered;
|
|
217
398
|
const top = ordered.slice(0, RECENT_PROJECTS_LIMIT);
|
|
218
399
|
|
|
219
400
|
list.innerHTML = "";
|
|
@@ -232,6 +413,17 @@ function populateRecentProjects(info) {
|
|
|
232
413
|
`<span class="setup__recent-card-meta">${esc(meta)}</span>` +
|
|
233
414
|
`</span>`;
|
|
234
415
|
|
|
416
|
+
const delBtn = document.createElement("button");
|
|
417
|
+
delBtn.type = "button";
|
|
418
|
+
delBtn.className = "setup__recent-card-delete";
|
|
419
|
+
delBtn.innerHTML = "×";
|
|
420
|
+
delBtn.title = "Delete project";
|
|
421
|
+
delBtn.addEventListener("click", (e) => {
|
|
422
|
+
e.stopPropagation();
|
|
423
|
+
confirmDeleteProject(p);
|
|
424
|
+
});
|
|
425
|
+
card.appendChild(delBtn);
|
|
426
|
+
|
|
235
427
|
card.addEventListener("click", () => {
|
|
236
428
|
if (typeof isStreaming !== "undefined" && isStreaming) {
|
|
237
429
|
showError("Cannot switch projects while AI is generating.");
|
|
@@ -248,40 +440,43 @@ function populateRecentProjects(info) {
|
|
|
248
440
|
}
|
|
249
441
|
|
|
250
442
|
// ---------------------------------------------------------------------------
|
|
251
|
-
//
|
|
443
|
+
// Project switcher (rendered into the editor-mode rail popover)
|
|
252
444
|
// ---------------------------------------------------------------------------
|
|
253
445
|
|
|
254
446
|
const railTooltip = document.getElementById("project-rail-tooltip");
|
|
255
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Populate the project switcher menu with all projects. Item DOM uses the
|
|
450
|
+
* stable `.project-rail__item*` class names so the rename / delete logic in
|
|
451
|
+
* chat.js + setup.js can keep targeting them.
|
|
452
|
+
*/
|
|
256
453
|
function populateProjectRail(info) {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
rail.innerHTML = "";
|
|
454
|
+
const list = document.getElementById("project-switcher-list");
|
|
455
|
+
if (!list) return;
|
|
456
|
+
list.innerHTML = "";
|
|
261
457
|
|
|
262
458
|
const projects = deduplicateProjects(info);
|
|
263
|
-
if (countEl) countEl.textContent = projects.length;
|
|
264
459
|
|
|
265
460
|
if (projects.length === 0) {
|
|
266
|
-
|
|
461
|
+
list.innerHTML = '<div class="project-switcher__empty">No projects yet.<br>Create one to get started.</div>';
|
|
267
462
|
return;
|
|
268
463
|
}
|
|
269
464
|
|
|
270
465
|
for (const p of projects) {
|
|
271
|
-
const item = document.createElement("
|
|
466
|
+
const item = document.createElement("button");
|
|
467
|
+
item.type = "button";
|
|
272
468
|
item.className = "project-rail__item";
|
|
273
469
|
item.dataset.name = p.name;
|
|
470
|
+
if (p.sessionId) item.dataset.sessionId = p.sessionId;
|
|
274
471
|
|
|
275
472
|
const initial = p.name.charAt(0).toUpperCase();
|
|
276
473
|
const meta = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
|
|
277
474
|
|
|
278
|
-
// Bubble (always visible — in collapsed mode this is the only thing shown)
|
|
279
475
|
const bubble = document.createElement("div");
|
|
280
476
|
bubble.className = "project-rail__item-bubble";
|
|
281
477
|
bubble.textContent = initial;
|
|
282
478
|
item.appendChild(bubble);
|
|
283
479
|
|
|
284
|
-
// Info (visible when expanded via CSS)
|
|
285
480
|
const infoEl = document.createElement("div");
|
|
286
481
|
infoEl.className = "project-rail__item-info";
|
|
287
482
|
infoEl.innerHTML = `
|
|
@@ -289,7 +484,6 @@ function populateProjectRail(info) {
|
|
|
289
484
|
<span class="project-rail__item-meta">${meta}</span>`;
|
|
290
485
|
item.appendChild(infoEl);
|
|
291
486
|
|
|
292
|
-
// Double-click on name to rename
|
|
293
487
|
const nameSpan = infoEl.querySelector(".project-rail__item-name");
|
|
294
488
|
if (nameSpan) {
|
|
295
489
|
nameSpan.addEventListener("dblclick", (e) => {
|
|
@@ -298,8 +492,8 @@ function populateProjectRail(info) {
|
|
|
298
492
|
});
|
|
299
493
|
}
|
|
300
494
|
|
|
301
|
-
// Delete button (visible when expanded + hover)
|
|
302
495
|
const delBtn = document.createElement("button");
|
|
496
|
+
delBtn.type = "button";
|
|
303
497
|
delBtn.className = "project-rail__item-delete";
|
|
304
498
|
delBtn.innerHTML = "×";
|
|
305
499
|
delBtn.title = "Delete project";
|
|
@@ -309,44 +503,17 @@ function populateProjectRail(info) {
|
|
|
309
503
|
});
|
|
310
504
|
item.appendChild(delBtn);
|
|
311
505
|
|
|
312
|
-
// Tooltip (only when collapsed — skip when expanded since name is visible)
|
|
313
|
-
item.addEventListener("mouseenter", () => {
|
|
314
|
-
const railEl = document.getElementById("project-rail");
|
|
315
|
-
if (railEl && railEl.classList.contains("project-rail--expanded")) return;
|
|
316
|
-
|
|
317
|
-
let stats = "";
|
|
318
|
-
if (p.moduleCount != null) {
|
|
319
|
-
stats = p.moduleCount + " section" + (p.moduleCount !== 1 ? "s" : "");
|
|
320
|
-
if (p.templateCount > 1) stats += " \u00b7 " + p.templateCount + " templates";
|
|
321
|
-
stats += p.updatedAt ? " \u00b7 " + timeAgo(p.updatedAt) : " \u00b7 on disk";
|
|
322
|
-
} else {
|
|
323
|
-
stats = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
railTooltip.innerHTML =
|
|
327
|
-
'<div class="project-rail__tooltip-name">' + esc(p.name) + "</div>" +
|
|
328
|
-
'<div class="project-rail__tooltip-stats">' + stats + "</div>";
|
|
329
|
-
|
|
330
|
-
const rect = item.getBoundingClientRect();
|
|
331
|
-
railTooltip.style.top = rect.top + "px";
|
|
332
|
-
railTooltip.classList.add("project-rail__tooltip--visible");
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
item.addEventListener("mouseleave", () => {
|
|
336
|
-
railTooltip.classList.remove("project-rail__tooltip--visible");
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
// Click to open (blocked while AI is generating)
|
|
340
506
|
item.addEventListener("click", () => {
|
|
341
507
|
if (typeof isStreaming !== "undefined" && isStreaming) {
|
|
342
508
|
showError("Cannot switch projects while AI is generating.");
|
|
343
509
|
return;
|
|
344
510
|
}
|
|
511
|
+
closeProjectSwitcher();
|
|
345
512
|
if (p.sessionId) resumeSession(p.sessionId);
|
|
346
513
|
else openTheme(p.name);
|
|
347
514
|
});
|
|
348
515
|
|
|
349
|
-
|
|
516
|
+
list.appendChild(item);
|
|
350
517
|
}
|
|
351
518
|
|
|
352
519
|
updateRailActive();
|
|
@@ -357,14 +524,99 @@ function updateRailActive() {
|
|
|
357
524
|
document.querySelectorAll(".project-rail__item").forEach((btn) => {
|
|
358
525
|
btn.classList.toggle("project-rail__item--active", btn.dataset.name === current);
|
|
359
526
|
});
|
|
527
|
+
// Refresh the rail's current-project bubble + name (editor mode only).
|
|
528
|
+
const bubble = document.getElementById("project-rail-current-bubble");
|
|
529
|
+
const nameEl = document.getElementById("project-rail-current-name");
|
|
530
|
+
if (bubble) bubble.textContent = current ? current.charAt(0).toUpperCase() : "P";
|
|
531
|
+
if (nameEl) nameEl.textContent = current || "";
|
|
532
|
+
const trigger = document.getElementById("project-rail-current");
|
|
533
|
+
if (trigger) trigger.title = current ? current + " — switch project" : "Switch project";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
// Switcher popover open/close
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
function openProjectSwitcher() {
|
|
541
|
+
const popover = document.getElementById("project-switcher");
|
|
542
|
+
const trigger = document.getElementById("project-rail-current");
|
|
543
|
+
if (!popover || !trigger) return;
|
|
544
|
+
const rect = trigger.getBoundingClientRect();
|
|
545
|
+
popover.style.top = Math.max(8, rect.top) + "px";
|
|
546
|
+
popover.hidden = false;
|
|
547
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
548
|
+
// Refresh data so the list reflects the latest sessions.
|
|
549
|
+
fetch("/api/setup")
|
|
550
|
+
.then((r) => r.json())
|
|
551
|
+
.then((info) => populateProjectRail(info))
|
|
552
|
+
.catch(() => {});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function closeProjectSwitcher() {
|
|
556
|
+
const popover = document.getElementById("project-switcher");
|
|
557
|
+
const trigger = document.getElementById("project-rail-current");
|
|
558
|
+
if (popover) popover.hidden = true;
|
|
559
|
+
if (trigger) trigger.setAttribute("aria-expanded", "false");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function toggleProjectSwitcher() {
|
|
563
|
+
const popover = document.getElementById("project-switcher");
|
|
564
|
+
if (!popover) return;
|
|
565
|
+
if (popover.hidden) openProjectSwitcher();
|
|
566
|
+
else closeProjectSwitcher();
|
|
360
567
|
}
|
|
361
568
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
569
|
+
document.getElementById("project-rail-current")?.addEventListener("click", (e) => {
|
|
570
|
+
e.stopPropagation();
|
|
571
|
+
toggleProjectSwitcher();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
document.getElementById("project-rail-back")?.addEventListener("click", () => {
|
|
575
|
+
if (typeof isStreaming !== "undefined" && isStreaming) {
|
|
576
|
+
showError("Cannot leave the editor while AI is generating.");
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
closeProjectSwitcher();
|
|
580
|
+
showSetup();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
document.getElementById("project-switcher-add")?.addEventListener("click", () => {
|
|
584
|
+
closeProjectSwitcher();
|
|
585
|
+
showSetup();
|
|
365
586
|
togglePanel("new");
|
|
366
587
|
});
|
|
367
588
|
|
|
589
|
+
// Close on outside click / Escape
|
|
590
|
+
document.addEventListener("click", (e) => {
|
|
591
|
+
const popover = document.getElementById("project-switcher");
|
|
592
|
+
if (!popover || popover.hidden) return;
|
|
593
|
+
const trigger = document.getElementById("project-rail-current");
|
|
594
|
+
if (popover.contains(e.target) || (trigger && trigger.contains(e.target))) return;
|
|
595
|
+
closeProjectSwitcher();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
document.addEventListener("keydown", (e) => {
|
|
599
|
+
if (e.key === "Escape") closeProjectSwitcher();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Tooltip on the current-project bubble (editor rail)
|
|
603
|
+
document.getElementById("project-rail-current")?.addEventListener("mouseenter", () => {
|
|
604
|
+
const popover = document.getElementById("project-switcher");
|
|
605
|
+
if (popover && !popover.hidden) return;
|
|
606
|
+
const name = currentAppTheme || currentDashboardTheme || "";
|
|
607
|
+
if (!name) return;
|
|
608
|
+
railTooltip.innerHTML =
|
|
609
|
+
'<div class="project-rail__tooltip-name">' + esc(name) + "</div>" +
|
|
610
|
+
'<div class="project-rail__tooltip-stats">Click to switch project</div>';
|
|
611
|
+
const rect = document.getElementById("project-rail-current").getBoundingClientRect();
|
|
612
|
+
railTooltip.style.top = rect.top + "px";
|
|
613
|
+
railTooltip.classList.add("project-rail__tooltip--visible");
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
document.getElementById("project-rail-current")?.addEventListener("mouseleave", () => {
|
|
617
|
+
railTooltip.classList.remove("project-rail__tooltip--visible");
|
|
618
|
+
});
|
|
619
|
+
|
|
368
620
|
// ---------------------------------------------------------------------------
|
|
369
621
|
// Inline rename
|
|
370
622
|
// ---------------------------------------------------------------------------
|
|
@@ -497,6 +749,146 @@ function confirmDeleteProject(project) {
|
|
|
497
749
|
});
|
|
498
750
|
}
|
|
499
751
|
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
// First-visit product intro walkthrough (3 steps)
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
|
|
756
|
+
const INTRO_SEEN_KEY = "vibespot:introSeen";
|
|
757
|
+
const INTRO_SAMPLE_PROMPT =
|
|
758
|
+
"A landing page for a B2B SaaS product called Northwind Analytics. " +
|
|
759
|
+
"Include a hero with a headline and CTA, three feature cards, a customer logo bar, " +
|
|
760
|
+
"a testimonial, and a final call-to-action section.";
|
|
761
|
+
|
|
762
|
+
function renderIntroProgress(stepIndex, totalSteps) {
|
|
763
|
+
let html = "";
|
|
764
|
+
for (let i = 0; i < totalSteps; i++) {
|
|
765
|
+
const cls = i === stepIndex ? "active" : i < stepIndex ? "done" : "";
|
|
766
|
+
html += `<div class="walkthrough__step-dot ${cls}">${i < stepIndex ? vsIcon("check", {size: "sm"}) : i + 1}</div>`;
|
|
767
|
+
if (i < totalSteps - 1) html += `<div class="walkthrough__step-line"></div>`;
|
|
768
|
+
}
|
|
769
|
+
return html;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function dismissIntroWalkthrough(info) {
|
|
773
|
+
try { localStorage.setItem(INTRO_SEEN_KEY, "1"); } catch {}
|
|
774
|
+
const url = new URL(location.href);
|
|
775
|
+
if (url.searchParams.has("intro")) {
|
|
776
|
+
url.searchParams.delete("intro");
|
|
777
|
+
history.replaceState(null, "", url.pathname + url.search + url.hash);
|
|
778
|
+
}
|
|
779
|
+
document.getElementById("walkthrough").classList.add("hidden");
|
|
780
|
+
// If the user still has no AI engine on a fresh install, fall through to
|
|
781
|
+
// the engine-setup walkthrough; otherwise reveal the normal setup options.
|
|
782
|
+
if (info && !info.aiAvailable && (info.sessions || []).length === 0 && (info.localThemes || []).length === 0) {
|
|
783
|
+
showWalkthrough();
|
|
784
|
+
} else {
|
|
785
|
+
document.getElementById("setup-options").classList.remove("hidden");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function showIntroWalkthrough(info) {
|
|
790
|
+
const walkthrough = document.getElementById("walkthrough");
|
|
791
|
+
const options = document.getElementById("setup-options");
|
|
792
|
+
const progress = document.getElementById("walkthrough-progress");
|
|
793
|
+
const content = document.getElementById("walkthrough-content");
|
|
794
|
+
if (!walkthrough || !options || !progress || !content) return;
|
|
795
|
+
|
|
796
|
+
walkthrough.classList.remove("hidden");
|
|
797
|
+
options.classList.add("hidden");
|
|
798
|
+
|
|
799
|
+
const STEPS = [
|
|
800
|
+
{
|
|
801
|
+
title: "Welcome to vibeSpot",
|
|
802
|
+
body: `
|
|
803
|
+
<p>vibeSpot turns plain-language descriptions into native HubSpot CMS landing pages.
|
|
804
|
+
Describe what you want, watch a live preview build, then upload the result straight to HubSpot.</p>
|
|
805
|
+
<ul class="walkthrough__bullets">
|
|
806
|
+
<li>Chat-driven editing with a side-by-side preview</li>
|
|
807
|
+
<li>Generates real HubL modules, not screenshots or mockups</li>
|
|
808
|
+
<li>Works with Claude, OpenAI, or Gemini — API key or CLI</li>
|
|
809
|
+
</ul>
|
|
810
|
+
`,
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
title: "How it maps to HubSpot",
|
|
814
|
+
body: `
|
|
815
|
+
<p>Every vibeSpot page becomes a fully editable HubSpot theme. The pieces line up like this:</p>
|
|
816
|
+
<ul class="walkthrough__bullets">
|
|
817
|
+
<li><strong>Sections</strong> → HubSpot <strong>modules</strong> with editable fields</li>
|
|
818
|
+
<li><strong>Shared CSS & tokens</strong> → theme-level <code>:root</code> variables</li>
|
|
819
|
+
<li><strong>Project</strong> → uploadable <strong>HubSpot CMS theme</strong> with templates</li>
|
|
820
|
+
</ul>
|
|
821
|
+
<p>Marketers can keep editing fields in HubSpot after upload — no code changes required.</p>
|
|
822
|
+
`,
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
title: "Try it with a pre-filled prompt",
|
|
826
|
+
body: `
|
|
827
|
+
<p>We’ll drop a sample prompt into the builder so you can see vibeSpot in action.
|
|
828
|
+
You can edit it before pressing <strong>Build</strong>.</p>
|
|
829
|
+
<div class="walkthrough__card walkthrough__sample-prompt">
|
|
830
|
+
<div class="walkthrough__card-title">Sample prompt</div>
|
|
831
|
+
<div class="walkthrough__sample-prompt-body">${esc(INTRO_SAMPLE_PROMPT)}</div>
|
|
832
|
+
</div>
|
|
833
|
+
`,
|
|
834
|
+
},
|
|
835
|
+
];
|
|
836
|
+
|
|
837
|
+
let stepIndex = 0;
|
|
838
|
+
|
|
839
|
+
function render() {
|
|
840
|
+
const step = STEPS[stepIndex];
|
|
841
|
+
progress.innerHTML = renderIntroProgress(stepIndex, STEPS.length);
|
|
842
|
+
|
|
843
|
+
const isLast = stepIndex === STEPS.length - 1;
|
|
844
|
+
const primaryLabel = isLast ? "Try it now" : "Next";
|
|
845
|
+
const backBtn = stepIndex > 0
|
|
846
|
+
? `<button class="btn btn--secondary" id="intro-back">Back</button>`
|
|
847
|
+
: "";
|
|
848
|
+
|
|
849
|
+
content.innerHTML = `
|
|
850
|
+
<div class="walkthrough__step-title">${esc(step.title)}</div>
|
|
851
|
+
<div class="walkthrough__step-desc">${step.body}</div>
|
|
852
|
+
<div class="walkthrough__actions">
|
|
853
|
+
<button class="btn btn--ghost" id="intro-skip">Skip intro</button>
|
|
854
|
+
<span class="walkthrough__actions-spacer"></span>
|
|
855
|
+
${backBtn}
|
|
856
|
+
<button class="btn btn--primary" id="intro-next">${primaryLabel}</button>
|
|
857
|
+
</div>
|
|
858
|
+
`;
|
|
859
|
+
|
|
860
|
+
document.getElementById("intro-skip").addEventListener("click", () => dismissIntroWalkthrough(info));
|
|
861
|
+
document.getElementById("intro-next").addEventListener("click", () => {
|
|
862
|
+
if (isLast) {
|
|
863
|
+
// Pre-fill the prompt and hand off to the normal builder flow.
|
|
864
|
+
const promptInput = document.getElementById("setup-prompt-input");
|
|
865
|
+
if (promptInput) {
|
|
866
|
+
promptInput.value = INTRO_SAMPLE_PROMPT;
|
|
867
|
+
promptInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
868
|
+
}
|
|
869
|
+
dismissIntroWalkthrough(info);
|
|
870
|
+
// Focus the prompt so the user lands ready to edit / submit.
|
|
871
|
+
setTimeout(() => {
|
|
872
|
+
const el = document.getElementById("setup-prompt-input");
|
|
873
|
+
if (el) {
|
|
874
|
+
el.focus();
|
|
875
|
+
if (typeof el.scrollIntoView === "function") {
|
|
876
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}, 0);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
stepIndex++;
|
|
883
|
+
render();
|
|
884
|
+
});
|
|
885
|
+
const backEl = document.getElementById("intro-back");
|
|
886
|
+
if (backEl) backEl.addEventListener("click", () => { stepIndex--; render(); });
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
render();
|
|
890
|
+
}
|
|
891
|
+
|
|
500
892
|
// ---------------------------------------------------------------------------
|
|
501
893
|
// Guided walkthrough (first-run experience)
|
|
502
894
|
// ---------------------------------------------------------------------------
|
|
@@ -765,10 +1157,12 @@ async function startFromPrompt() {
|
|
|
765
1157
|
showLoading("Creating theme...");
|
|
766
1158
|
|
|
767
1159
|
try {
|
|
1160
|
+
const createBody = { name: themeName };
|
|
1161
|
+
if (window.__pendingAssetType) createBody.assetType = window.__pendingAssetType;
|
|
768
1162
|
const res = await fetch("/api/setup/create", {
|
|
769
1163
|
method: "POST",
|
|
770
1164
|
headers: { "Content-Type": "application/json" },
|
|
771
|
-
body: JSON.stringify(
|
|
1165
|
+
body: JSON.stringify(createBody),
|
|
772
1166
|
});
|
|
773
1167
|
const data = await res.json();
|
|
774
1168
|
|
|
@@ -833,7 +1227,10 @@ function renderStarterGrid(starters) {
|
|
|
833
1227
|
return;
|
|
834
1228
|
}
|
|
835
1229
|
|
|
836
|
-
|
|
1230
|
+
const pageStarters = starters.filter((s) => s.contentType !== "email");
|
|
1231
|
+
const emailStarters = starters.filter((s) => s.contentType === "email");
|
|
1232
|
+
|
|
1233
|
+
const renderCards = (list) => list.map((s) => `
|
|
837
1234
|
<div class="starter-card${_selectedStarterId === s.id ? " selected" : ""}" data-starter-id="${escHtml(s.id)}">
|
|
838
1235
|
<span class="starter-card__name">${escHtml(s.name)}</span>
|
|
839
1236
|
<span class="starter-card__desc">${escHtml(s.description)}</span>
|
|
@@ -841,6 +1238,22 @@ function renderStarterGrid(starters) {
|
|
|
841
1238
|
</div>
|
|
842
1239
|
`).join("");
|
|
843
1240
|
|
|
1241
|
+
const renderGroup = (title, list) =>
|
|
1242
|
+
`<div class="starter-grid__group">
|
|
1243
|
+
<h4 class="starter-grid__heading">${escHtml(title)}</h4>
|
|
1244
|
+
<div class="starter-grid__section">${renderCards(list)}</div>
|
|
1245
|
+
</div>`;
|
|
1246
|
+
|
|
1247
|
+
let html = "";
|
|
1248
|
+
if (_serverContentMode === "email") {
|
|
1249
|
+
if (emailStarters.length > 0) html += renderGroup("Email Templates", emailStarters);
|
|
1250
|
+
if (pageStarters.length > 0) html += renderGroup("Page Templates", pageStarters);
|
|
1251
|
+
} else {
|
|
1252
|
+
if (pageStarters.length > 0) html += renderGroup("Page Templates", pageStarters);
|
|
1253
|
+
if (emailStarters.length > 0) html += renderGroup("Email Templates", emailStarters);
|
|
1254
|
+
}
|
|
1255
|
+
grid.innerHTML = html;
|
|
1256
|
+
|
|
844
1257
|
grid.querySelectorAll(".starter-card").forEach((card) => {
|
|
845
1258
|
card.addEventListener("click", () => selectStarter(card.dataset.starterId));
|
|
846
1259
|
});
|
|
@@ -892,7 +1305,8 @@ async function createFromStarter() {
|
|
|
892
1305
|
}
|
|
893
1306
|
|
|
894
1307
|
async function fetchTheme() {
|
|
895
|
-
const
|
|
1308
|
+
const nameEl = document.getElementById("fetch-theme-name") || document.getElementById("dl-theme-name");
|
|
1309
|
+
const name = nameEl ? nameEl.value.trim() : "";
|
|
896
1310
|
if (!name) {
|
|
897
1311
|
showError("Please enter the theme name from your HubSpot account.");
|
|
898
1312
|
return;
|
|
@@ -997,16 +1411,12 @@ function showApp(themeName) {
|
|
|
997
1411
|
* Used as fallback or when navigating from dashboard to a specific template.
|
|
998
1412
|
*/
|
|
999
1413
|
function showAppDirect(themeName) {
|
|
1000
|
-
setupScreen.classList.add("hidden");
|
|
1001
|
-
document.getElementById("setup-topbar").classList.add("hidden");
|
|
1002
|
-
document.getElementById("project-rail")?.classList.remove("project-rail--expanded");
|
|
1003
1414
|
if (typeof hideDashboard === "function") hideDashboard();
|
|
1415
|
+
appBody.dataset.mode = "editor";
|
|
1004
1416
|
appScreen.classList.remove("hidden");
|
|
1417
|
+
document.getElementById("project-rail")?.setAttribute("data-mode", "editor");
|
|
1005
1418
|
document.getElementById("theme-name").textContent = themeName;
|
|
1006
1419
|
|
|
1007
|
-
const urlEl = document.getElementById("browser-url");
|
|
1008
|
-
if (urlEl) urlEl.textContent = themeName + ".vibespot.app";
|
|
1009
|
-
|
|
1010
1420
|
currentAppTheme = themeName;
|
|
1011
1421
|
const target = "#/app/" + encodeURIComponent(themeName);
|
|
1012
1422
|
if (location.hash !== target) {
|
|
@@ -1025,9 +1435,9 @@ function showAppDirect(themeName) {
|
|
|
1025
1435
|
function showSetup() {
|
|
1026
1436
|
appScreen.classList.add("hidden");
|
|
1027
1437
|
if (typeof hideDashboard === "function") hideDashboard();
|
|
1028
|
-
|
|
1029
|
-
document.getElementById("
|
|
1030
|
-
|
|
1438
|
+
appBody.dataset.mode = "project-home";
|
|
1439
|
+
document.getElementById("project-rail")?.setAttribute("data-mode", "project-home");
|
|
1440
|
+
closeProjectSwitcher();
|
|
1031
1441
|
currentAppTheme = "";
|
|
1032
1442
|
|
|
1033
1443
|
hideLoading();
|
|
@@ -1040,25 +1450,16 @@ function showSetup() {
|
|
|
1040
1450
|
initSetup();
|
|
1041
1451
|
}
|
|
1042
1452
|
|
|
1043
|
-
//
|
|
1044
|
-
document.getElementById("
|
|
1045
|
-
|
|
1046
|
-
appScreen.classList.add("hidden");
|
|
1047
|
-
showDashboard(currentAppTheme);
|
|
1048
|
-
}
|
|
1453
|
+
// Editor back button → go back to setup
|
|
1454
|
+
document.getElementById("editor-back")?.addEventListener("click", () => {
|
|
1455
|
+
showSetup();
|
|
1049
1456
|
});
|
|
1050
1457
|
|
|
1051
|
-
// Logo click → go back to setup
|
|
1458
|
+
// Logo click → go back to setup
|
|
1052
1459
|
document.querySelectorAll(".topbar__brand").forEach((el) => {
|
|
1053
1460
|
el.style.cursor = "pointer";
|
|
1054
1461
|
el.addEventListener("click", () => {
|
|
1055
|
-
|
|
1056
|
-
if (dashEl && !dashEl.classList.contains("hidden")) {
|
|
1057
|
-
showSetup();
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
// Fallback
|
|
1061
|
-
if (!appScreen.classList.contains("hidden")) {
|
|
1462
|
+
if (appBody.dataset.mode === "editor") {
|
|
1062
1463
|
showSetup();
|
|
1063
1464
|
}
|
|
1064
1465
|
});
|
|
@@ -1097,7 +1498,7 @@ let remoteThemesLoaded = false;
|
|
|
1097
1498
|
|
|
1098
1499
|
function togglePanel(action) {
|
|
1099
1500
|
const panels = document.querySelectorAll(".setup__panel");
|
|
1100
|
-
const buttons = document.querySelectorAll(".
|
|
1501
|
+
const buttons = document.querySelectorAll(".setup__entry-card");
|
|
1101
1502
|
|
|
1102
1503
|
// Close if same panel clicked
|
|
1103
1504
|
if (activePanel === action) {
|
|
@@ -1118,8 +1519,8 @@ function togglePanel(action) {
|
|
|
1118
1519
|
activePanel = action;
|
|
1119
1520
|
}
|
|
1120
1521
|
|
|
1121
|
-
// Mark
|
|
1122
|
-
const btn = document.querySelector(`.
|
|
1522
|
+
// Mark card active
|
|
1523
|
+
const btn = document.querySelector(`.setup__entry-card[data-action="${action}"]`);
|
|
1123
1524
|
if (btn) btn.classList.add("active");
|
|
1124
1525
|
|
|
1125
1526
|
// Focus input if applicable
|
|
@@ -1135,39 +1536,225 @@ function togglePanel(action) {
|
|
|
1135
1536
|
if (action === "continue") populateContinuePanel();
|
|
1136
1537
|
}
|
|
1137
1538
|
|
|
1539
|
+
let _bulkSelected = new Set();
|
|
1540
|
+
|
|
1138
1541
|
function populateContinuePanel() {
|
|
1139
1542
|
const container = document.getElementById("continue-projects");
|
|
1140
1543
|
const empty = document.getElementById("continue-empty");
|
|
1141
1544
|
if (!container) return;
|
|
1142
1545
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
if (railItems.length === 0) {
|
|
1546
|
+
const projects = _allProjects;
|
|
1547
|
+
if (!projects || projects.length === 0) {
|
|
1146
1548
|
container.innerHTML = "";
|
|
1147
|
-
|
|
1549
|
+
_bulkSelected.clear();
|
|
1550
|
+
if (empty) empty.classList.remove("hidden");
|
|
1148
1551
|
return;
|
|
1149
1552
|
}
|
|
1150
1553
|
|
|
1151
|
-
empty.classList.add("hidden");
|
|
1152
|
-
|
|
1554
|
+
if (empty) empty.classList.add("hidden");
|
|
1555
|
+
_bulkSelected.clear();
|
|
1556
|
+
|
|
1557
|
+
const toolbar = document.createElement("div");
|
|
1558
|
+
toolbar.className = "projects-bulk-toolbar hidden";
|
|
1559
|
+
toolbar.id = "projects-bulk-toolbar";
|
|
1560
|
+
toolbar.innerHTML =
|
|
1561
|
+
`<span class="projects-bulk-toolbar__count" id="bulk-count">0 selected</span>` +
|
|
1562
|
+
`<button type="button" class="btn btn--sm btn--secondary" id="bulk-duplicate">Duplicate</button>` +
|
|
1563
|
+
`<button type="button" class="btn btn--sm btn--danger" id="bulk-delete">Delete</button>`;
|
|
1564
|
+
|
|
1565
|
+
const table = document.createElement("table");
|
|
1566
|
+
table.className = "projects-table";
|
|
1567
|
+
table.innerHTML = `<thead><tr>
|
|
1568
|
+
<th class="projects-table__th-check"><input type="checkbox" id="bulk-select-all" class="projects-table__checkbox" title="Select all" /></th>
|
|
1569
|
+
<th>Name</th>
|
|
1570
|
+
<th>Pages</th>
|
|
1571
|
+
<th>Emails</th>
|
|
1572
|
+
<th>Modules</th>
|
|
1573
|
+
<th>Brand Assets</th>
|
|
1574
|
+
<th></th>
|
|
1575
|
+
</tr></thead>`;
|
|
1576
|
+
|
|
1577
|
+
const tbody = document.createElement("tbody");
|
|
1578
|
+
for (const p of projects) {
|
|
1579
|
+
const tr = document.createElement("tr");
|
|
1580
|
+
tr.dataset.projectName = p.name;
|
|
1581
|
+
|
|
1582
|
+
const checkTd = document.createElement("td");
|
|
1583
|
+
checkTd.className = "projects-table__td-check";
|
|
1584
|
+
const cb = document.createElement("input");
|
|
1585
|
+
cb.type = "checkbox";
|
|
1586
|
+
cb.className = "projects-table__checkbox";
|
|
1587
|
+
cb.dataset.projectName = p.name;
|
|
1588
|
+
cb.addEventListener("change", () => {
|
|
1589
|
+
if (cb.checked) _bulkSelected.add(p.name);
|
|
1590
|
+
else _bulkSelected.delete(p.name);
|
|
1591
|
+
tr.classList.toggle("projects-table__row--selected", cb.checked);
|
|
1592
|
+
syncBulkToolbar();
|
|
1593
|
+
});
|
|
1594
|
+
checkTd.appendChild(cb);
|
|
1595
|
+
tr.appendChild(checkTd);
|
|
1596
|
+
|
|
1597
|
+
const nameTd = document.createElement("td");
|
|
1598
|
+
nameTd.className = "projects-table__name";
|
|
1599
|
+
nameTd.textContent = p.name;
|
|
1600
|
+
tr.appendChild(nameTd);
|
|
1601
|
+
|
|
1602
|
+
for (const val of [p.pageCount ?? 0, p.emailCount ?? 0, p.moduleCount ?? 0]) {
|
|
1603
|
+
const td = document.createElement("td");
|
|
1604
|
+
td.textContent = String(val);
|
|
1605
|
+
tr.appendChild(td);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const brandTd = document.createElement("td");
|
|
1609
|
+
brandTd.textContent = p.hasBrandAssets ? "✓" : "—";
|
|
1610
|
+
tr.appendChild(brandTd);
|
|
1611
|
+
|
|
1612
|
+
const actionsCell = document.createElement("td");
|
|
1613
|
+
actionsCell.className = "projects-table__actions";
|
|
1614
|
+
tr.appendChild(actionsCell);
|
|
1153
1615
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
pill.addEventListener("click", () => {
|
|
1163
|
-
if (sessionId) {
|
|
1164
|
-
resumeSession(sessionId);
|
|
1165
|
-
} else {
|
|
1166
|
-
openTheme(name);
|
|
1616
|
+
const openBtn = document.createElement("button");
|
|
1617
|
+
openBtn.type = "button";
|
|
1618
|
+
openBtn.className = "btn btn--sm btn--primary";
|
|
1619
|
+
openBtn.textContent = "Open";
|
|
1620
|
+
openBtn.addEventListener("click", () => {
|
|
1621
|
+
if (typeof isStreaming !== "undefined" && isStreaming) {
|
|
1622
|
+
showError("Cannot switch projects while AI is generating.");
|
|
1623
|
+
return;
|
|
1167
1624
|
}
|
|
1625
|
+
if (p.sessionId) resumeSession(p.sessionId);
|
|
1626
|
+
else openTheme(p.name);
|
|
1627
|
+
});
|
|
1628
|
+
actionsCell.appendChild(openBtn);
|
|
1629
|
+
|
|
1630
|
+
const delBtn = document.createElement("button");
|
|
1631
|
+
delBtn.type = "button";
|
|
1632
|
+
delBtn.className = "btn btn--sm btn--danger";
|
|
1633
|
+
delBtn.textContent = "Delete";
|
|
1634
|
+
delBtn.addEventListener("click", () => {
|
|
1635
|
+
confirmDeleteProject(p);
|
|
1168
1636
|
});
|
|
1169
|
-
|
|
1637
|
+
actionsCell.appendChild(delBtn);
|
|
1638
|
+
|
|
1639
|
+
tbody.appendChild(tr);
|
|
1640
|
+
}
|
|
1641
|
+
table.appendChild(tbody);
|
|
1642
|
+
|
|
1643
|
+
container.innerHTML = "";
|
|
1644
|
+
container.appendChild(toolbar);
|
|
1645
|
+
container.appendChild(table);
|
|
1646
|
+
|
|
1647
|
+
const selectAll = document.getElementById("bulk-select-all");
|
|
1648
|
+
selectAll.addEventListener("change", () => {
|
|
1649
|
+
const cbs = container.querySelectorAll("tbody .projects-table__checkbox");
|
|
1650
|
+
cbs.forEach((c) => {
|
|
1651
|
+
c.checked = selectAll.checked;
|
|
1652
|
+
const name = c.dataset.projectName;
|
|
1653
|
+
const row = c.closest("tr");
|
|
1654
|
+
if (selectAll.checked) _bulkSelected.add(name);
|
|
1655
|
+
else _bulkSelected.delete(name);
|
|
1656
|
+
if (row) row.classList.toggle("projects-table__row--selected", selectAll.checked);
|
|
1657
|
+
});
|
|
1658
|
+
syncBulkToolbar();
|
|
1170
1659
|
});
|
|
1660
|
+
|
|
1661
|
+
document.getElementById("bulk-delete").addEventListener("click", () => bulkDeleteProjects());
|
|
1662
|
+
document.getElementById("bulk-duplicate").addEventListener("click", () => bulkDuplicateProjects());
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function syncBulkToolbar() {
|
|
1666
|
+
const toolbar = document.getElementById("projects-bulk-toolbar");
|
|
1667
|
+
const countEl = document.getElementById("bulk-count");
|
|
1668
|
+
const selectAll = document.getElementById("bulk-select-all");
|
|
1669
|
+
if (!toolbar) return;
|
|
1670
|
+
|
|
1671
|
+
const n = _bulkSelected.size;
|
|
1672
|
+
toolbar.classList.toggle("hidden", n === 0);
|
|
1673
|
+
if (countEl) countEl.textContent = `${n} selected`;
|
|
1674
|
+
|
|
1675
|
+
if (selectAll) {
|
|
1676
|
+
const total = document.querySelectorAll("#continue-projects tbody .projects-table__checkbox").length;
|
|
1677
|
+
selectAll.checked = n > 0 && n === total;
|
|
1678
|
+
selectAll.indeterminate = n > 0 && n < total;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function bulkDeleteProjects() {
|
|
1683
|
+
if (_bulkSelected.size === 0) return;
|
|
1684
|
+
const names = [..._bulkSelected];
|
|
1685
|
+
const projects = _allProjects.filter((p) => names.includes(p.name));
|
|
1686
|
+
|
|
1687
|
+
const overlay = document.createElement("div");
|
|
1688
|
+
overlay.className = "confirm-overlay";
|
|
1689
|
+
overlay.innerHTML = `
|
|
1690
|
+
<div class="confirm-dialog">
|
|
1691
|
+
<div class="confirm-dialog__title">Delete ${projects.length} project${projects.length > 1 ? "s" : ""}?</div>
|
|
1692
|
+
<div class="confirm-dialog__detail">${projects.map((p) => `<strong>${esc(p.name)}</strong>`).join(", ")}</div>
|
|
1693
|
+
<label class="confirm-dialog__check">
|
|
1694
|
+
<input type="checkbox" id="confirm-bulk-delete-files" checked />
|
|
1695
|
+
<span>Also delete local files</span>
|
|
1696
|
+
</label>
|
|
1697
|
+
<p class="confirm-dialog__warn">Deleting local files cannot be undone.</p>
|
|
1698
|
+
<div class="confirm-dialog__actions">
|
|
1699
|
+
<button class="btn btn--secondary" id="confirm-bulk-cancel">Cancel</button>
|
|
1700
|
+
<button class="btn btn--danger" id="confirm-bulk-delete">Delete</button>
|
|
1701
|
+
</div>
|
|
1702
|
+
</div>
|
|
1703
|
+
`;
|
|
1704
|
+
document.body.appendChild(overlay);
|
|
1705
|
+
|
|
1706
|
+
document.getElementById("confirm-bulk-cancel").addEventListener("click", () => overlay.remove());
|
|
1707
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
|
1708
|
+
|
|
1709
|
+
document.getElementById("confirm-bulk-delete").addEventListener("click", async () => {
|
|
1710
|
+
const deleteFiles = document.getElementById("confirm-bulk-delete-files").checked;
|
|
1711
|
+
overlay.remove();
|
|
1712
|
+
|
|
1713
|
+
for (const p of projects) {
|
|
1714
|
+
try {
|
|
1715
|
+
if (p.sessionId) {
|
|
1716
|
+
await fetch("/api/themes", {
|
|
1717
|
+
method: "DELETE",
|
|
1718
|
+
headers: { "Content-Type": "application/json" },
|
|
1719
|
+
body: JSON.stringify({ sessionId: p.sessionId, deleteFiles }),
|
|
1720
|
+
});
|
|
1721
|
+
} else if (deleteFiles) {
|
|
1722
|
+
await fetch("/api/themes/delete-local", {
|
|
1723
|
+
method: "POST",
|
|
1724
|
+
headers: { "Content-Type": "application/json" },
|
|
1725
|
+
body: JSON.stringify({ themeName: p.name }),
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
} catch { /* continue deleting others */ }
|
|
1729
|
+
}
|
|
1730
|
+
_bulkSelected.clear();
|
|
1731
|
+
await initSetup();
|
|
1732
|
+
populateContinuePanel();
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
async function bulkDuplicateProjects() {
|
|
1737
|
+
if (_bulkSelected.size === 0) return;
|
|
1738
|
+
const names = [..._bulkSelected];
|
|
1739
|
+
const projects = _allProjects.filter((p) => names.includes(p.name) && p.sessionId);
|
|
1740
|
+
|
|
1741
|
+
if (projects.length === 0) {
|
|
1742
|
+
showError("Only saved projects can be duplicated.");
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
for (const p of projects) {
|
|
1747
|
+
try {
|
|
1748
|
+
await fetch("/api/themes/duplicate", {
|
|
1749
|
+
method: "POST",
|
|
1750
|
+
headers: { "Content-Type": "application/json" },
|
|
1751
|
+
body: JSON.stringify({ sessionId: p.sessionId }),
|
|
1752
|
+
});
|
|
1753
|
+
} catch { /* continue */ }
|
|
1754
|
+
}
|
|
1755
|
+
_bulkSelected.clear();
|
|
1756
|
+
await initSetup();
|
|
1757
|
+
populateContinuePanel();
|
|
1171
1758
|
}
|
|
1172
1759
|
|
|
1173
1760
|
async function loadDownloadPanel() {
|
|
@@ -1224,7 +1811,7 @@ function initDlAccountSwitch(accounts, activeId) {
|
|
|
1224
1811
|
let html = '<div style="display:flex;flex-direction:column;gap:6px">';
|
|
1225
1812
|
for (const acct of accounts) {
|
|
1226
1813
|
const isActive = acct.portalId === activeId;
|
|
1227
|
-
html += `<button class="btn btn--${isActive ? "primary" : "secondary"} dl-acct-btn" data-portal="${esc(acct.portalId)}" style="text-align:left;padding:6px 12px;font-size:13px">${esc(acct.portalName || acct.portalId)} (${esc(acct.portalId)})${isActive ? "
|
|
1814
|
+
html += `<button class="btn btn--${isActive ? "primary" : "secondary"} dl-acct-btn" data-portal="${esc(acct.portalId)}" style="text-align:left;padding:6px 12px;font-size:13px">${esc(acct.portalName || acct.portalId)} (${esc(acct.portalId)})${isActive ? ' <span class="vs-icon-inline">' + vsIcon("check", {size: "sm"}) + '</span>' : ""}</button>`;
|
|
1228
1815
|
}
|
|
1229
1816
|
html += `<button class="btn btn--secondary dl-acct-btn" data-portal="__new" style="text-align:left;padding:6px 12px;font-size:13px">+ Add another account</button>`;
|
|
1230
1817
|
html += '</div>';
|
|
@@ -1279,33 +1866,59 @@ async function downloadThemeByName() {
|
|
|
1279
1866
|
// Event listeners
|
|
1280
1867
|
// ---------------------------------------------------------------------------
|
|
1281
1868
|
|
|
1282
|
-
//
|
|
1283
|
-
|
|
1284
|
-
|
|
1869
|
+
// Asset-type cards (guided entry — VIB-255). The "From Template" card opens
|
|
1870
|
+
// the existing starter grid; "Import" shows source picker; others reveal a
|
|
1871
|
+
// pre-scoped describe prompt.
|
|
1872
|
+
document.querySelectorAll(".setup__type-card").forEach((card) => {
|
|
1873
|
+
card.addEventListener("click", () => {
|
|
1874
|
+
const action = card.dataset.action;
|
|
1875
|
+
if (action === "starter") {
|
|
1876
|
+
activePanel = null;
|
|
1877
|
+
togglePanel("starter");
|
|
1878
|
+
setTimeout(() => {
|
|
1879
|
+
document.getElementById("panel-starter")?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1880
|
+
}, 60);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
const assetType = card.dataset.assetType;
|
|
1884
|
+
if (assetType === "import") {
|
|
1885
|
+
showImportSources();
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
showScopedPrompt(card);
|
|
1889
|
+
});
|
|
1285
1890
|
});
|
|
1286
1891
|
|
|
1287
|
-
//
|
|
1288
|
-
document.
|
|
1289
|
-
|
|
1290
|
-
activePanel = null;
|
|
1291
|
-
togglePanel(btn.dataset.action);
|
|
1292
|
-
setTimeout(() => {
|
|
1293
|
-
document.getElementById("panel-starter")?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1294
|
-
}, 60);
|
|
1295
|
-
});
|
|
1892
|
+
// "Back" link inside the scoped describe prompt restores the asset-type cards.
|
|
1893
|
+
document.getElementById("setup-prompt-back")?.addEventListener("click", () => {
|
|
1894
|
+
showAssetTypeCards();
|
|
1296
1895
|
});
|
|
1297
1896
|
|
|
1298
|
-
//
|
|
1299
|
-
function
|
|
1300
|
-
const
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
toggle.classList.toggle("setup__more-toggle--open", willExpand);
|
|
1897
|
+
// Import source picker
|
|
1898
|
+
function showImportSources() {
|
|
1899
|
+
const cards = document.getElementById("setup-type-cards");
|
|
1900
|
+
const importPanel = document.getElementById("setup-import-sources");
|
|
1901
|
+
const question = document.getElementById("setup-question");
|
|
1902
|
+
if (cards) cards.classList.add("hidden");
|
|
1903
|
+
if (question) question.classList.add("hidden");
|
|
1904
|
+
if (importPanel) importPanel.classList.remove("hidden");
|
|
1307
1905
|
}
|
|
1308
|
-
|
|
1906
|
+
|
|
1907
|
+
document.getElementById("setup-import-back")?.addEventListener("click", () => {
|
|
1908
|
+
const importPanel = document.getElementById("setup-import-sources");
|
|
1909
|
+
if (importPanel) importPanel.classList.add("hidden");
|
|
1910
|
+
showAssetTypeCards();
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
document.querySelectorAll(".setup__import-btn").forEach((btn) => {
|
|
1914
|
+
btn.addEventListener("click", () => {
|
|
1915
|
+
const action = btn.dataset.action;
|
|
1916
|
+
const importPanel = document.getElementById("setup-import-sources");
|
|
1917
|
+
if (importPanel) importPanel.classList.add("hidden");
|
|
1918
|
+
showAssetTypeCards();
|
|
1919
|
+
togglePanel(action);
|
|
1920
|
+
});
|
|
1921
|
+
});
|
|
1309
1922
|
|
|
1310
1923
|
// "View all" link in recent projects → open the full Continue panel
|
|
1311
1924
|
document.getElementById("setup-recent-all")?.addEventListener("click", () => {
|
|
@@ -1739,8 +2352,7 @@ function handleRoute() {
|
|
|
1739
2352
|
if (appTemplateMatch) {
|
|
1740
2353
|
const themeName = decodeURIComponent(appTemplateMatch[1]);
|
|
1741
2354
|
const templateId = decodeURIComponent(appTemplateMatch[2]);
|
|
1742
|
-
|
|
1743
|
-
if (currentAppTheme === themeName && !appScreen.classList.contains("hidden")) return;
|
|
2355
|
+
if (currentAppTheme === themeName && appBody.dataset.mode === "editor") return;
|
|
1744
2356
|
// Open theme then activate template
|
|
1745
2357
|
openTheme(themeName).then(() => {
|
|
1746
2358
|
if (typeof showChat === "function") {
|
|
@@ -1754,24 +2366,22 @@ function handleRoute() {
|
|
|
1754
2366
|
const appMatch = hash.match(/^#\/app\/([^/]+)$/);
|
|
1755
2367
|
if (appMatch) {
|
|
1756
2368
|
const themeName = decodeURIComponent(appMatch[1]);
|
|
1757
|
-
if (currentAppTheme === themeName &&
|
|
2369
|
+
if (currentAppTheme === themeName && appBody.dataset.mode === "editor") return;
|
|
1758
2370
|
openTheme(themeName);
|
|
1759
2371
|
return;
|
|
1760
2372
|
}
|
|
1761
2373
|
|
|
1762
|
-
// #/dashboard/{themeName} → show
|
|
2374
|
+
// #/dashboard/{themeName} → show editor for theme
|
|
1763
2375
|
const dashMatch = hash.match(/^#\/dashboard\/(.+)$/);
|
|
1764
2376
|
if (dashMatch) {
|
|
1765
2377
|
const themeName = decodeURIComponent(dashMatch[1]);
|
|
1766
|
-
|
|
1767
|
-
if (currentDashboardTheme === themeName && dashEl && !dashEl.classList.contains("hidden")) return;
|
|
2378
|
+
if (currentDashboardTheme === themeName && appBody.dataset.mode === "editor") return;
|
|
1768
2379
|
openTheme(themeName);
|
|
1769
2380
|
return;
|
|
1770
2381
|
}
|
|
1771
2382
|
|
|
1772
2383
|
// Default: show setup
|
|
1773
|
-
|
|
1774
|
-
if (!appScreen.classList.contains("hidden") || (dashEl && !dashEl.classList.contains("hidden"))) {
|
|
2384
|
+
if (appBody.dataset.mode === "editor") {
|
|
1775
2385
|
showSetup();
|
|
1776
2386
|
}
|
|
1777
2387
|
}
|