vibespot 0.4.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/LICENSE +33 -0
- package/README.md +118 -0
- package/assets/content-guide.md +445 -0
- package/assets/conversion-guide.md +693 -0
- package/assets/design-guide.md +380 -0
- package/assets/hubspot-rules.md +560 -0
- package/assets/page-types.md +116 -0
- package/bin/vibespot.mjs +11 -0
- package/dist/index.js +6552 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
- package/ui/chat.js +803 -0
- package/ui/dashboard.js +383 -0
- package/ui/dialog.js +117 -0
- package/ui/field-editor.js +292 -0
- package/ui/index.html +393 -0
- package/ui/preview.js +132 -0
- package/ui/settings.js +927 -0
- package/ui/setup.js +830 -0
- package/ui/styles.css +2552 -0
- package/ui/upload-panel.js +554 -0
package/ui/setup.js
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup screen — onboarding flow in the browser.
|
|
3
|
+
* Handles theme creation, fetching, opening, and session resume.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const setupScreen = document.getElementById("setup-screen");
|
|
7
|
+
const appScreen = document.getElementById("app-screen");
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Load setup info on page load
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const ENGINE_DISPLAY_NAMES = {
|
|
14
|
+
"claude-code": "Claude Code",
|
|
15
|
+
"anthropic-api": "Anthropic API",
|
|
16
|
+
"openai-api": "OpenAI API",
|
|
17
|
+
"gemini-cli": "Gemini CLI",
|
|
18
|
+
"gemini-api": "Gemini API",
|
|
19
|
+
"codex-cli": "Codex CLI",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function initSetup() {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch("/api/setup");
|
|
25
|
+
const info = await res.json();
|
|
26
|
+
|
|
27
|
+
// Populate sidebar with all projects (sessions + local themes)
|
|
28
|
+
populateSidebar(info);
|
|
29
|
+
|
|
30
|
+
// Auto-select engine if available but not yet chosen
|
|
31
|
+
if (info.availableEngines && info.availableEngines.length > 0 && !info.activeEngine) {
|
|
32
|
+
const engine = info.availableEngines[0];
|
|
33
|
+
await fetch("/api/settings/engine", {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({ engine }),
|
|
37
|
+
});
|
|
38
|
+
info.activeEngine = engine;
|
|
39
|
+
info.aiAvailable = true;
|
|
40
|
+
// Update statusbar
|
|
41
|
+
const statusEngine = document.getElementById("status-engine");
|
|
42
|
+
if (statusEngine) statusEngine.textContent = ENGINE_DISPLAY_NAMES[engine] || engine;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Show environment alerts
|
|
46
|
+
const alerts = document.getElementById("setup-alerts");
|
|
47
|
+
alerts.innerHTML = "";
|
|
48
|
+
if (!info.aiAvailable) {
|
|
49
|
+
alerts.innerHTML += `
|
|
50
|
+
<div class="setup__alert setup__alert--warn">
|
|
51
|
+
<div>No AI engine configured. Paste an API key to get started:</div>
|
|
52
|
+
<div class="setup__alert-key-row">
|
|
53
|
+
<input type="password" class="setup__alert-key-input" id="alert-api-key" placeholder="sk-ant-api03-..." />
|
|
54
|
+
<button class="btn btn--primary btn--sm" id="alert-api-save">Save</button>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="setup__alert-alt">or <a href="#" id="alert-setup-link">open settings</a> for more options</div>
|
|
57
|
+
</div>`;
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
const link = document.getElementById("alert-setup-link");
|
|
60
|
+
if (link) link.addEventListener("click", (e) => { e.preventDefault(); openSettings(); });
|
|
61
|
+
const saveBtn = document.getElementById("alert-api-save");
|
|
62
|
+
const keyInput = document.getElementById("alert-api-key");
|
|
63
|
+
if (saveBtn && keyInput) {
|
|
64
|
+
const doSave = () => saveAlertApiKey(keyInput.value.trim());
|
|
65
|
+
saveBtn.addEventListener("click", doSave);
|
|
66
|
+
keyInput.addEventListener("keydown", (e) => { if (e.key === "Enter") doSave(); });
|
|
67
|
+
}
|
|
68
|
+
}, 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if we should show the walkthrough (fresh environment)
|
|
72
|
+
// Add ?walkthrough to URL to force-show it for testing
|
|
73
|
+
if (new URLSearchParams(location.search).has("walkthrough") ||
|
|
74
|
+
(!info.aiAvailable && info.sessions.length === 0 && info.localThemes.length === 0)) {
|
|
75
|
+
showWalkthrough();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Show fetch section if hs is installed
|
|
80
|
+
if (info.hsInstalled) {
|
|
81
|
+
document.getElementById("section-fetch").classList.remove("hidden");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
} catch (err) {
|
|
85
|
+
showError("Could not connect to server. Is vibeSpot running?");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function saveAlertApiKey(key) {
|
|
90
|
+
if (!key) return;
|
|
91
|
+
// Detect provider from key prefix
|
|
92
|
+
let provider;
|
|
93
|
+
if (key.startsWith("sk-ant-")) provider = "anthropic";
|
|
94
|
+
else if (key.startsWith("sk-")) provider = "openai";
|
|
95
|
+
else if (key.startsWith("AIza")) provider = "gemini";
|
|
96
|
+
else provider = "anthropic"; // default guess
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch("/api/settings/apikey", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ provider, apiKey: key }),
|
|
103
|
+
});
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
if (data.error) { await vibeAlert(data.error, "Error"); return; }
|
|
106
|
+
|
|
107
|
+
// Update statusbar if engine was auto-selected
|
|
108
|
+
if (data.autoSelectedEngine) {
|
|
109
|
+
const statusEngine = document.getElementById("status-engine");
|
|
110
|
+
if (statusEngine) statusEngine.textContent = ENGINE_DISPLAY_NAMES[data.autoSelectedEngine] || data.autoSelectedEngine;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Re-init to refresh everything
|
|
114
|
+
initSetup();
|
|
115
|
+
} catch (err) {
|
|
116
|
+
await vibeAlert("Failed to save API key: " + err.message, "Error");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Sidebar — project list (like Lovable)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function populateSidebar(info) {
|
|
125
|
+
const list = document.getElementById("sidebar-project-list");
|
|
126
|
+
const countEl = document.getElementById("sidebar-project-count");
|
|
127
|
+
list.innerHTML = "";
|
|
128
|
+
|
|
129
|
+
// Build a combined, deduplicated list of projects
|
|
130
|
+
const projects = [];
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
|
|
133
|
+
// Add sessions first (most recent)
|
|
134
|
+
for (const s of info.sessions || []) {
|
|
135
|
+
if (!seen.has(s.themeName)) {
|
|
136
|
+
seen.add(s.themeName);
|
|
137
|
+
projects.push({
|
|
138
|
+
name: s.themeName,
|
|
139
|
+
type: "session",
|
|
140
|
+
sessionId: s.id,
|
|
141
|
+
updatedAt: s.updatedAt,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Add local themes that aren't already sessions
|
|
147
|
+
for (const name of info.localThemes || []) {
|
|
148
|
+
if (!seen.has(name)) {
|
|
149
|
+
seen.add(name);
|
|
150
|
+
projects.push({ name, type: "local", sessionId: null, updatedAt: null });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
countEl.textContent = projects.length;
|
|
155
|
+
|
|
156
|
+
if (projects.length === 0) {
|
|
157
|
+
list.innerHTML = `<div class="setup-sidebar__empty">No projects yet.<br>Create one to get started.</div>`;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const p of projects) {
|
|
162
|
+
const item = document.createElement("div");
|
|
163
|
+
item.className = "setup-sidebar__item";
|
|
164
|
+
const initial = p.name.charAt(0).toUpperCase();
|
|
165
|
+
const meta = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
|
|
166
|
+
|
|
167
|
+
const openBtn = document.createElement("button");
|
|
168
|
+
openBtn.className = "setup-sidebar__item-open";
|
|
169
|
+
openBtn.innerHTML = `
|
|
170
|
+
<div class="setup-sidebar__item-icon">${esc(initial)}</div>
|
|
171
|
+
<div class="setup-sidebar__item-info">
|
|
172
|
+
<span class="setup-sidebar__item-name">${esc(p.name)}</span>
|
|
173
|
+
<span class="setup-sidebar__item-meta">${meta}</span>
|
|
174
|
+
</div>
|
|
175
|
+
`;
|
|
176
|
+
openBtn.addEventListener("click", () => {
|
|
177
|
+
if (p.sessionId) {
|
|
178
|
+
resumeSession(p.sessionId);
|
|
179
|
+
} else {
|
|
180
|
+
openTheme(p.name);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
item.appendChild(openBtn);
|
|
184
|
+
|
|
185
|
+
const delBtn = document.createElement("button");
|
|
186
|
+
delBtn.className = "setup-sidebar__item-delete";
|
|
187
|
+
delBtn.innerHTML = "×";
|
|
188
|
+
delBtn.title = "Delete project";
|
|
189
|
+
delBtn.addEventListener("click", (e) => {
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
confirmDeleteProject(p);
|
|
192
|
+
});
|
|
193
|
+
item.appendChild(delBtn);
|
|
194
|
+
|
|
195
|
+
list.appendChild(item);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Delete project confirmation
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
function confirmDeleteProject(project) {
|
|
204
|
+
// Build a custom confirm dialog
|
|
205
|
+
const overlay = document.createElement("div");
|
|
206
|
+
overlay.className = "confirm-overlay";
|
|
207
|
+
overlay.innerHTML = `
|
|
208
|
+
<div class="confirm-dialog">
|
|
209
|
+
<div class="confirm-dialog__title">Delete "${esc(project.name)}"?</div>
|
|
210
|
+
<label class="confirm-dialog__check">
|
|
211
|
+
<input type="checkbox" id="confirm-delete-files" checked />
|
|
212
|
+
<span>Also delete local files</span>
|
|
213
|
+
</label>
|
|
214
|
+
<p class="confirm-dialog__warn">Deleting local files cannot be undone.</p>
|
|
215
|
+
<div class="confirm-dialog__actions">
|
|
216
|
+
<button class="btn btn--secondary" id="confirm-cancel">Cancel</button>
|
|
217
|
+
<button class="btn btn--danger" id="confirm-delete">Delete</button>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
`;
|
|
221
|
+
document.body.appendChild(overlay);
|
|
222
|
+
|
|
223
|
+
document.getElementById("confirm-cancel").addEventListener("click", () => overlay.remove());
|
|
224
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
|
225
|
+
|
|
226
|
+
document.getElementById("confirm-delete").addEventListener("click", async () => {
|
|
227
|
+
const deleteFiles = document.getElementById("confirm-delete-files").checked;
|
|
228
|
+
overlay.remove();
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
if (project.sessionId) {
|
|
232
|
+
await fetch("/api/themes", {
|
|
233
|
+
method: "DELETE",
|
|
234
|
+
headers: { "Content-Type": "application/json" },
|
|
235
|
+
body: JSON.stringify({ sessionId: project.sessionId, deleteFiles }),
|
|
236
|
+
});
|
|
237
|
+
} else if (deleteFiles) {
|
|
238
|
+
// Local-only theme (no session) — delete via dedicated endpoint
|
|
239
|
+
await fetch("/api/themes/delete-local", {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: { "Content-Type": "application/json" },
|
|
242
|
+
body: JSON.stringify({ themeName: project.name }),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Refresh the sidebar
|
|
246
|
+
initSetup();
|
|
247
|
+
} catch {
|
|
248
|
+
showError("Failed to delete project.");
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Guided walkthrough (first-run experience)
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
async function showWalkthrough() {
|
|
258
|
+
const walkthrough = document.getElementById("walkthrough");
|
|
259
|
+
const options = document.getElementById("setup-options");
|
|
260
|
+
|
|
261
|
+
walkthrough.classList.remove("hidden");
|
|
262
|
+
options.classList.add("hidden");
|
|
263
|
+
|
|
264
|
+
// Fetch full environment status for CLI tool details
|
|
265
|
+
let envData;
|
|
266
|
+
try {
|
|
267
|
+
const res = await fetch("/api/settings/status");
|
|
268
|
+
envData = await res.json();
|
|
269
|
+
} catch {
|
|
270
|
+
showError("Could not load environment status.");
|
|
271
|
+
walkthrough.classList.add("hidden");
|
|
272
|
+
options.classList.remove("hidden");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const env = envData.environment;
|
|
277
|
+
const progress = document.getElementById("walkthrough-progress");
|
|
278
|
+
const content = document.getElementById("walkthrough-content");
|
|
279
|
+
progress.innerHTML = "";
|
|
280
|
+
|
|
281
|
+
content.innerHTML = `
|
|
282
|
+
<div class="walkthrough__step-title">Set up your AI engine</div>
|
|
283
|
+
<div class="walkthrough__step-desc">vibeSpot needs an AI engine to build HubSpot pages. The fastest way is to paste an API key.</div>
|
|
284
|
+
|
|
285
|
+
<div class="walkthrough__card walkthrough__card--highlight">
|
|
286
|
+
<div class="walkthrough__card-title">Paste an API key <span class="walkthrough__badge">Easiest</span></div>
|
|
287
|
+
<div class="walkthrough__key-row">
|
|
288
|
+
<label>Anthropic</label>
|
|
289
|
+
<input type="password" class="walkthrough__key-input" id="wt-key-anthropic" placeholder="sk-ant-api03-..." />
|
|
290
|
+
<button class="btn btn--primary btn--sm" data-provider="anthropic">Save</button>
|
|
291
|
+
<a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener" class="walkthrough__key-link">Get key</a>
|
|
292
|
+
</div>
|
|
293
|
+
<div class="walkthrough__key-row">
|
|
294
|
+
<label>OpenAI</label>
|
|
295
|
+
<input type="password" class="walkthrough__key-input" id="wt-key-openai" placeholder="sk-..." />
|
|
296
|
+
<button class="btn btn--primary btn--sm" data-provider="openai">Save</button>
|
|
297
|
+
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener" class="walkthrough__key-link">Get key</a>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="walkthrough__key-row">
|
|
300
|
+
<label>Google AI</label>
|
|
301
|
+
<input type="password" class="walkthrough__key-input" id="wt-key-gemini" placeholder="AIza..." />
|
|
302
|
+
<button class="btn btn--primary btn--sm" data-provider="gemini">Save</button>
|
|
303
|
+
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener" class="walkthrough__key-link">Get key</a>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="walkthrough__card">
|
|
308
|
+
<div class="walkthrough__card-title">Or use a CLI tool</div>
|
|
309
|
+
<div class="walkthrough__tool-list">
|
|
310
|
+
${cliToolRow("Claude Code", "claude-code", env.tools.claudeCode)}
|
|
311
|
+
${cliToolRow("Gemini CLI", "gemini-cli", env.tools.geminiCli)}
|
|
312
|
+
${cliToolRow("Codex CLI", "codex-cli", env.tools.codexCli)}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div class="walkthrough__actions">
|
|
317
|
+
<button class="btn btn--secondary" id="wt-skip">Skip for now</button>
|
|
318
|
+
</div>
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
// API key save handlers
|
|
322
|
+
content.querySelectorAll(".walkthrough__card--highlight button[data-provider]").forEach((btn) => {
|
|
323
|
+
const provider = btn.dataset.provider;
|
|
324
|
+
const input = document.getElementById("wt-key-" + provider);
|
|
325
|
+
const doSave = async () => {
|
|
326
|
+
const key = input.value.trim();
|
|
327
|
+
if (!key) return;
|
|
328
|
+
btn.disabled = true;
|
|
329
|
+
btn.textContent = "...";
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch("/api/settings/apikey", {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "Content-Type": "application/json" },
|
|
334
|
+
body: JSON.stringify({ provider, apiKey: key }),
|
|
335
|
+
});
|
|
336
|
+
const data = await res.json();
|
|
337
|
+
if (data.error) { await vibeAlert(data.error, "Error"); btn.disabled = false; btn.textContent = "Save"; return; }
|
|
338
|
+
if (data.autoSelectedEngine) {
|
|
339
|
+
const statusEngine = document.getElementById("status-engine");
|
|
340
|
+
if (statusEngine) statusEngine.textContent = ENGINE_DISPLAY_NAMES[data.autoSelectedEngine] || data.autoSelectedEngine;
|
|
341
|
+
}
|
|
342
|
+
clearWalkthroughParam();
|
|
343
|
+
walkthrough.classList.add("hidden");
|
|
344
|
+
options.classList.remove("hidden");
|
|
345
|
+
initSetup();
|
|
346
|
+
} catch (err) {
|
|
347
|
+
await vibeAlert("Failed to save: " + err.message, "Error");
|
|
348
|
+
btn.disabled = false;
|
|
349
|
+
btn.textContent = "Save";
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
btn.addEventListener("click", doSave);
|
|
353
|
+
input.addEventListener("keydown", (e) => { if (e.key === "Enter") doSave(); });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// CLI tool action handlers
|
|
357
|
+
content.querySelectorAll("[data-cli-action]").forEach((btn) => {
|
|
358
|
+
btn.addEventListener("click", () => handleCliAction(btn.dataset.cliEngine, btn.dataset.cliAction, btn));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Skip
|
|
362
|
+
document.getElementById("wt-skip").addEventListener("click", () => {
|
|
363
|
+
clearWalkthroughParam();
|
|
364
|
+
walkthrough.classList.add("hidden");
|
|
365
|
+
options.classList.remove("hidden");
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Strip ?walkthrough from URL so re-init doesn't re-show it */
|
|
370
|
+
function clearWalkthroughParam() {
|
|
371
|
+
const url = new URL(location.href);
|
|
372
|
+
if (url.searchParams.has("walkthrough")) {
|
|
373
|
+
url.searchParams.delete("walkthrough");
|
|
374
|
+
history.replaceState(null, "", url.pathname + url.search + url.hash);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function cliToolRow(name, engineId, toolInfo) {
|
|
379
|
+
let statusHtml, actionHtml;
|
|
380
|
+
if (toolInfo.found && toolInfo.authenticated) {
|
|
381
|
+
statusHtml = `<span class="walkthrough__tool-status walkthrough__tool-status--ok">Ready</span>`;
|
|
382
|
+
actionHtml = `<button class="btn btn--sm btn--primary" data-cli-action="select" data-cli-engine="${engineId}">Use</button>`;
|
|
383
|
+
} else if (toolInfo.found && !toolInfo.authenticated) {
|
|
384
|
+
statusHtml = `<span class="walkthrough__tool-status walkthrough__tool-status--missing">Not signed in</span>`;
|
|
385
|
+
actionHtml = `<button class="btn btn--sm btn--secondary" data-cli-action="auth" data-cli-engine="${engineId}">Sign in</button>`;
|
|
386
|
+
} else {
|
|
387
|
+
statusHtml = `<span class="walkthrough__tool-status walkthrough__tool-status--missing">Not installed</span>`;
|
|
388
|
+
actionHtml = `<button class="btn btn--sm btn--secondary" data-cli-action="install" data-cli-engine="${engineId}">Install</button>`;
|
|
389
|
+
}
|
|
390
|
+
return `<div class="walkthrough__tool-item">
|
|
391
|
+
<span class="settings__dot settings__dot--${toolInfo.found && toolInfo.authenticated ? "success" : toolInfo.found ? "warn" : "muted"}"></span>
|
|
392
|
+
<span class="walkthrough__tool-name">${esc(name)}</span>
|
|
393
|
+
${statusHtml}
|
|
394
|
+
${actionHtml}
|
|
395
|
+
</div>`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function handleCliAction(engineId, action, btn) {
|
|
399
|
+
const toolMap = { "claude-code": "claude", "gemini-cli": "gemini", "codex-cli": "codex" };
|
|
400
|
+
const tool = toolMap[engineId];
|
|
401
|
+
if (!tool) return;
|
|
402
|
+
|
|
403
|
+
if (action === "select") {
|
|
404
|
+
// Already installed + authed, just select
|
|
405
|
+
await fetch("/api/settings/engine", {
|
|
406
|
+
method: "POST",
|
|
407
|
+
headers: { "Content-Type": "application/json" },
|
|
408
|
+
body: JSON.stringify({ engine: engineId }),
|
|
409
|
+
});
|
|
410
|
+
const statusEngine = document.getElementById("status-engine");
|
|
411
|
+
if (statusEngine) statusEngine.textContent = ENGINE_DISPLAY_NAMES[engineId] || engineId;
|
|
412
|
+
clearWalkthroughParam();
|
|
413
|
+
document.getElementById("walkthrough").classList.add("hidden");
|
|
414
|
+
document.getElementById("setup-options").classList.remove("hidden");
|
|
415
|
+
initSetup();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const endpoint = action === "install" ? "/api/settings/install" : "/api/settings/cli-auth";
|
|
420
|
+
btn.disabled = true;
|
|
421
|
+
const origText = btn.textContent;
|
|
422
|
+
btn.innerHTML = '<span class="upload-spinner"></span>';
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const res = await fetch(endpoint, {
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers: { "Content-Type": "application/json" },
|
|
428
|
+
body: JSON.stringify({ tool }),
|
|
429
|
+
});
|
|
430
|
+
const data = await res.json();
|
|
431
|
+
if (data.jobId) {
|
|
432
|
+
// Poll until complete
|
|
433
|
+
await pollJob(data.jobId);
|
|
434
|
+
}
|
|
435
|
+
// Refresh walkthrough to show updated status
|
|
436
|
+
showWalkthrough();
|
|
437
|
+
} catch {
|
|
438
|
+
btn.disabled = false;
|
|
439
|
+
btn.textContent = origText;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function pollJob(jobId) {
|
|
444
|
+
for (let i = 0; i < 60; i++) {
|
|
445
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
446
|
+
try {
|
|
447
|
+
const res = await fetch("/api/settings/job/" + jobId);
|
|
448
|
+
const data = await res.json();
|
|
449
|
+
if (data.status === "completed" || data.status === "failed") return;
|
|
450
|
+
} catch { return; }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Actions
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
async function createTheme() {
|
|
459
|
+
const name = document.getElementById("new-theme-name").value.trim();
|
|
460
|
+
if (!name) {
|
|
461
|
+
showError("Please enter a name for your theme.");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
showLoading("Creating theme...");
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const res = await fetch("/api/setup/create", {
|
|
469
|
+
method: "POST",
|
|
470
|
+
headers: { "Content-Type": "application/json" },
|
|
471
|
+
body: JSON.stringify({ name }),
|
|
472
|
+
});
|
|
473
|
+
const data = await res.json();
|
|
474
|
+
|
|
475
|
+
if (data.error) {
|
|
476
|
+
showError(data.error);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
showApp(data.themeName);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
showError("Failed to create theme: " + err.message);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function fetchTheme() {
|
|
487
|
+
const name = document.getElementById("fetch-theme-name").value.trim();
|
|
488
|
+
if (!name) {
|
|
489
|
+
showError("Please enter the theme name from your HubSpot account.");
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
showLoading("Fetching theme from HubSpot...");
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const res = await fetch("/api/setup/fetch", {
|
|
497
|
+
method: "POST",
|
|
498
|
+
headers: { "Content-Type": "application/json" },
|
|
499
|
+
body: JSON.stringify({ name }),
|
|
500
|
+
});
|
|
501
|
+
const data = await res.json();
|
|
502
|
+
|
|
503
|
+
if (data.error) {
|
|
504
|
+
showError(data.error);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
showApp(data.themeName);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
showError("Failed to fetch theme: " + err.message);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function openTheme(pathOrName) {
|
|
515
|
+
showLoading("Opening theme...");
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
const res = await fetch("/api/setup/open", {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: { "Content-Type": "application/json" },
|
|
521
|
+
body: JSON.stringify({ path: pathOrName }),
|
|
522
|
+
});
|
|
523
|
+
const data = await res.json();
|
|
524
|
+
|
|
525
|
+
if (data.error) {
|
|
526
|
+
showError(data.error);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
showApp(data.themeName);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
showError("Failed to open theme: " + err.message);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function resumeSession(sessionId) {
|
|
537
|
+
showLoading("Resuming session...");
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const res = await fetch("/api/setup/resume", {
|
|
541
|
+
method: "POST",
|
|
542
|
+
headers: { "Content-Type": "application/json" },
|
|
543
|
+
body: JSON.stringify({ sessionId }),
|
|
544
|
+
});
|
|
545
|
+
const data = await res.json();
|
|
546
|
+
|
|
547
|
+
if (data.error) {
|
|
548
|
+
showError(data.error);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
showApp(data.themeName);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
showError("Failed to resume session: " + err.message);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// API key management moved to settings panel (settings.js)
|
|
559
|
+
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
// UI transitions
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
let currentAppTheme = "";
|
|
565
|
+
|
|
566
|
+
function showApp(themeName) {
|
|
567
|
+
// Route through dashboard instead of going directly to chat
|
|
568
|
+
if (typeof showDashboard === "function") {
|
|
569
|
+
currentAppTheme = themeName;
|
|
570
|
+
showDashboard(themeName);
|
|
571
|
+
} else {
|
|
572
|
+
// Fallback if dashboard.js not loaded
|
|
573
|
+
showAppDirect(themeName);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Direct app view — shows chat screen without dashboard.
|
|
579
|
+
* Used as fallback or when navigating from dashboard to a specific template.
|
|
580
|
+
*/
|
|
581
|
+
function showAppDirect(themeName) {
|
|
582
|
+
setupScreen.classList.add("hidden");
|
|
583
|
+
document.getElementById("setup-topbar").classList.add("hidden");
|
|
584
|
+
if (typeof hideDashboard === "function") hideDashboard();
|
|
585
|
+
appScreen.classList.remove("hidden");
|
|
586
|
+
document.getElementById("theme-name").textContent = themeName;
|
|
587
|
+
|
|
588
|
+
const urlEl = document.getElementById("browser-url");
|
|
589
|
+
if (urlEl) urlEl.textContent = themeName + ".vibespot.app";
|
|
590
|
+
|
|
591
|
+
currentAppTheme = themeName;
|
|
592
|
+
const target = "#/app/" + encodeURIComponent(themeName);
|
|
593
|
+
if (location.hash !== target) {
|
|
594
|
+
history.pushState(null, "", target);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (typeof connectWebSocket === "function") {
|
|
598
|
+
connectWebSocket();
|
|
599
|
+
}
|
|
600
|
+
if (typeof refreshPreview === "function") {
|
|
601
|
+
refreshPreview();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function showSetup() {
|
|
606
|
+
appScreen.classList.add("hidden");
|
|
607
|
+
if (typeof hideDashboard === "function") hideDashboard();
|
|
608
|
+
setupScreen.classList.remove("hidden");
|
|
609
|
+
document.getElementById("setup-topbar").classList.remove("hidden");
|
|
610
|
+
currentAppTheme = "";
|
|
611
|
+
|
|
612
|
+
hideLoading();
|
|
613
|
+
|
|
614
|
+
if (location.hash && location.hash !== "#/") {
|
|
615
|
+
history.pushState(null, "", "#/");
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
initSetup();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Logo click → go back to dashboard (from chat) or setup (from dashboard)
|
|
622
|
+
document.querySelectorAll(".topbar__brand").forEach((el) => {
|
|
623
|
+
el.style.cursor = "pointer";
|
|
624
|
+
el.addEventListener("click", () => {
|
|
625
|
+
const dashEl = document.getElementById("dashboard-screen");
|
|
626
|
+
// From chat → go to dashboard
|
|
627
|
+
if (!appScreen.classList.contains("hidden") && currentAppTheme) {
|
|
628
|
+
if (typeof showDashboard === "function") {
|
|
629
|
+
appScreen.classList.add("hidden");
|
|
630
|
+
showDashboard(currentAppTheme);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// From dashboard → go to setup
|
|
635
|
+
if (dashEl && !dashEl.classList.contains("hidden")) {
|
|
636
|
+
showSetup();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
// Fallback
|
|
640
|
+
if (!appScreen.classList.contains("hidden")) {
|
|
641
|
+
showSetup();
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
function showLoading(text) {
|
|
647
|
+
hideError();
|
|
648
|
+
document.getElementById("setup-options").classList.add("hidden");
|
|
649
|
+
document.getElementById("setup-loading").classList.remove("hidden");
|
|
650
|
+
document.getElementById("setup-loading-text").textContent = text;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function hideLoading() {
|
|
654
|
+
document.getElementById("setup-options").classList.remove("hidden");
|
|
655
|
+
document.getElementById("setup-loading").classList.add("hidden");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function showError(message) {
|
|
659
|
+
hideLoading();
|
|
660
|
+
const el = document.getElementById("setup-error");
|
|
661
|
+
el.textContent = message;
|
|
662
|
+
el.classList.remove("hidden");
|
|
663
|
+
setTimeout(() => el.classList.add("hidden"), 8000);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function hideError() {
|
|
667
|
+
document.getElementById("setup-error").classList.add("hidden");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// Event listeners
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
document.getElementById("btn-create-theme").addEventListener("click", createTheme);
|
|
675
|
+
document.getElementById("new-theme-name").addEventListener("keydown", (e) => {
|
|
676
|
+
if (e.key === "Enter") { e.preventDefault(); createTheme(); }
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
document.getElementById("btn-fetch-theme").addEventListener("click", fetchTheme);
|
|
680
|
+
document.getElementById("fetch-theme-name").addEventListener("keydown", (e) => {
|
|
681
|
+
if (e.key === "Enter") { e.preventDefault(); fetchTheme(); }
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
document.getElementById("btn-open-theme").addEventListener("click", () => {
|
|
685
|
+
openTheme(document.getElementById("open-theme-path").value.trim());
|
|
686
|
+
});
|
|
687
|
+
document.getElementById("open-theme-path").addEventListener("keydown", (e) => {
|
|
688
|
+
if (e.key === "Enter") { e.preventDefault(); document.getElementById("btn-open-theme").click(); }
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Import from GitHub (on setup screen)
|
|
692
|
+
document.getElementById("import-btn").addEventListener("click", async () => {
|
|
693
|
+
const urlInput = document.getElementById("import-url");
|
|
694
|
+
const url = urlInput.value.trim();
|
|
695
|
+
if (!url) return;
|
|
696
|
+
|
|
697
|
+
// Extract repo name to use as theme name
|
|
698
|
+
const repoMatch = url.match(/github\.com\/[\w.-]+\/([\w.-]+)/);
|
|
699
|
+
const themeName = repoMatch ? repoMatch[1].replace(/\.git$/, "") : "imported-project";
|
|
700
|
+
|
|
701
|
+
showLoading(`Importing ${themeName}...`);
|
|
702
|
+
urlInput.disabled = true;
|
|
703
|
+
document.getElementById("import-btn").disabled = true;
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
// First create the theme
|
|
707
|
+
const setupRes = await fetch("/api/setup/create", {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: { "Content-Type": "application/json" },
|
|
710
|
+
body: JSON.stringify({ themeName }),
|
|
711
|
+
});
|
|
712
|
+
const setupData = await setupRes.json();
|
|
713
|
+
if (setupData.error) {
|
|
714
|
+
showError(`Failed to create theme: ${setupData.error}`);
|
|
715
|
+
urlInput.disabled = false;
|
|
716
|
+
document.getElementById("import-btn").disabled = false;
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Now import
|
|
721
|
+
const importRes = await fetch("/api/import", {
|
|
722
|
+
method: "POST",
|
|
723
|
+
headers: { "Content-Type": "application/json" },
|
|
724
|
+
body: JSON.stringify({ url }),
|
|
725
|
+
});
|
|
726
|
+
const importData = await importRes.json();
|
|
727
|
+
if (importData.error) {
|
|
728
|
+
showError(`Import failed: ${importData.error}`);
|
|
729
|
+
urlInput.disabled = false;
|
|
730
|
+
document.getElementById("import-btn").disabled = false;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Show the app and send conversion prompt
|
|
735
|
+
showApp(themeName);
|
|
736
|
+
if (typeof sendMessage === "function" && importData.conversionPrompt) {
|
|
737
|
+
sendMessage(importData.conversionPrompt);
|
|
738
|
+
}
|
|
739
|
+
} catch (err) {
|
|
740
|
+
showError(`Import failed: ${err.message}`);
|
|
741
|
+
urlInput.disabled = false;
|
|
742
|
+
document.getElementById("import-btn").disabled = false;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
document.getElementById("import-url").addEventListener("keydown", (e) => {
|
|
746
|
+
if (e.key === "Enter") { e.preventDefault(); document.getElementById("import-btn").click(); }
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// API key is now handled in the settings panel (settings.js)
|
|
750
|
+
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
// Helpers
|
|
753
|
+
// ---------------------------------------------------------------------------
|
|
754
|
+
|
|
755
|
+
function esc(str) {
|
|
756
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function timeAgo(timestamp) {
|
|
760
|
+
const diff = Date.now() - timestamp;
|
|
761
|
+
const mins = Math.floor(diff / 60000);
|
|
762
|
+
if (mins < 1) return "just now";
|
|
763
|
+
if (mins < 60) return mins + "m ago";
|
|
764
|
+
const hours = Math.floor(mins / 60);
|
|
765
|
+
if (hours < 24) return hours + "h ago";
|
|
766
|
+
const days = Math.floor(hours / 24);
|
|
767
|
+
if (days < 7) return days + "d ago";
|
|
768
|
+
return new Date(timestamp).toLocaleDateString();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ---------------------------------------------------------------------------
|
|
772
|
+
// Hash router — enables bookmarks and browser back/forward
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
|
|
775
|
+
function handleRoute() {
|
|
776
|
+
const hash = location.hash || "#/";
|
|
777
|
+
|
|
778
|
+
// #/app/{themeName}/{templateId} → open specific template in chat
|
|
779
|
+
const appTemplateMatch = hash.match(/^#\/app\/([^/]+)\/(.+)$/);
|
|
780
|
+
if (appTemplateMatch) {
|
|
781
|
+
const themeName = decodeURIComponent(appTemplateMatch[1]);
|
|
782
|
+
const templateId = decodeURIComponent(appTemplateMatch[2]);
|
|
783
|
+
// Already showing this — nothing to do
|
|
784
|
+
if (currentAppTheme === themeName && !appScreen.classList.contains("hidden")) return;
|
|
785
|
+
// Open theme then activate template
|
|
786
|
+
openTheme(themeName).then(() => {
|
|
787
|
+
if (typeof showChat === "function") {
|
|
788
|
+
showChat(themeName, templateId);
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// #/app/{themeName} → open theme (goes to dashboard or direct)
|
|
795
|
+
const appMatch = hash.match(/^#\/app\/([^/]+)$/);
|
|
796
|
+
if (appMatch) {
|
|
797
|
+
const themeName = decodeURIComponent(appMatch[1]);
|
|
798
|
+
if (currentAppTheme === themeName && !appScreen.classList.contains("hidden")) return;
|
|
799
|
+
openTheme(themeName);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// #/dashboard/{themeName} → show dashboard for theme
|
|
804
|
+
const dashMatch = hash.match(/^#\/dashboard\/(.+)$/);
|
|
805
|
+
if (dashMatch) {
|
|
806
|
+
const themeName = decodeURIComponent(dashMatch[1]);
|
|
807
|
+
const dashEl = document.getElementById("dashboard-screen");
|
|
808
|
+
if (currentDashboardTheme === themeName && dashEl && !dashEl.classList.contains("hidden")) return;
|
|
809
|
+
openTheme(themeName);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Default: show setup
|
|
814
|
+
const dashEl = document.getElementById("dashboard-screen");
|
|
815
|
+
if (!appScreen.classList.contains("hidden") || (dashEl && !dashEl.classList.contains("hidden"))) {
|
|
816
|
+
showSetup();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
window.addEventListener("popstate", handleRoute);
|
|
821
|
+
|
|
822
|
+
// ---------------------------------------------------------------------------
|
|
823
|
+
// Initialize — check URL hash first, fall back to setup screen
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
|
|
826
|
+
if (location.hash && (location.hash.startsWith("#/app/") || location.hash.startsWith("#/dashboard/"))) {
|
|
827
|
+
handleRoute();
|
|
828
|
+
} else {
|
|
829
|
+
initSetup();
|
|
830
|
+
}
|