vibespot 0.9.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibespot",
3
- "version": "0.9.4",
3
+ "version": "1.0.0",
4
4
  "description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
5
5
  "type": "module",
6
6
  "bin": {
package/ui/chat.js CHANGED
@@ -101,7 +101,7 @@ function handleWsMessage(msg) {
101
101
  if (m.role === "user") {
102
102
  appendUserMessage(m.content, m.timestamp);
103
103
  } else if (m.role === "assistant") {
104
- appendRestoredAssistantMessage(m.content, m.timestamp);
104
+ appendRestoredAssistantMessage(m.content, m.timestamp, m.pipeline);
105
105
  }
106
106
  }
107
107
  scrollToBottom();
@@ -143,16 +143,294 @@ function handleWsMessage(msg) {
143
143
  break;
144
144
 
145
145
  case "error":
146
+ stopPipelineTimer();
146
147
  finishStreaming();
148
+ pipelineBubbleEl = null;
149
+ pipelineStepsEl = null;
150
+ pipelineModulesEl = null;
147
151
  appendAssistantError(msg.message);
148
152
  setStatus("Error");
149
153
  break;
150
154
 
151
155
  case "pong":
152
156
  break;
157
+
158
+ // --- Agentic pipeline events ---
159
+ case "agent_step":
160
+ handleAgentStep(msg);
161
+ break;
162
+ case "agent_decision":
163
+ handleAgentDecision(msg);
164
+ break;
165
+ case "module_progress":
166
+ handleModuleProgress(msg);
167
+ break;
168
+ case "pipeline_complete":
169
+ handlePipelineComplete(msg);
170
+ break;
171
+ case "pipeline_partial":
172
+ handlePipelinePartial(msg);
173
+ break;
174
+ case "agentic_prompt":
175
+ handleAgenticPrompt();
176
+ break;
153
177
  }
154
178
  }
155
179
 
180
+ // ---------------------------------------------------------------------------
181
+ // Agentic pipeline UI
182
+ // ---------------------------------------------------------------------------
183
+
184
+ const STEP_LABELS = {
185
+ analyzing: "Analyzing",
186
+ designing: "Designing",
187
+ developing: "Developing",
188
+ quality_check: "Quality Check",
189
+ };
190
+ const STEP_ORDER = ["analyzing", "designing", "developing", "quality_check"];
191
+
192
+ let pipelineBubbleEl = null;
193
+ let pipelineStepsEl = null;
194
+ let pipelineModulesEl = null;
195
+
196
+ function ensurePipelineBubble() {
197
+ if (pipelineBubbleEl) return;
198
+
199
+ if (!isStreaming) {
200
+ isStreaming = true;
201
+ sendBtn.disabled = true;
202
+ streamStartTime = Date.now();
203
+ if (typeof showGeneratingPreview === "function") showGeneratingPreview();
204
+ }
205
+
206
+ // If startStreaming() already created an empty bubble, repurpose it
207
+ if (streamingMsgEl) {
208
+ const existingDiv = streamingMsgEl.closest(".chat-msg");
209
+ if (existingDiv) {
210
+ streamingMsgEl.innerHTML = `
211
+ <div class="pipeline-steps"></div>
212
+ <div class="pipeline-modules"></div>
213
+ <div class="pipeline-timer"></div>`;
214
+ pipelineBubbleEl = existingDiv;
215
+ pipelineStepsEl = streamingMsgEl.querySelector(".pipeline-steps");
216
+ pipelineModulesEl = streamingMsgEl.querySelector(".pipeline-modules");
217
+ startPipelineTimer(streamingMsgEl.querySelector(".pipeline-timer"));
218
+ scrollToBottom();
219
+ return;
220
+ }
221
+ }
222
+
223
+ const time = formatMessageTime(Date.now());
224
+ const div = document.createElement("div");
225
+ div.className = "chat-msg chat-msg--assistant chat-msg--streaming";
226
+ div.innerHTML = `
227
+ <div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
228
+ <div class="chat-msg__content">
229
+ <div class="chat-msg__header">
230
+ <span class="chat-msg__sender">vibeSpot AI</span>
231
+ <span class="chat-msg__time">${time}</span>
232
+ </div>
233
+ <div class="chat-msg__bubble">
234
+ <div class="pipeline-steps"></div>
235
+ <div class="pipeline-modules"></div>
236
+ <div class="pipeline-timer"></div>
237
+ </div>
238
+ </div>`;
239
+ messagesEl.appendChild(div);
240
+
241
+ pipelineBubbleEl = div;
242
+ pipelineStepsEl = div.querySelector(".pipeline-steps");
243
+ pipelineModulesEl = div.querySelector(".pipeline-modules");
244
+ streamingMsgEl = div.querySelector(".chat-msg__bubble");
245
+ startPipelineTimer(div.querySelector(".pipeline-timer"));
246
+ scrollToBottom();
247
+ }
248
+
249
+ function handleAgentStep(msg) {
250
+ ensurePipelineBubble();
251
+
252
+ // Mark previous steps as done
253
+ const existing = pipelineStepsEl.querySelectorAll(".pipeline-step");
254
+ existing.forEach((el) => {
255
+ if (!el.classList.contains("pipeline-step--done")) {
256
+ el.classList.add("pipeline-step--done");
257
+ el.classList.remove("pipeline-step--active");
258
+ }
259
+ });
260
+
261
+ // Add new step
262
+ const step = document.createElement("div");
263
+ step.className = "pipeline-step pipeline-step--active";
264
+ step.dataset.step = msg.step;
265
+ step.innerHTML = `<span class="pipeline-step__icon">⟳</span> <span class="pipeline-step__label">${msg.label || STEP_LABELS[msg.step] || msg.step}</span>`;
266
+
267
+ // Insert quality_check AFTER module cards so the visual order is:
268
+ // developing → module cards → quality check
269
+ if (msg.step === "quality_check" && pipelineModulesEl) {
270
+ pipelineModulesEl.after(step);
271
+ } else {
272
+ pipelineStepsEl.appendChild(step);
273
+ }
274
+
275
+ // Clear module cards when entering developing
276
+ if (msg.step === "developing") {
277
+ pipelineModulesEl.innerHTML = "";
278
+ }
279
+
280
+ scrollToBottom();
281
+ }
282
+
283
+ function handleAgentDecision(msg) {
284
+ if (!pipelineBubbleEl) return;
285
+
286
+ // Find the step element — may be inside pipelineStepsEl or after pipelineModulesEl
287
+ const bubble = streamingMsgEl || pipelineBubbleEl;
288
+ const steps = bubble.querySelectorAll(".pipeline-step");
289
+ const lastStep = steps[steps.length - 1];
290
+ if (lastStep) {
291
+ const detail = document.createElement("div");
292
+ detail.className = "pipeline-step__decision";
293
+ detail.textContent = msg.decision;
294
+ lastStep.appendChild(detail);
295
+ }
296
+ scrollToBottom();
297
+ }
298
+
299
+ function handleModuleProgress(msg) {
300
+ ensurePipelineBubble();
301
+
302
+ let card = pipelineModulesEl.querySelector(`[data-module="${CSS.escape(msg.module)}"]`);
303
+ if (!card) {
304
+ card = document.createElement("div");
305
+ card.className = "pipeline-module-card";
306
+ card.dataset.module = msg.module;
307
+ card.innerHTML = `<span class="pipeline-module-card__name">${escapeHtml(msg.module)}</span> <span class="pipeline-module-card__status"></span>`;
308
+ pipelineModulesEl.appendChild(card);
309
+ }
310
+
311
+ const statusEl = card.querySelector(".pipeline-module-card__status");
312
+ card.className = "pipeline-module-card pipeline-module-card--" + msg.status;
313
+
314
+ const statusLabels = {
315
+ queued: "queued",
316
+ generating: "generating...",
317
+ validating: "validating...",
318
+ retrying: "retrying...",
319
+ complete: "✓",
320
+ failed: "✗",
321
+ };
322
+ statusEl.textContent = statusLabels[msg.status] || msg.status;
323
+ scrollToBottom();
324
+ }
325
+
326
+ function handlePipelineComplete(msg) {
327
+ if (!pipelineBubbleEl) return;
328
+
329
+ stopPipelineTimer();
330
+
331
+ // Remove the live timer element
332
+ const timerEl = pipelineBubbleEl.querySelector(".pipeline-timer");
333
+ if (timerEl) timerEl.remove();
334
+
335
+ // Mark all steps as done (search whole bubble since quality_check is outside pipelineStepsEl)
336
+ const bubble = streamingMsgEl || pipelineBubbleEl;
337
+ bubble.querySelectorAll(".pipeline-step").forEach((el) => {
338
+ el.classList.add("pipeline-step--done");
339
+ el.classList.remove("pipeline-step--active");
340
+ });
341
+
342
+ // Add completion stats after the last element in the bubble
343
+ const stats = document.createElement("div");
344
+ stats.className = "pipeline-stats";
345
+ const duration = formatDuration(msg.durationMs);
346
+ stats.textContent = `Generated ${msg.modulesGenerated} module${msg.modulesGenerated === 1 ? "" : "s"} in ${duration}`;
347
+ if (msg.modulesUnchanged > 0) {
348
+ stats.textContent += ` (${msg.modulesUnchanged} unchanged)`;
349
+ }
350
+ // Place stats after quality_check step (or after modules if no quality step)
351
+ const qualityStep = bubble.querySelector('[data-step="quality_check"]');
352
+ if (qualityStep) {
353
+ qualityStep.after(stats);
354
+ } else if (pipelineModulesEl) {
355
+ pipelineModulesEl.after(stats);
356
+ } else {
357
+ pipelineStepsEl.appendChild(stats);
358
+ }
359
+
360
+ clearStreamStatus();
361
+ finishStreaming();
362
+
363
+ // Reset pipeline state
364
+ pipelineBubbleEl = null;
365
+ pipelineStepsEl = null;
366
+ pipelineModulesEl = null;
367
+ }
368
+
369
+ function handlePipelinePartial(msg) {
370
+ if (!pipelineBubbleEl) return;
371
+
372
+ stopPipelineTimer();
373
+
374
+ const timerEl = pipelineBubbleEl.querySelector(".pipeline-timer");
375
+ if (timerEl) timerEl.remove();
376
+
377
+ const bubble = streamingMsgEl || pipelineBubbleEl;
378
+ bubble.querySelectorAll(".pipeline-step").forEach((el) => {
379
+ el.classList.add("pipeline-step--done");
380
+ el.classList.remove("pipeline-step--active");
381
+ });
382
+
383
+ const stats = document.createElement("div");
384
+ stats.className = "pipeline-stats pipeline-stats--partial";
385
+ const duration = formatDuration(msg.durationMs);
386
+ stats.textContent = `${msg.succeeded.length} modules succeeded, ${msg.failed.length} failed in ${duration}`;
387
+ const qualityStep = bubble.querySelector('[data-step="quality_check"]');
388
+ if (qualityStep) {
389
+ qualityStep.after(stats);
390
+ } else if (pipelineModulesEl) {
391
+ pipelineModulesEl.after(stats);
392
+ } else {
393
+ pipelineStepsEl.appendChild(stats);
394
+ }
395
+
396
+ clearStreamStatus();
397
+ finishStreaming();
398
+
399
+ pipelineBubbleEl = null;
400
+ pipelineStepsEl = null;
401
+ pipelineModulesEl = null;
402
+ }
403
+
404
+ async function handleAgenticPrompt() {
405
+ // First-run onboarding: show dialog explaining agentic mode
406
+ if (typeof vibeConfirm !== "function") return;
407
+
408
+ const agreed = await vibeConfirm(
409
+ "Agentic Pipeline Available",
410
+ "vibeSpot can decompose AI generation into specialized agents:\n\n" +
411
+ "• Intent Analyzer — classifies your request\n" +
412
+ "• Page Architect — designs the page structure\n" +
413
+ "• Module Developer — generates each module in parallel\n" +
414
+ "• Validator — checks and auto-fixes errors\n\n" +
415
+ "Tradeoffs:\n" +
416
+ "✓ Better quality — each agent is focused on one task\n" +
417
+ "✓ Structured output — eliminates JSON parsing failures\n" +
418
+ "✓ Only changed modules regenerated on edits\n" +
419
+ "✗ Uses more calls per request (API calls or CLI subprocess calls)\n\n" +
420
+ "You can change this anytime in Settings.",
421
+ "Use Agentic Pipeline",
422
+ "Keep Single-Call",
423
+ );
424
+
425
+ try {
426
+ await fetch("/api/settings", {
427
+ method: "POST",
428
+ headers: { "Content-Type": "application/json" },
429
+ body: JSON.stringify({ agenticMode: agreed }),
430
+ });
431
+ } catch { /* ignore */ }
432
+ }
433
+
156
434
  // ---------------------------------------------------------------------------
157
435
  // File attachments
158
436
  // ---------------------------------------------------------------------------
@@ -480,11 +758,34 @@ function stopStreamTimer() {
480
758
  function formatDuration(ms) {
481
759
  const totalSec = Math.floor(ms / 1000);
482
760
  if (totalSec < 60) return totalSec + "s";
483
- const min = Math.floor(totalSec / 60);
761
+ const hours = Math.floor(totalSec / 3600);
762
+ const min = Math.floor((totalSec % 3600) / 60);
484
763
  const sec = totalSec % 60;
764
+ if (hours > 0) {
765
+ return hours + "h " + (min < 10 ? "0" : "") + min + "m " + (sec < 10 ? "0" : "") + sec + "s";
766
+ }
485
767
  return min + "m " + (sec < 10 ? "0" : "") + sec + "s";
486
768
  }
487
769
 
770
+ let pipelineTimerInterval = null;
771
+
772
+ function startPipelineTimer(timerEl) {
773
+ stopPipelineTimer();
774
+ if (!timerEl) return;
775
+ const update = () => {
776
+ timerEl.textContent = formatDuration(Date.now() - streamStartTime);
777
+ };
778
+ update();
779
+ pipelineTimerInterval = setInterval(update, 1000);
780
+ }
781
+
782
+ function stopPipelineTimer() {
783
+ if (pipelineTimerInterval) {
784
+ clearInterval(pipelineTimerInterval);
785
+ pipelineTimerInterval = null;
786
+ }
787
+ }
788
+
488
789
  function clearStreamStatus() {
489
790
  if (!streamingMsgEl) return;
490
791
  const statusEl = streamingMsgEl.querySelector(".stream-status");
@@ -558,6 +859,9 @@ function renderMarkdown(text) {
558
859
  // Also strip unclosed code fences (truncated responses)
559
860
  text = text.replace(/```[\s\S]*$/g, "");
560
861
 
862
+ // Escape HTML to prevent XSS from AI/user content
863
+ text = escapeHtml(text);
864
+
561
865
  // Inline code: `...`
562
866
  text = text.replace(/`([^`]+)`/g, "<code>$1</code>");
563
867
 
@@ -614,16 +918,52 @@ function setStatus(text) {
614
918
  // Restored / system messages
615
919
  // ---------------------------------------------------------------------------
616
920
 
617
- function appendRestoredAssistantMessage(text, timestamp) {
921
+ function appendRestoredAssistantMessage(text, timestamp, pipeline) {
618
922
  const time = formatMessageTime(timestamp);
619
923
  const div = document.createElement("div");
620
924
  div.className = "chat-msg chat-msg--assistant";
621
- div.innerHTML = `
622
- <div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
623
- <div class="chat-msg__content">
624
- ${time ? `<div class="chat-msg__header"><span class="chat-msg__sender">vibeSpot AI</span><span class="chat-msg__time">${time}</span></div>` : ""}
625
- <div class="chat-msg__bubble">${renderMarkdown(text)}</div>
626
- </div>`;
925
+
926
+ if (pipeline && pipeline.steps && pipeline.steps.length > 0) {
927
+ // Render detailed pipeline structure
928
+ const stepsHtml = pipeline.steps.map((s) => {
929
+ const icon = "&#x2714;";
930
+ const decisionsHtml = (s.decisions || [])
931
+ .map((d) => `<div class="pipeline-step__decision">${escapeHtml(d)}</div>`)
932
+ .join("");
933
+ return `<div class="pipeline-step pipeline-step--done"><span class="pipeline-step__icon">${icon}</span> <span class="pipeline-step__label">${escapeHtml(s.label)}</span>${decisionsHtml}</div>`;
934
+ }).join("");
935
+
936
+ const modulesHtml = pipeline.modules && pipeline.modules.length > 0
937
+ ? pipeline.modules.map((m) => {
938
+ const statusClass = m.status === "failed" ? "pipeline-module-card--failed" : "pipeline-module-card--done";
939
+ const statusIcon = m.status === "failed" ? "&#x2718;" : "&#x2714;";
940
+ return `<div class="pipeline-module-card ${statusClass}">${statusIcon} ${escapeHtml(m.name)}</div>`;
941
+ }).join("")
942
+ : "";
943
+
944
+ const duration = formatDuration(pipeline.stats.durationMs);
945
+ let statsText = `Generated ${pipeline.stats.modulesGenerated} module${pipeline.stats.modulesGenerated === 1 ? "" : "s"} in ${duration}`;
946
+ if (pipeline.stats.modulesUnchanged > 0) {
947
+ statsText += ` (${pipeline.stats.modulesUnchanged} unchanged)`;
948
+ }
949
+ const statsClass = pipeline.stats.modulesFailed > 0 ? "pipeline-stats pipeline-stats--partial" : "pipeline-stats";
950
+
951
+ div.innerHTML = `
952
+ <div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
953
+ <div class="chat-msg__content">
954
+ ${time ? `<div class="chat-msg__header"><span class="chat-msg__sender">vibeSpot AI</span><span class="chat-msg__time">${time}</span></div>` : ""}
955
+ <div class="chat-msg__bubble">
956
+ <div class="pipeline-steps">${stepsHtml}${modulesHtml ? `<div class="pipeline-modules-restored">${modulesHtml}</div>` : ""}<div class="${statsClass}">${statsText}</div></div>
957
+ </div>
958
+ </div>`;
959
+ } else {
960
+ div.innerHTML = `
961
+ <div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
962
+ <div class="chat-msg__content">
963
+ ${time ? `<div class="chat-msg__header"><span class="chat-msg__sender">vibeSpot AI</span><span class="chat-msg__time">${time}</span></div>` : ""}
964
+ <div class="chat-msg__bubble">${renderMarkdown(text)}</div>
965
+ </div>`;
966
+ }
627
967
  messagesEl.appendChild(div);
628
968
  }
629
969
 
package/ui/dashboard.js CHANGED
@@ -293,30 +293,11 @@ function renderBrandAssets(assets) {
293
293
  bvIcon.classList.remove("brand-asset-upload__icon--done");
294
294
  }
295
295
 
296
- // View buttons show only when asset exists
297
- let sgView = document.getElementById("btn-view-styleguide");
298
- if (!sgView) {
299
- sgView = document.createElement("button");
300
- sgView.id = "btn-view-styleguide";
301
- sgView.className = "btn btn--sm btn--ghost brand-asset-view";
302
- sgView.textContent = "View";
303
- sgView.title = "View styleguide";
304
- sgView.addEventListener("click", viewStyleguide);
305
- document.getElementById("brand-upload-styleguide")?.after(sgView);
306
- }
307
- sgView.style.display = assets.hasStyleguide ? "" : "none";
308
-
309
- let bvView = document.getElementById("btn-view-brandvoice");
310
- if (!bvView) {
311
- bvView = document.createElement("button");
312
- bvView.id = "btn-view-brandvoice";
313
- bvView.className = "btn btn--sm btn--ghost brand-asset-view";
314
- bvView.textContent = "View";
315
- bvView.title = "View brand voice";
316
- bvView.addEventListener("click", viewBrandvoice);
317
- document.getElementById("brand-upload-brandvoice")?.after(bvView);
318
- }
319
- bvView.style.display = assets.hasBrandvoice ? "" : "none";
296
+ // Show/hide action buttons based on asset existence
297
+ const sgActions = document.getElementById("brand-actions-styleguide");
298
+ if (sgActions) sgActions.classList.toggle("hidden", !assets.hasStyleguide);
299
+ const bvActions = document.getElementById("brand-actions-brandvoice");
300
+ if (bvActions) bvActions.classList.toggle("hidden", !assets.hasBrandvoice);
320
301
 
321
302
  // Humanify toggle
322
303
  const humanifyCheckbox = document.getElementById("humanify-checkbox");
@@ -353,6 +334,29 @@ async function viewBrandvoice() {
353
334
  }
354
335
  }
355
336
 
337
+ async function deleteBrandAsset(type) {
338
+ const label = type === "styleguide" ? "styleguide" : "brand voice";
339
+ const ok = await vibeConfirm(`Remove ${label}?`, "This will delete the file from disk.", { confirmLabel: "Remove", confirmClass: "btn--danger" });
340
+ if (!ok) return;
341
+ try {
342
+ const res = await fetch("/api/brand-assets", {
343
+ method: "DELETE",
344
+ headers: { "Content-Type": "application/json" },
345
+ body: JSON.stringify({ type }),
346
+ });
347
+ const data = await res.json();
348
+ if (data.ok) showDashboard(currentDashboardTheme);
349
+ else await vibeAlert(data.error || "Failed to remove", "Error");
350
+ } catch (err) {
351
+ await vibeAlert("Failed to remove: " + err.message, "Error");
352
+ }
353
+ }
354
+
355
+ document.getElementById("btn-view-styleguide")?.addEventListener("click", viewStyleguide);
356
+ document.getElementById("btn-view-brandvoice")?.addEventListener("click", viewBrandvoice);
357
+ document.getElementById("btn-delete-styleguide")?.addEventListener("click", () => deleteBrandAsset("styleguide"));
358
+ document.getElementById("btn-delete-brandvoice")?.addEventListener("click", () => deleteBrandAsset("brandvoice"));
359
+
356
360
  // ---------------------------------------------------------------------------
357
361
  // Actions
358
362
  // ---------------------------------------------------------------------------
package/ui/dialog.js CHANGED
@@ -231,7 +231,9 @@ function vibeViewContent(content, title, downloadFilename) {
231
231
 
232
232
  const body = document.createElement("div");
233
233
  body.className = "confirm-dialog__content-view md-body";
234
- body.innerHTML = typeof marked !== "undefined" ? marked.parse(content) : esc(content);
234
+ // Sanitize: escape HTML in content before markdown parsing to prevent XSS
235
+ const safeContent = esc(content);
236
+ body.innerHTML = typeof marked !== "undefined" ? marked.parse(safeContent) : safeContent;
235
237
  // Inject color swatches next to hex codes
236
238
  body.innerHTML = body.innerHTML.replace(
237
239
  /(#[0-9A-Fa-f]{3,8})(?![0-9A-Fa-f])/g,
package/ui/index.html CHANGED
@@ -205,16 +205,28 @@
205
205
  <span class="brand-asset-toggle__label">Humanify</span>
206
206
  <span class="brand-asset-toggle__tooltip" data-tooltip="Strips AI-sounding copy: removes em dashes, banned words like 'delve' and 'leverage', cliché openers, and forced enthusiasm. Makes your landing page read like a human wrote it.">?</span>
207
207
  </div>
208
- <label class="brand-asset-upload" id="brand-upload-styleguide">
209
- <input type="file" accept=".md,.txt" hidden>
210
- <span class="brand-asset-upload__icon" id="brand-icon-styleguide">+</span>
211
- <span class="brand-asset-upload__label">styleguide.md</span>
212
- </label>
213
- <label class="brand-asset-upload" id="brand-upload-brandvoice">
214
- <input type="file" accept=".md,.txt" hidden>
215
- <span class="brand-asset-upload__icon" id="brand-icon-brandvoice">+</span>
216
- <span class="brand-asset-upload__label">brandvoice.md</span>
217
- </label>
208
+ <div class="brand-asset-row">
209
+ <label class="brand-asset-upload" id="brand-upload-styleguide">
210
+ <input type="file" accept=".md,.txt" hidden>
211
+ <span class="brand-asset-upload__icon" id="brand-icon-styleguide">+</span>
212
+ <span class="brand-asset-upload__label">styleguide.md</span>
213
+ </label>
214
+ <span class="brand-asset-actions hidden" id="brand-actions-styleguide">
215
+ <button class="brand-asset-action" title="View" id="btn-view-styleguide"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
216
+ <button class="brand-asset-action brand-asset-action--delete" title="Remove" id="btn-delete-styleguide">&times;</button>
217
+ </span>
218
+ </div>
219
+ <div class="brand-asset-row">
220
+ <label class="brand-asset-upload" id="brand-upload-brandvoice">
221
+ <input type="file" accept=".md,.txt" hidden>
222
+ <span class="brand-asset-upload__icon" id="brand-icon-brandvoice">+</span>
223
+ <span class="brand-asset-upload__label">brandvoice.md</span>
224
+ </label>
225
+ <span class="brand-asset-actions hidden" id="brand-actions-brandvoice">
226
+ <button class="brand-asset-action" title="View" id="btn-view-brandvoice"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
227
+ <button class="brand-asset-action brand-asset-action--delete" title="Remove" id="btn-delete-brandvoice">&times;</button>
228
+ </span>
229
+ </div>
218
230
  <button class="btn btn--sm btn--outline" id="btn-extract-design" title="Auto-extract design system from this theme's CSS and modules">Extract from Theme</button>
219
231
  <button class="btn btn--sm btn--outline" id="btn-import-reference" title="Import design from another HubSpot theme or local folder">Import Reference</button>
220
232
  </div>
package/ui/settings.js CHANGED
@@ -160,6 +160,53 @@ function renderAITab(body, data) {
160
160
 
161
161
  body.appendChild(section);
162
162
 
163
+ // Agentic Pipeline section
164
+ const agenticSection = el("section", "settings__section");
165
+ agenticSection.appendChild(sectionTitle("Agentic Pipeline"));
166
+
167
+ const isCli = activeEngine && ["claude-code", "gemini-cli", "codex-cli"].includes(activeEngine);
168
+ const agenticMode = config.agenticMode;
169
+
170
+ if (isCli) {
171
+ agenticSection.appendChild(desc("Decompose AI generation into specialized agents with per-module parallel calls. Better quality and structured output enforcement. CLI engines use subprocess calls — may be slower than API engines."));
172
+ } else {
173
+ agenticSection.appendChild(desc("Decompose AI generation into specialized agents with per-module parallel calls. Better quality and structured output enforcement."));
174
+ }
175
+
176
+ const toggleRow = el("div", "settings__toggle-row");
177
+ const labelWrap = el("div", "");
178
+ const label = el("div", "settings__toggle-label");
179
+ label.textContent = "Enable Agentic Pipeline";
180
+ labelWrap.appendChild(label);
181
+
182
+ const sub = el("div", "settings__toggle-label-sub");
183
+ if (agenticMode === true) {
184
+ sub.textContent = "Active — multi-stage pipeline with parallel module generation";
185
+ sub.style.color = "var(--success)";
186
+ } else if (agenticMode === false) {
187
+ sub.textContent = "Disabled — using single-call mode";
188
+ sub.style.color = "var(--text-muted)";
189
+ } else {
190
+ sub.textContent = "Not configured — will be prompted on first generation";
191
+ sub.style.color = "var(--warning)";
192
+ }
193
+ labelWrap.appendChild(sub);
194
+ toggleRow.appendChild(labelWrap);
195
+
196
+ const toggle = el("button", "settings__toggle" + (agenticMode === true ? " active" : ""));
197
+ toggle.addEventListener("click", async () => {
198
+ const newVal = agenticMode !== true;
199
+ await fetch("/api/settings", {
200
+ method: "POST",
201
+ headers: { "Content-Type": "application/json" },
202
+ body: JSON.stringify({ agenticMode: newVal }),
203
+ });
204
+ refreshSettings();
205
+ });
206
+ toggleRow.appendChild(toggle);
207
+ agenticSection.appendChild(toggleRow);
208
+ body.appendChild(agenticSection);
209
+
163
210
  // API Keys section
164
211
  const keysSection = el("section", "settings__section");
165
212
  keysSection.appendChild(sectionTitle("API Keys"));
@@ -564,6 +611,25 @@ function renderVibeSpotTab(body, data) {
564
611
  const versionVal = el("span", "settings__card-meta");
565
612
  versionVal.textContent = data.version || "dev";
566
613
  versionRow.appendChild(versionVal);
614
+
615
+ const changelogBtn = el("button", "settings__btn settings__btn--small");
616
+ changelogBtn.textContent = "Changelog";
617
+ changelogBtn.style.marginLeft = "8px";
618
+ changelogBtn.addEventListener("click", async () => {
619
+ try {
620
+ const resp = await fetch("/api/changelog");
621
+ const json = await resp.json();
622
+ if (json.changelog) {
623
+ vibeViewContent(json.changelog, "Changelog");
624
+ } else {
625
+ vibeAlert("Changelog not available.");
626
+ }
627
+ } catch {
628
+ vibeAlert("Failed to load changelog.");
629
+ }
630
+ });
631
+ versionRow.appendChild(changelogBtn);
632
+
567
633
  card.appendChild(versionRow);
568
634
 
569
635
  // Workspace