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/settings.js
ADDED
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings panel — environment detection, AI engine selection, API keys, tool install, auth flows.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// State
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
let settingsData = null;
|
|
10
|
+
const activePolls = {};
|
|
11
|
+
|
|
12
|
+
const ENGINE_LABELS = {
|
|
13
|
+
"claude-code": "Claude Code",
|
|
14
|
+
"anthropic-api": "Anthropic API",
|
|
15
|
+
"openai-api": "OpenAI API",
|
|
16
|
+
"gemini-cli": "Gemini CLI",
|
|
17
|
+
"gemini-api": "Gemini API",
|
|
18
|
+
"codex-cli": "OpenAI Codex",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Open / Close
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function openSettings() {
|
|
26
|
+
// Close menu if open
|
|
27
|
+
if (typeof closeMenu === "function") closeMenu();
|
|
28
|
+
document.getElementById("settings-overlay").classList.remove("hidden");
|
|
29
|
+
refreshSettings();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function closeSettings() {
|
|
33
|
+
document.getElementById("settings-overlay").classList.add("hidden");
|
|
34
|
+
Object.keys(activePolls).forEach((id) => {
|
|
35
|
+
clearInterval(activePolls[id]);
|
|
36
|
+
delete activePolls[id];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Fetch and render
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
async function refreshSettings() {
|
|
45
|
+
const body = document.getElementById("settings-body");
|
|
46
|
+
body.innerHTML = `<div class="settings__loading"><div class="settings__spinner-lg"></div><span>Loading environment...</span></div>`;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch("/api/settings/status");
|
|
50
|
+
settingsData = await res.json();
|
|
51
|
+
renderSettings(settingsData);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
body.innerHTML = `<div class="settings__loading" style="color:var(--error)">Failed to load settings</div>`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderSettings(data) {
|
|
58
|
+
const body = document.getElementById("settings-body");
|
|
59
|
+
const env = data.environment;
|
|
60
|
+
const config = data.config;
|
|
61
|
+
|
|
62
|
+
body.innerHTML = "";
|
|
63
|
+
|
|
64
|
+
// --- AI Engines section ---
|
|
65
|
+
const aiSection = el("section", "settings__section");
|
|
66
|
+
aiSection.appendChild(sectionTitle("AI Engine"));
|
|
67
|
+
|
|
68
|
+
// Engine selector pills
|
|
69
|
+
const selectEl = el("div", "settings__engine-select");
|
|
70
|
+
const allEngines = [
|
|
71
|
+
{ id: "claude-code", label: "Claude Code" },
|
|
72
|
+
{ id: "anthropic-api", label: "Anthropic API" },
|
|
73
|
+
{ id: "openai-api", label: "OpenAI API" },
|
|
74
|
+
{ id: "gemini-cli", label: "Gemini CLI" },
|
|
75
|
+
{ id: "gemini-api", label: "Gemini API" },
|
|
76
|
+
{ id: "codex-cli", label: "Codex CLI" },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const eng of allEngines) {
|
|
80
|
+
const available = env.availableEngines.includes(eng.id);
|
|
81
|
+
const btn = el("button", "settings__engine-option");
|
|
82
|
+
btn.textContent = eng.label;
|
|
83
|
+
btn.disabled = !available;
|
|
84
|
+
if (config.aiEngine === eng.id) btn.classList.add("active");
|
|
85
|
+
if (!config.aiEngine && available && env.availableEngines[0] === eng.id) {
|
|
86
|
+
// Highlight first available if none selected
|
|
87
|
+
}
|
|
88
|
+
btn.addEventListener("click", () => setEngine(eng.id));
|
|
89
|
+
selectEl.appendChild(btn);
|
|
90
|
+
}
|
|
91
|
+
aiSection.appendChild(selectEl);
|
|
92
|
+
|
|
93
|
+
// Model selector (for the active engine)
|
|
94
|
+
const activeEngine = config.aiEngine || (env.availableEngines.length > 0 ? env.availableEngines[0] : null);
|
|
95
|
+
if (activeEngine) {
|
|
96
|
+
const modelRow = el("div", "settings__model-row");
|
|
97
|
+
const modelLabel = el("span", "settings__card-label");
|
|
98
|
+
modelLabel.textContent = "Model";
|
|
99
|
+
modelRow.appendChild(modelLabel);
|
|
100
|
+
|
|
101
|
+
const modelSelect = el("select", "settings__model-select");
|
|
102
|
+
const models = getModelsForEngine(activeEngine);
|
|
103
|
+
const currentModel = getCurrentModel(activeEngine, config);
|
|
104
|
+
|
|
105
|
+
for (const m of models) {
|
|
106
|
+
const opt = document.createElement("option");
|
|
107
|
+
opt.value = m.id;
|
|
108
|
+
opt.textContent = m.label;
|
|
109
|
+
if (m.id === currentModel) opt.selected = true;
|
|
110
|
+
modelSelect.appendChild(opt);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Custom model option
|
|
114
|
+
const customOpt = document.createElement("option");
|
|
115
|
+
customOpt.value = "__custom__";
|
|
116
|
+
customOpt.textContent = "Custom...";
|
|
117
|
+
if (currentModel && !models.find((m) => m.id === currentModel)) {
|
|
118
|
+
customOpt.selected = true;
|
|
119
|
+
}
|
|
120
|
+
modelSelect.appendChild(customOpt);
|
|
121
|
+
|
|
122
|
+
modelSelect.addEventListener("change", async () => {
|
|
123
|
+
if (modelSelect.value === "__custom__") {
|
|
124
|
+
const custom = await vibePrompt("Enter model name");
|
|
125
|
+
if (custom) setEngineModel(activeEngine, custom);
|
|
126
|
+
else refreshSettings();
|
|
127
|
+
} else {
|
|
128
|
+
setEngineModel(activeEngine, modelSelect.value);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
modelRow.appendChild(modelSelect);
|
|
133
|
+
aiSection.appendChild(modelRow);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// CLI tools subsection
|
|
137
|
+
aiSection.appendChild(subsectionTitle("CLI Tools"));
|
|
138
|
+
const cliTools = [
|
|
139
|
+
{ key: "claudeCode", name: "Claude Code", installId: "claude", url: "https://claude.ai/code" },
|
|
140
|
+
{ key: "geminiCli", name: "Gemini CLI", installId: "gemini", url: "https://github.com/google-gemini/gemini-cli" },
|
|
141
|
+
{ key: "codexCli", name: "Codex CLI", installId: "codex", url: "https://github.com/openai/codex" },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
for (const tool of cliTools) {
|
|
145
|
+
const info = env.tools[tool.key];
|
|
146
|
+
aiSection.appendChild(createToolCard(tool.name, info, tool.installId, tool.url));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// API keys subsection
|
|
150
|
+
aiSection.appendChild(subsectionTitle("API Keys"));
|
|
151
|
+
const providers = [
|
|
152
|
+
{ key: "anthropic", name: "Anthropic", placeholder: "sk-ant-api03-..." },
|
|
153
|
+
{ key: "openai", name: "OpenAI", placeholder: "sk-..." },
|
|
154
|
+
{ key: "gemini", name: "Google AI", placeholder: "AIza..." },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
for (const prov of providers) {
|
|
158
|
+
const keyInfo = env.apiKeys[prov.key];
|
|
159
|
+
aiSection.appendChild(createApiKeyCard(prov.key, prov.name, prov.placeholder, keyInfo));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
body.appendChild(aiSection);
|
|
163
|
+
|
|
164
|
+
// --- HubSpot section ---
|
|
165
|
+
const hsSection = el("section", "settings__section");
|
|
166
|
+
hsSection.appendChild(sectionTitle("HubSpot"));
|
|
167
|
+
hsSection.appendChild(createHubSpotCard(env.tools.hubspot));
|
|
168
|
+
body.appendChild(hsSection);
|
|
169
|
+
|
|
170
|
+
// --- GitHub section ---
|
|
171
|
+
const ghSection = el("section", "settings__section");
|
|
172
|
+
ghSection.appendChild(sectionTitle("GitHub"));
|
|
173
|
+
ghSection.appendChild(createGitHubCard(env.tools.github));
|
|
174
|
+
body.appendChild(ghSection);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Tool card
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function createToolCard(name, info, installId, url) {
|
|
182
|
+
const card = el("div", "settings__card");
|
|
183
|
+
|
|
184
|
+
const row = el("div", "settings__card-row");
|
|
185
|
+
|
|
186
|
+
if (info.found) {
|
|
187
|
+
row.appendChild(dot(info.authenticated ? "success" : "warn"));
|
|
188
|
+
} else {
|
|
189
|
+
row.appendChild(dot("muted"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const label = el("span", "settings__card-label");
|
|
193
|
+
label.textContent = name;
|
|
194
|
+
row.appendChild(label);
|
|
195
|
+
|
|
196
|
+
if (info.found) {
|
|
197
|
+
const meta = el("span", "settings__card-meta");
|
|
198
|
+
meta.textContent = `v${info.version}`;
|
|
199
|
+
row.appendChild(meta);
|
|
200
|
+
} else {
|
|
201
|
+
const installBtn = el("button", "settings__btn");
|
|
202
|
+
installBtn.textContent = "Install";
|
|
203
|
+
installBtn.addEventListener("click", () => installTool(installId, installBtn));
|
|
204
|
+
row.appendChild(installBtn);
|
|
205
|
+
|
|
206
|
+
if (url) {
|
|
207
|
+
const link = el("a", "settings__btn");
|
|
208
|
+
link.textContent = "Docs";
|
|
209
|
+
link.href = url;
|
|
210
|
+
link.target = "_blank";
|
|
211
|
+
link.style.textDecoration = "none";
|
|
212
|
+
row.appendChild(link);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
card.appendChild(row);
|
|
217
|
+
|
|
218
|
+
// Auth action row for installed but not authenticated CLI tools
|
|
219
|
+
if (info.found && !info.authenticated) {
|
|
220
|
+
const authRow = el("div", "settings__card-row settings__card-row--sub");
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
// Claude Code / Gemini CLI — browser-based sign in
|
|
224
|
+
const authLabel = el("span", "settings__card-meta");
|
|
225
|
+
authLabel.textContent = info.authDetail || "Not authenticated";
|
|
226
|
+
authLabel.style.color = "var(--warning, #f59e0b)";
|
|
227
|
+
authRow.appendChild(authLabel);
|
|
228
|
+
|
|
229
|
+
const authBtn = el("button", "settings__btn settings__btn--primary");
|
|
230
|
+
authBtn.textContent = "Sign in";
|
|
231
|
+
authBtn.addEventListener("click", () => authCLI(installId, authBtn));
|
|
232
|
+
authRow.appendChild(authBtn);
|
|
233
|
+
|
|
234
|
+
card.appendChild(authRow);
|
|
235
|
+
}
|
|
236
|
+
} else if (info.found && info.authenticated) {
|
|
237
|
+
const authRow = el("div", "settings__card-row settings__card-row--sub");
|
|
238
|
+
const authLabel = el("span", "settings__card-meta");
|
|
239
|
+
authLabel.textContent = "Authenticated";
|
|
240
|
+
authLabel.style.color = "var(--success, #22c55e)";
|
|
241
|
+
authRow.appendChild(authLabel);
|
|
242
|
+
card.appendChild(authRow);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return card;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// API key card
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
function createApiKeyCard(provider, name, placeholder, keyInfo) {
|
|
253
|
+
const card = el("div", "settings__apikey-row");
|
|
254
|
+
|
|
255
|
+
const label = el("span", "settings__apikey-label");
|
|
256
|
+
label.textContent = name;
|
|
257
|
+
card.appendChild(label);
|
|
258
|
+
|
|
259
|
+
if (keyInfo.configured) {
|
|
260
|
+
// Show masked key + edit/clear buttons
|
|
261
|
+
const value = el("span", "settings__apikey-value");
|
|
262
|
+
value.textContent = keyInfo.masked;
|
|
263
|
+
card.appendChild(value);
|
|
264
|
+
|
|
265
|
+
const actions = el("div", "settings__apikey-actions");
|
|
266
|
+
|
|
267
|
+
const editBtn = el("button", "settings__btn");
|
|
268
|
+
editBtn.textContent = "Edit";
|
|
269
|
+
editBtn.addEventListener("click", () => {
|
|
270
|
+
// Replace card content with input
|
|
271
|
+
showApiKeyInput(card, provider, name, placeholder);
|
|
272
|
+
});
|
|
273
|
+
actions.appendChild(editBtn);
|
|
274
|
+
|
|
275
|
+
const clearBtn = el("button", "settings__btn");
|
|
276
|
+
clearBtn.textContent = "Clear";
|
|
277
|
+
clearBtn.addEventListener("click", async () => {
|
|
278
|
+
await fetch("/api/settings/apikey", {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { "Content-Type": "application/json" },
|
|
281
|
+
body: JSON.stringify({ provider, apiKey: null }),
|
|
282
|
+
});
|
|
283
|
+
refreshSettings();
|
|
284
|
+
});
|
|
285
|
+
actions.appendChild(clearBtn);
|
|
286
|
+
|
|
287
|
+
card.appendChild(actions);
|
|
288
|
+
} else {
|
|
289
|
+
showApiKeyInput(card, provider, name, placeholder);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return card;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function showApiKeyInput(container, provider, name, placeholder) {
|
|
296
|
+
// Clear existing content except label
|
|
297
|
+
const label = container.querySelector(".settings__apikey-label");
|
|
298
|
+
container.innerHTML = "";
|
|
299
|
+
if (label) container.appendChild(label);
|
|
300
|
+
else {
|
|
301
|
+
const lbl = el("span", "settings__apikey-label");
|
|
302
|
+
lbl.textContent = name;
|
|
303
|
+
container.appendChild(lbl);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const input = el("input", "settings__apikey-input");
|
|
307
|
+
input.type = "password";
|
|
308
|
+
input.placeholder = placeholder;
|
|
309
|
+
container.appendChild(input);
|
|
310
|
+
|
|
311
|
+
const saveBtn = el("button", "settings__btn settings__btn--primary");
|
|
312
|
+
saveBtn.textContent = "Save";
|
|
313
|
+
saveBtn.addEventListener("click", () => saveApiKey(provider, input.value, saveBtn));
|
|
314
|
+
container.appendChild(saveBtn);
|
|
315
|
+
|
|
316
|
+
input.addEventListener("keydown", (e) => {
|
|
317
|
+
if (e.key === "Enter") { e.preventDefault(); saveBtn.click(); }
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
input.focus();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// HubSpot card
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
function createHubSpotCard(hs) {
|
|
328
|
+
const card = el("div", "settings__card");
|
|
329
|
+
|
|
330
|
+
// CLI status
|
|
331
|
+
const cliRow = el("div", "settings__card-row");
|
|
332
|
+
cliRow.appendChild(dot(hs.found ? "success" : "warn"));
|
|
333
|
+
const cliLabel = el("span", "settings__card-label");
|
|
334
|
+
cliLabel.textContent = "HubSpot CLI";
|
|
335
|
+
cliRow.appendChild(cliLabel);
|
|
336
|
+
|
|
337
|
+
if (hs.found) {
|
|
338
|
+
const meta = el("span", "settings__card-meta");
|
|
339
|
+
meta.textContent = `v${hs.version}`;
|
|
340
|
+
cliRow.appendChild(meta);
|
|
341
|
+
} else {
|
|
342
|
+
const installBtn = el("button", "settings__btn");
|
|
343
|
+
installBtn.textContent = "Install";
|
|
344
|
+
installBtn.addEventListener("click", () => installTool("hubspot", installBtn));
|
|
345
|
+
cliRow.appendChild(installBtn);
|
|
346
|
+
}
|
|
347
|
+
card.appendChild(cliRow);
|
|
348
|
+
|
|
349
|
+
// Accounts list (if CLI is installed)
|
|
350
|
+
if (hs.found) {
|
|
351
|
+
const accounts = hs.accounts || [];
|
|
352
|
+
|
|
353
|
+
if (accounts.length > 0) {
|
|
354
|
+
// Show each account with switch/remove actions
|
|
355
|
+
accounts.forEach((acct) => {
|
|
356
|
+
const row = el("div", "settings__card-row");
|
|
357
|
+
row.appendChild(dot(acct.isDefault ? "success" : "muted"));
|
|
358
|
+
const label = el("span", "settings__card-label");
|
|
359
|
+
label.textContent = `${acct.name} (${acct.portalId})`;
|
|
360
|
+
if (acct.isDefault) label.textContent += " — active";
|
|
361
|
+
row.appendChild(label);
|
|
362
|
+
|
|
363
|
+
const actions = el("span", "settings__card-actions");
|
|
364
|
+
|
|
365
|
+
if (!acct.isDefault) {
|
|
366
|
+
const useBtn = el("button", "settings__btn settings__btn--small");
|
|
367
|
+
useBtn.textContent = "Use";
|
|
368
|
+
useBtn.addEventListener("click", () => switchHsAccount(acct.portalId, useBtn));
|
|
369
|
+
actions.appendChild(useBtn);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const removeBtn = el("button", "settings__btn settings__btn--small settings__btn--danger");
|
|
373
|
+
removeBtn.textContent = "Remove";
|
|
374
|
+
removeBtn.addEventListener("click", () => removeHsAccount(acct.portalId, removeBtn));
|
|
375
|
+
actions.appendChild(removeBtn);
|
|
376
|
+
|
|
377
|
+
row.appendChild(actions);
|
|
378
|
+
card.appendChild(row);
|
|
379
|
+
});
|
|
380
|
+
} else if (!hs.authenticated) {
|
|
381
|
+
const authRow = el("div", "settings__card-row");
|
|
382
|
+
authRow.appendChild(dot("warn"));
|
|
383
|
+
const authLabel = el("span", "settings__card-label");
|
|
384
|
+
authLabel.textContent = "Not authenticated";
|
|
385
|
+
authRow.appendChild(authLabel);
|
|
386
|
+
card.appendChild(authRow);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// "Add account" button (always available when CLI is installed)
|
|
390
|
+
const addRow = el("div", "settings__card-row");
|
|
391
|
+
const addBtn = el("button", "settings__btn settings__btn--primary");
|
|
392
|
+
addBtn.textContent = "Add Account";
|
|
393
|
+
addBtn.addEventListener("click", () => startHsAuth(addBtn, card));
|
|
394
|
+
addRow.appendChild(addBtn);
|
|
395
|
+
card.appendChild(addRow);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return card;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// GitHub card
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
function createGitHubCard(gh) {
|
|
406
|
+
const card = el("div", "settings__card");
|
|
407
|
+
|
|
408
|
+
// CLI status
|
|
409
|
+
const cliRow = el("div", "settings__card-row");
|
|
410
|
+
cliRow.appendChild(dot(gh.found ? "success" : "muted"));
|
|
411
|
+
const cliLabel = el("span", "settings__card-label");
|
|
412
|
+
cliLabel.textContent = "GitHub CLI";
|
|
413
|
+
cliRow.appendChild(cliLabel);
|
|
414
|
+
|
|
415
|
+
if (gh.found) {
|
|
416
|
+
const meta = el("span", "settings__card-meta");
|
|
417
|
+
meta.textContent = `v${gh.version}`;
|
|
418
|
+
cliRow.appendChild(meta);
|
|
419
|
+
} else {
|
|
420
|
+
const installBtn = el("button", "settings__btn");
|
|
421
|
+
installBtn.textContent = "Install";
|
|
422
|
+
installBtn.addEventListener("click", () => installTool("gh", installBtn));
|
|
423
|
+
cliRow.appendChild(installBtn);
|
|
424
|
+
}
|
|
425
|
+
card.appendChild(cliRow);
|
|
426
|
+
|
|
427
|
+
// Auth status
|
|
428
|
+
if (gh.found) {
|
|
429
|
+
const authRow = el("div", "settings__card-row");
|
|
430
|
+
authRow.appendChild(dot(gh.authenticated ? "success" : "muted"));
|
|
431
|
+
const authLabel = el("span", "settings__card-label");
|
|
432
|
+
authLabel.textContent = gh.authenticated
|
|
433
|
+
? `Logged in as ${gh.username}`
|
|
434
|
+
: "Not authenticated";
|
|
435
|
+
authRow.appendChild(authLabel);
|
|
436
|
+
|
|
437
|
+
const actions = el("span", "settings__card-actions");
|
|
438
|
+
|
|
439
|
+
if (gh.authenticated) {
|
|
440
|
+
// Switch account = logout + login
|
|
441
|
+
const switchBtn = el("button", "settings__btn settings__btn--small");
|
|
442
|
+
switchBtn.textContent = "Switch";
|
|
443
|
+
switchBtn.addEventListener("click", () => switchGhAccount(switchBtn));
|
|
444
|
+
actions.appendChild(switchBtn);
|
|
445
|
+
|
|
446
|
+
const logoutBtn = el("button", "settings__btn settings__btn--small settings__btn--danger");
|
|
447
|
+
logoutBtn.textContent = "Log out";
|
|
448
|
+
logoutBtn.addEventListener("click", () => logoutGh(logoutBtn));
|
|
449
|
+
actions.appendChild(logoutBtn);
|
|
450
|
+
} else {
|
|
451
|
+
const authBtn = el("button", "settings__btn settings__btn--primary");
|
|
452
|
+
authBtn.textContent = "Log in";
|
|
453
|
+
authBtn.addEventListener("click", () => startGhAuth(authBtn));
|
|
454
|
+
actions.appendChild(authBtn);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
authRow.appendChild(actions);
|
|
458
|
+
card.appendChild(authRow);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return card;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Actions
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
async function setEngine(engineId) {
|
|
469
|
+
await fetch("/api/settings/engine", {
|
|
470
|
+
method: "POST",
|
|
471
|
+
headers: { "Content-Type": "application/json" },
|
|
472
|
+
body: JSON.stringify({ engine: engineId }),
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Update statusbar engine label
|
|
476
|
+
const statusEngine = document.getElementById("status-engine");
|
|
477
|
+
if (statusEngine) {
|
|
478
|
+
statusEngine.textContent = ENGINE_LABELS[engineId] || engineId;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
refreshSettings();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function saveApiKey(provider, key, btn) {
|
|
485
|
+
if (!key || !key.trim()) return;
|
|
486
|
+
btn.disabled = true;
|
|
487
|
+
btn.textContent = "Saving...";
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const res = await fetch("/api/settings/apikey", {
|
|
491
|
+
method: "POST",
|
|
492
|
+
headers: { "Content-Type": "application/json" },
|
|
493
|
+
body: JSON.stringify({ provider, apiKey: key.trim() }),
|
|
494
|
+
});
|
|
495
|
+
const data = await res.json();
|
|
496
|
+
if (data.ok) {
|
|
497
|
+
refreshSettings();
|
|
498
|
+
} else {
|
|
499
|
+
btn.textContent = "Error";
|
|
500
|
+
setTimeout(() => { btn.textContent = "Save"; btn.disabled = false; }, 2000);
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
btn.textContent = "Error";
|
|
504
|
+
setTimeout(() => { btn.textContent = "Save"; btn.disabled = false; }, 2000);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function installTool(toolId, btn) {
|
|
509
|
+
btn.disabled = true;
|
|
510
|
+
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const res = await fetch("/api/settings/install", {
|
|
514
|
+
method: "POST",
|
|
515
|
+
headers: { "Content-Type": "application/json" },
|
|
516
|
+
body: JSON.stringify({ tool: toolId }),
|
|
517
|
+
});
|
|
518
|
+
const data = await res.json();
|
|
519
|
+
|
|
520
|
+
if (data.jobId) {
|
|
521
|
+
pollJob(data.jobId, () => {
|
|
522
|
+
refreshSettings();
|
|
523
|
+
}, (err) => {
|
|
524
|
+
btn.textContent = "Failed";
|
|
525
|
+
btn.disabled = false;
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
btn.textContent = "Failed";
|
|
530
|
+
btn.disabled = false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function switchHsAccount(portalId, btn) {
|
|
535
|
+
btn.disabled = true;
|
|
536
|
+
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const res = await fetch("/api/settings/hs-switch", {
|
|
540
|
+
method: "POST",
|
|
541
|
+
headers: { "Content-Type": "application/json" },
|
|
542
|
+
body: JSON.stringify({ portalId }),
|
|
543
|
+
});
|
|
544
|
+
const data = await res.json();
|
|
545
|
+
if (data.jobId) {
|
|
546
|
+
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
547
|
+
btn.textContent = "Failed";
|
|
548
|
+
btn.disabled = false;
|
|
549
|
+
});
|
|
550
|
+
} else {
|
|
551
|
+
refreshSettings();
|
|
552
|
+
}
|
|
553
|
+
} catch {
|
|
554
|
+
btn.textContent = "Failed";
|
|
555
|
+
btn.disabled = false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function removeHsAccount(portalId, btn) {
|
|
560
|
+
btn.disabled = true;
|
|
561
|
+
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const res = await fetch("/api/settings/hs-switch", {
|
|
565
|
+
method: "POST",
|
|
566
|
+
headers: { "Content-Type": "application/json" },
|
|
567
|
+
body: JSON.stringify({ portalId, action: "remove" }),
|
|
568
|
+
});
|
|
569
|
+
const data = await res.json();
|
|
570
|
+
if (data.jobId) {
|
|
571
|
+
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
572
|
+
btn.textContent = "Failed";
|
|
573
|
+
btn.disabled = false;
|
|
574
|
+
});
|
|
575
|
+
} else {
|
|
576
|
+
refreshSettings();
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
btn.textContent = "Failed";
|
|
580
|
+
btn.disabled = false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function logoutGh(btn) {
|
|
585
|
+
btn.disabled = true;
|
|
586
|
+
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const res = await fetch("/api/settings/gh-logout", {
|
|
590
|
+
method: "POST",
|
|
591
|
+
headers: { "Content-Type": "application/json" },
|
|
592
|
+
body: JSON.stringify({}),
|
|
593
|
+
});
|
|
594
|
+
const data = await res.json();
|
|
595
|
+
if (data.jobId) {
|
|
596
|
+
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
597
|
+
btn.textContent = "Failed";
|
|
598
|
+
btn.disabled = false;
|
|
599
|
+
});
|
|
600
|
+
} else {
|
|
601
|
+
refreshSettings();
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
btn.textContent = "Failed";
|
|
605
|
+
btn.disabled = false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function switchGhAccount(btn) {
|
|
610
|
+
btn.disabled = true;
|
|
611
|
+
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
// Logout first, then trigger login
|
|
615
|
+
const res = await fetch("/api/settings/gh-logout", {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers: { "Content-Type": "application/json" },
|
|
618
|
+
body: JSON.stringify({}),
|
|
619
|
+
});
|
|
620
|
+
const data = await res.json();
|
|
621
|
+
if (data.jobId) {
|
|
622
|
+
pollJob(data.jobId, () => {
|
|
623
|
+
// After logout completes, start login flow
|
|
624
|
+
startGhAuth(btn);
|
|
625
|
+
}, () => {
|
|
626
|
+
btn.textContent = "Failed";
|
|
627
|
+
btn.disabled = false;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
btn.textContent = "Failed";
|
|
632
|
+
btn.disabled = false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function startHsAuth(btn, card) {
|
|
637
|
+
btn.disabled = true;
|
|
638
|
+
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
const res = await fetch("/api/settings/hs-auth", {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: { "Content-Type": "application/json" },
|
|
644
|
+
body: JSON.stringify({ force: true }),
|
|
645
|
+
});
|
|
646
|
+
const data = await res.json();
|
|
647
|
+
|
|
648
|
+
if (data.needsKey) {
|
|
649
|
+
// Show instructions to get a personal access key
|
|
650
|
+
btn.textContent = "Connect";
|
|
651
|
+
btn.disabled = false;
|
|
652
|
+
|
|
653
|
+
const instructions = el("div", "settings__instructions");
|
|
654
|
+
instructions.innerHTML = `
|
|
655
|
+
<strong>Connect your HubSpot account:</strong>
|
|
656
|
+
<ol>
|
|
657
|
+
${data.steps.map((s) => `<li>${escSettings(s)}</li>`).join("")}
|
|
658
|
+
</ol>
|
|
659
|
+
<a href="${escSettings(data.url)}" target="_blank">Open HubSpot →</a>
|
|
660
|
+
<div class="settings__pak-row">
|
|
661
|
+
<input type="password" class="settings__apikey-input" id="hs-pak-input" placeholder="Personal access key..." />
|
|
662
|
+
<button class="settings__btn settings__btn--primary" id="hs-pak-save">Save</button>
|
|
663
|
+
</div>
|
|
664
|
+
`;
|
|
665
|
+
card.appendChild(instructions);
|
|
666
|
+
|
|
667
|
+
document.getElementById("hs-pak-save").addEventListener("click", async () => {
|
|
668
|
+
const key = document.getElementById("hs-pak-input").value.trim();
|
|
669
|
+
if (!key) return;
|
|
670
|
+
const saveBtn = document.getElementById("hs-pak-save");
|
|
671
|
+
saveBtn.disabled = true;
|
|
672
|
+
saveBtn.innerHTML = '<span class="settings__spinner"></span>';
|
|
673
|
+
|
|
674
|
+
const authRes = await fetch("/api/settings/hs-auth", {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { "Content-Type": "application/json" },
|
|
677
|
+
body: JSON.stringify({ personalAccessKey: key }),
|
|
678
|
+
});
|
|
679
|
+
const authData = await authRes.json();
|
|
680
|
+
|
|
681
|
+
if (authData.jobId) {
|
|
682
|
+
pollJob(authData.jobId, () => refreshSettings(), () => {
|
|
683
|
+
saveBtn.textContent = "Failed";
|
|
684
|
+
saveBtn.disabled = false;
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
refreshSettings();
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (data.jobId) {
|
|
694
|
+
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
695
|
+
btn.textContent = "Failed";
|
|
696
|
+
btn.disabled = false;
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
btn.textContent = "Failed";
|
|
701
|
+
btn.disabled = false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function startGhAuth(btn) {
|
|
706
|
+
btn.disabled = true;
|
|
707
|
+
btn.innerHTML = '<span class="settings__spinner"></span> Check browser...';
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
const res = await fetch("/api/settings/gh-auth", {
|
|
711
|
+
method: "POST",
|
|
712
|
+
headers: { "Content-Type": "application/json" },
|
|
713
|
+
body: JSON.stringify({}),
|
|
714
|
+
});
|
|
715
|
+
const data = await res.json();
|
|
716
|
+
|
|
717
|
+
if (data.alreadyAuthenticated) {
|
|
718
|
+
refreshSettings();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (data.jobId) {
|
|
723
|
+
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
724
|
+
btn.textContent = "Failed";
|
|
725
|
+
btn.disabled = false;
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
btn.textContent = "Failed";
|
|
730
|
+
btn.disabled = false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
// Job polling
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
|
|
738
|
+
function pollJob(jobId, onComplete, onError) {
|
|
739
|
+
const interval = setInterval(async () => {
|
|
740
|
+
try {
|
|
741
|
+
const res = await fetch(`/api/settings/job/${jobId}`);
|
|
742
|
+
const job = await res.json();
|
|
743
|
+
|
|
744
|
+
if (job.status === "completed") {
|
|
745
|
+
clearInterval(interval);
|
|
746
|
+
delete activePolls[jobId];
|
|
747
|
+
onComplete();
|
|
748
|
+
} else if (job.status === "failed") {
|
|
749
|
+
clearInterval(interval);
|
|
750
|
+
delete activePolls[jobId];
|
|
751
|
+
onError(job.output);
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
clearInterval(interval);
|
|
755
|
+
delete activePolls[jobId];
|
|
756
|
+
onError("Connection lost");
|
|
757
|
+
}
|
|
758
|
+
}, 2000);
|
|
759
|
+
|
|
760
|
+
activePolls[jobId] = interval;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ---------------------------------------------------------------------------
|
|
764
|
+
// CLI auth
|
|
765
|
+
// ---------------------------------------------------------------------------
|
|
766
|
+
|
|
767
|
+
async function authCLI(cli, btn, apiKey) {
|
|
768
|
+
btn.disabled = true;
|
|
769
|
+
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
770
|
+
|
|
771
|
+
try {
|
|
772
|
+
const payload = { cli };
|
|
773
|
+
if (apiKey) payload.apiKey = apiKey;
|
|
774
|
+
|
|
775
|
+
const res = await fetch("/api/settings/cli-auth", {
|
|
776
|
+
method: "POST",
|
|
777
|
+
headers: { "Content-Type": "application/json" },
|
|
778
|
+
body: JSON.stringify(payload),
|
|
779
|
+
});
|
|
780
|
+
const data = await res.json();
|
|
781
|
+
|
|
782
|
+
if (data.error) {
|
|
783
|
+
btn.textContent = "Failed";
|
|
784
|
+
setTimeout(() => { btn.textContent = "Sign in"; btn.disabled = false; }, 2000);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (data.hint) {
|
|
789
|
+
// Show hint to user (e.g., check browser)
|
|
790
|
+
btn.innerHTML = '<span class="settings__spinner"></span> Check browser...';
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (data.jobId) {
|
|
794
|
+
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
795
|
+
btn.textContent = "Failed — try again";
|
|
796
|
+
btn.disabled = false;
|
|
797
|
+
});
|
|
798
|
+
} else {
|
|
799
|
+
// Immediate success (e.g., Codex API key saved)
|
|
800
|
+
refreshSettings();
|
|
801
|
+
}
|
|
802
|
+
} catch {
|
|
803
|
+
btn.textContent = "Failed";
|
|
804
|
+
setTimeout(() => { btn.textContent = "Sign in"; btn.disabled = false; }, 2000);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ---------------------------------------------------------------------------
|
|
809
|
+
// Model selection helpers
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
|
|
812
|
+
function getModelsForEngine(engine) {
|
|
813
|
+
switch (engine) {
|
|
814
|
+
case "claude-code":
|
|
815
|
+
return [
|
|
816
|
+
{ id: "sonnet", label: "Claude Sonnet (default)" },
|
|
817
|
+
{ id: "opus", label: "Claude Opus" },
|
|
818
|
+
{ id: "haiku", label: "Claude Haiku" },
|
|
819
|
+
];
|
|
820
|
+
case "anthropic-api":
|
|
821
|
+
return [
|
|
822
|
+
{ id: "claude-sonnet-4-20250514", label: "Claude Sonnet 4 (default)" },
|
|
823
|
+
{ id: "claude-opus-4-20250514", label: "Claude Opus 4" },
|
|
824
|
+
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
|
|
825
|
+
{ id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
|
|
826
|
+
];
|
|
827
|
+
case "openai-api":
|
|
828
|
+
return [
|
|
829
|
+
{ id: "gpt-4o", label: "GPT-4o (default)" },
|
|
830
|
+
{ id: "gpt-4o-mini", label: "GPT-4o Mini" },
|
|
831
|
+
{ id: "o3", label: "o3" },
|
|
832
|
+
{ id: "o4-mini", label: "o4 Mini" },
|
|
833
|
+
];
|
|
834
|
+
case "gemini-cli":
|
|
835
|
+
case "gemini-api":
|
|
836
|
+
return [
|
|
837
|
+
{ id: "gemini-2.0-flash", label: "Gemini 2.0 Flash (default)" },
|
|
838
|
+
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
|
839
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
|
|
840
|
+
];
|
|
841
|
+
case "codex-cli":
|
|
842
|
+
return [
|
|
843
|
+
{ id: "o4-mini", label: "o4 Mini (default)" },
|
|
844
|
+
{ id: "o3", label: "o3" },
|
|
845
|
+
{ id: "gpt-4o", label: "GPT-4o" },
|
|
846
|
+
];
|
|
847
|
+
default:
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function getCurrentModel(engine, config) {
|
|
853
|
+
switch (engine) {
|
|
854
|
+
case "claude-code": return config.claudeCodeModel || "sonnet";
|
|
855
|
+
case "anthropic-api": return config.anthropicApiModel || "claude-sonnet-4-20250514";
|
|
856
|
+
case "openai-api": return config.openaiApiModel || "gpt-4o";
|
|
857
|
+
default: return null;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function setEngineModel(engine, model) {
|
|
862
|
+
await fetch("/api/settings/engine", {
|
|
863
|
+
method: "POST",
|
|
864
|
+
headers: { "Content-Type": "application/json" },
|
|
865
|
+
body: JSON.stringify({ engine, model }),
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// Update statusbar
|
|
869
|
+
const statusEngine = document.getElementById("status-engine");
|
|
870
|
+
if (statusEngine) {
|
|
871
|
+
const label = ENGINE_LABELS[engine] || engine;
|
|
872
|
+
statusEngine.textContent = label;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
refreshSettings();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ---------------------------------------------------------------------------
|
|
879
|
+
// DOM helpers
|
|
880
|
+
// ---------------------------------------------------------------------------
|
|
881
|
+
|
|
882
|
+
function el(tag, className) {
|
|
883
|
+
const e = document.createElement(tag);
|
|
884
|
+
if (className) e.className = className;
|
|
885
|
+
return e;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function dot(variant) {
|
|
889
|
+
const d = el("span", `settings__dot settings__dot--${variant}`);
|
|
890
|
+
return d;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function sectionTitle(text) {
|
|
894
|
+
const h = el("h3", "settings__section-title");
|
|
895
|
+
h.textContent = text;
|
|
896
|
+
return h;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function subsectionTitle(text) {
|
|
900
|
+
const h = el("h4", "settings__subsection-title");
|
|
901
|
+
h.textContent = text;
|
|
902
|
+
return h;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function escSettings(str) {
|
|
906
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ---------------------------------------------------------------------------
|
|
910
|
+
// Event listeners
|
|
911
|
+
// ---------------------------------------------------------------------------
|
|
912
|
+
|
|
913
|
+
document.getElementById("btn-settings").addEventListener("click", openSettings);
|
|
914
|
+
document.getElementById("settings-close").addEventListener("click", closeSettings);
|
|
915
|
+
document.getElementById("settings-overlay").addEventListener("click", (e) => {
|
|
916
|
+
if (e.target.id === "settings-overlay") closeSettings();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// Setup screen settings button
|
|
920
|
+
document.getElementById("btn-setup-settings").addEventListener("click", openSettings);
|
|
921
|
+
|
|
922
|
+
// Escape key
|
|
923
|
+
document.addEventListener("keydown", (e) => {
|
|
924
|
+
if (e.key === "Escape" && !document.getElementById("settings-overlay").classList.contains("hidden")) {
|
|
925
|
+
closeSettings();
|
|
926
|
+
}
|
|
927
|
+
});
|