vibespot 1.1.0 → 1.2.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/ui/preview.js CHANGED
@@ -4,10 +4,39 @@
4
4
 
5
5
  const previewFrame = document.getElementById("preview-frame");
6
6
 
7
+ // Highlights to apply once the iframe finishes loading after the next refresh.
8
+ let pendingChangedModules = null;
9
+ let pendingNewModules = null;
10
+
11
+ previewFrame.addEventListener("load", () => {
12
+ if (!pendingChangedModules && !pendingNewModules) return;
13
+ const changed = pendingChangedModules;
14
+ const fresh = pendingNewModules;
15
+ pendingChangedModules = null;
16
+ pendingNewModules = null;
17
+ try {
18
+ const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
19
+ if (!doc || !doc.body) return;
20
+ ensureChangeHighlightStyles(doc);
21
+ if (fresh && fresh.length) animateNewModules(doc, fresh);
22
+ if (changed && changed.length) highlightChangedModules(doc, changed, fresh || []);
23
+ } catch {
24
+ // cross-origin — skip
25
+ }
26
+ });
27
+
7
28
  /**
8
29
  * Refresh the preview iframe by reloading from /preview endpoint.
30
+ *
31
+ * @param {Object} [opts]
32
+ * @param {string[]} [opts.changedModules] Module names that were just regenerated.
33
+ * @param {string[]} [opts.newModules] Subset of changedModules that are first-time additions.
9
34
  */
10
- function refreshPreview() {
35
+ function refreshPreview(opts) {
36
+ if (opts && (opts.changedModules || opts.newModules)) {
37
+ pendingChangedModules = opts.changedModules || null;
38
+ pendingNewModules = opts.newModules || null;
39
+ }
11
40
  // Use srcdoc approach: fetch preview HTML and set as srcdoc
12
41
  // This avoids cache issues and allows the iframe to update smoothly
13
42
  fetch("/preview")
@@ -20,6 +49,79 @@ function refreshPreview() {
20
49
  });
21
50
  }
22
51
 
52
+ // ---------------------------------------------------------------------------
53
+ // Change highlighting — outline glow on regenerated modules and slide-in for
54
+ // brand-new modules. Styles are injected into the sandboxed preview iframe.
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function ensureChangeHighlightStyles(doc) {
58
+ if (doc.getElementById("vibespot-change-highlight-css")) return;
59
+ const style = doc.createElement("style");
60
+ style.id = "vibespot-change-highlight-css";
61
+ style.textContent = `
62
+ @keyframes vibespot-change-glow {
63
+ 0% { outline-color: rgba(232, 97, 58, 0.85); box-shadow: 0 0 0 6px rgba(232, 97, 58, 0.18); }
64
+ 70% { outline-color: rgba(232, 97, 58, 0.55); box-shadow: 0 0 0 4px rgba(232, 97, 58, 0.10); }
65
+ 100% { outline-color: rgba(232, 97, 58, 0); box-shadow: 0 0 0 0 rgba(232, 97, 58, 0); }
66
+ }
67
+ .vibespot-module--changed {
68
+ outline: 2px solid rgba(232, 97, 58, 0.85);
69
+ outline-offset: 4px;
70
+ border-radius: 2px;
71
+ animation: vibespot-change-glow 2s ease-out forwards;
72
+ }
73
+ @keyframes vibespot-module-slide-in {
74
+ 0% { opacity: 0; transform: translateY(24px); }
75
+ 100% { opacity: 1; transform: translateY(0); }
76
+ }
77
+ .vibespot-module--new {
78
+ animation: vibespot-module-slide-in 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both;
79
+ }
80
+ @media (prefers-reduced-motion: reduce) {
81
+ .vibespot-module--changed,
82
+ .vibespot-module--new { animation: none; }
83
+ .vibespot-module--changed { outline-color: transparent; }
84
+ }
85
+ `;
86
+ doc.head.appendChild(style);
87
+ }
88
+
89
+ function highlightChangedModules(doc, moduleNames, newModuleNames) {
90
+ // Skip modules that are already getting the slide-in animation — the slide-in
91
+ // is a stronger signal on its own.
92
+ const newSet = new Set(newModuleNames);
93
+ for (const name of moduleNames) {
94
+ if (newSet.has(name)) continue;
95
+ const el = doc.querySelector(`[data-module="${cssEscape(name)}"]`);
96
+ if (!el) continue;
97
+ el.classList.remove("vibespot-module--changed");
98
+ // Force a reflow so the animation restarts if the class was just removed.
99
+ void el.offsetWidth;
100
+ el.classList.add("vibespot-module--changed");
101
+ setTimeout(() => {
102
+ el.classList.remove("vibespot-module--changed");
103
+ }, 2200);
104
+ }
105
+ }
106
+
107
+ function animateNewModules(doc, moduleNames) {
108
+ for (const name of moduleNames) {
109
+ const el = doc.querySelector(`[data-module="${cssEscape(name)}"]`);
110
+ if (!el) continue;
111
+ el.classList.add("vibespot-module--new");
112
+ setTimeout(() => {
113
+ el.classList.remove("vibespot-module--new");
114
+ }, 700);
115
+ }
116
+ }
117
+
118
+ function cssEscape(value) {
119
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
120
+ return CSS.escape(value);
121
+ }
122
+ return String(value).replace(/["\\]/g, "\\$&");
123
+ }
124
+
23
125
  /**
24
126
  * Scroll the preview iframe to a specific module by name.
25
127
  */
@@ -278,3 +380,216 @@ function clearAllModulesWorking() {
278
380
 
279
381
  // Preview refresh is triggered by setup.js after a session is created.
280
382
  // Do NOT auto-refresh here.
383
+
384
+ // ---------------------------------------------------------------------------
385
+ // Select mode — click an element in the preview to reference it in chat
386
+ // ---------------------------------------------------------------------------
387
+
388
+ const selectModeBtn = document.getElementById("select-mode-toggle");
389
+ const previewContainer = document.getElementById("preview-container");
390
+
391
+ let selectModeActive = false;
392
+ let hoveredEl = null;
393
+ let hoverLabelEl = null;
394
+ let onMouseOver = null;
395
+ let onMouseOut = null;
396
+ let onClick = null;
397
+ let onKeyDown = null;
398
+
399
+ function ensureSelectModeStyles(doc) {
400
+ if (doc.getElementById("vibespot-select-css")) return;
401
+ const style = doc.createElement("style");
402
+ style.id = "vibespot-select-css";
403
+ style.textContent = `
404
+ html.vibespot-select-mode, html.vibespot-select-mode * { cursor: crosshair !important; }
405
+ html.vibespot-select-mode a, html.vibespot-select-mode button { pointer-events: none; }
406
+ .vibespot-select-hover {
407
+ outline: 2px solid #e8613a !important;
408
+ outline-offset: 2px !important;
409
+ background-color: rgba(232, 97, 58, 0.08) !important;
410
+ }
411
+ .vibespot-select-label {
412
+ position: fixed;
413
+ z-index: 2147483647;
414
+ pointer-events: none;
415
+ font: 500 11px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
416
+ color: #fff;
417
+ background: #e8613a;
418
+ padding: 3px 8px;
419
+ border-radius: 4px;
420
+ box-shadow: 0 2px 6px rgba(0,0,0,0.25);
421
+ white-space: nowrap;
422
+ max-width: 70vw;
423
+ overflow: hidden;
424
+ text-overflow: ellipsis;
425
+ }
426
+ `;
427
+ doc.head.appendChild(style);
428
+ }
429
+
430
+ function describeElement(el) {
431
+ if (!el) return "";
432
+ const moduleEl = el.closest("[data-module]");
433
+ const moduleName = moduleEl ? moduleEl.getAttribute("data-module") : null;
434
+ const tag = (el.tagName || "").toLowerCase();
435
+ let kind = tag;
436
+ if (tag.match(/^h[1-6]$/)) kind = "headline";
437
+ else if (tag === "p") kind = "paragraph";
438
+ else if (tag === "a") kind = "link";
439
+ else if (tag === "button") kind = "button";
440
+ else if (tag === "img") kind = "image";
441
+ else if (tag === "ul" || tag === "ol") kind = "list";
442
+ else if (tag === "li") kind = "list item";
443
+
444
+ if (moduleName && moduleEl === el) return moduleName;
445
+ if (moduleName) return `${moduleName} > ${kind}`;
446
+ return kind;
447
+ }
448
+
449
+ function buildChatPrefill(el) {
450
+ if (!el) return "";
451
+ const moduleEl = el.closest("[data-module]");
452
+ const moduleName = moduleEl ? moduleEl.getAttribute("data-module") : null;
453
+ const tag = (el.tagName || "").toLowerCase();
454
+ const text = (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 80);
455
+
456
+ let elementPart;
457
+ if (tag.match(/^h[1-6]$/)) elementPart = "the headline";
458
+ else if (tag === "p") elementPart = "the paragraph";
459
+ else if (tag === "a") elementPart = "the link";
460
+ else if (tag === "button") elementPart = "the button";
461
+ else if (tag === "img") elementPart = "the image";
462
+ else elementPart = `the ${tag}`;
463
+
464
+ if (moduleEl === el && moduleName) {
465
+ return `In the ${moduleName} section, `;
466
+ }
467
+ if (moduleName) {
468
+ const quote = text ? ` ("${text}${text.length === 80 ? "…" : ""}")` : "";
469
+ return `In the ${moduleName} section, ${elementPart}${quote} `;
470
+ }
471
+ return `${elementPart} `;
472
+ }
473
+
474
+ function clearHover(doc) {
475
+ if (hoveredEl) {
476
+ hoveredEl.classList.remove("vibespot-select-hover");
477
+ hoveredEl = null;
478
+ }
479
+ if (hoverLabelEl && hoverLabelEl.parentNode) {
480
+ hoverLabelEl.parentNode.removeChild(hoverLabelEl);
481
+ }
482
+ hoverLabelEl = null;
483
+ }
484
+
485
+ function attachSelectHandlers() {
486
+ let doc;
487
+ try {
488
+ doc = previewFrame.contentDocument || previewFrame.contentWindow?.document;
489
+ } catch { return; }
490
+ if (!doc || !doc.body) return;
491
+
492
+ ensureSelectModeStyles(doc);
493
+ doc.documentElement.classList.add("vibespot-select-mode");
494
+
495
+ onMouseOver = (e) => {
496
+ const el = e.target;
497
+ if (!el || el === doc.body || el === doc.documentElement) return;
498
+ if (el.classList && el.classList.contains("vibespot-select-label")) return;
499
+ if (hoveredEl === el) return;
500
+ if (hoveredEl) hoveredEl.classList.remove("vibespot-select-hover");
501
+ hoveredEl = el;
502
+ el.classList.add("vibespot-select-hover");
503
+
504
+ if (!hoverLabelEl) {
505
+ hoverLabelEl = doc.createElement("div");
506
+ hoverLabelEl.className = "vibespot-select-label";
507
+ doc.body.appendChild(hoverLabelEl);
508
+ }
509
+ hoverLabelEl.textContent = describeElement(el);
510
+ const rect = el.getBoundingClientRect();
511
+ hoverLabelEl.style.top = Math.max(4, rect.top - 22) + "px";
512
+ hoverLabelEl.style.left = Math.max(4, rect.left) + "px";
513
+ };
514
+
515
+ onMouseOut = (e) => {
516
+ if (!e.relatedTarget) clearHover(doc);
517
+ };
518
+
519
+ onClick = (e) => {
520
+ e.preventDefault();
521
+ e.stopPropagation();
522
+ const prefill = buildChatPrefill(e.target);
523
+ deactivateSelectMode();
524
+ if (typeof window.prefillChatInput === "function") {
525
+ window.prefillChatInput(prefill);
526
+ }
527
+ };
528
+
529
+ onKeyDown = (e) => {
530
+ if (e.key === "Escape") deactivateSelectMode();
531
+ };
532
+
533
+ doc.addEventListener("mouseover", onMouseOver, true);
534
+ doc.addEventListener("mouseout", onMouseOut, true);
535
+ doc.addEventListener("click", onClick, true);
536
+ doc.addEventListener("keydown", onKeyDown, true);
537
+ }
538
+
539
+ function detachSelectHandlers() {
540
+ let doc;
541
+ try {
542
+ doc = previewFrame.contentDocument || previewFrame.contentWindow?.document;
543
+ } catch { return; }
544
+ if (!doc) return;
545
+ doc.documentElement.classList.remove("vibespot-select-mode");
546
+ clearHover(doc);
547
+ if (onMouseOver) doc.removeEventListener("mouseover", onMouseOver, true);
548
+ if (onMouseOut) doc.removeEventListener("mouseout", onMouseOut, true);
549
+ if (onClick) doc.removeEventListener("click", onClick, true);
550
+ if (onKeyDown) doc.removeEventListener("keydown", onKeyDown, true);
551
+ onMouseOver = onMouseOut = onClick = onKeyDown = null;
552
+ }
553
+
554
+ function activateSelectMode() {
555
+ if (selectModeActive) return;
556
+ selectModeActive = true;
557
+ if (selectModeBtn) selectModeBtn.setAttribute("aria-pressed", "true");
558
+ if (previewContainer) previewContainer.classList.add("preview--select-mode");
559
+ attachSelectHandlers();
560
+ }
561
+
562
+ function deactivateSelectMode() {
563
+ if (!selectModeActive) return;
564
+ selectModeActive = false;
565
+ if (selectModeBtn) selectModeBtn.setAttribute("aria-pressed", "false");
566
+ if (previewContainer) previewContainer.classList.remove("preview--select-mode");
567
+ detachSelectHandlers();
568
+ }
569
+
570
+ function setSelectModeDisabled(disabled) {
571
+ if (!selectModeBtn) return;
572
+ if (disabled) {
573
+ deactivateSelectMode();
574
+ selectModeBtn.disabled = true;
575
+ } else {
576
+ selectModeBtn.disabled = false;
577
+ }
578
+ }
579
+
580
+ if (selectModeBtn) {
581
+ selectModeBtn.addEventListener("click", () => {
582
+ if (selectModeBtn.disabled) return;
583
+ if (selectModeActive) deactivateSelectMode();
584
+ else activateSelectMode();
585
+ });
586
+ }
587
+
588
+ if (previewFrame) {
589
+ previewFrame.addEventListener("load", () => {
590
+ if (selectModeActive) attachSelectHandlers();
591
+ });
592
+ }
593
+
594
+ window.setSelectModeDisabled = setSelectModeDisabled;
595
+ window.deactivateSelectMode = deactivateSelectMode;
package/ui/settings.js CHANGED
@@ -100,7 +100,7 @@ function renderAITab(body, data) {
100
100
  // Engine selector
101
101
  const section = el("section", "settings__section");
102
102
  section.appendChild(sectionTitle("Engine"));
103
- section.appendChild(desc("Choose which AI engine generates your HubSpot modules. API engines need an API key. CLI engines need the tool installed on your system."));
103
+ section.appendChild(desc("Choose which AI engine generates your HubSpot sections. API engines need an API key. CLI engines need the tool installed on your system."));
104
104
 
105
105
  const selectEl = el("div", "settings__engine-select");
106
106
  const allEngines = [
@@ -176,9 +176,9 @@ function renderAITab(body, data) {
176
176
  const agenticMode = config.agenticMode;
177
177
 
178
178
  if (isCli) {
179
- 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."));
179
+ agenticSection.appendChild(desc("Decompose AI generation into specialized agents with per-section parallel calls. Better quality and structured output enforcement. CLI engines use subprocess calls — may be slower than API engines."));
180
180
  } else {
181
- agenticSection.appendChild(desc("Decompose AI generation into specialized agents with per-module parallel calls. Better quality and structured output enforcement."));
181
+ agenticSection.appendChild(desc("Decompose AI generation into specialized agents with per-section parallel calls. Better quality and structured output enforcement."));
182
182
  }
183
183
 
184
184
  const toggleRow = el("div", "settings__toggle-row");
@@ -189,7 +189,7 @@ function renderAITab(body, data) {
189
189
 
190
190
  const sub = el("div", "settings__toggle-label-sub");
191
191
  if (agenticMode === true) {
192
- sub.textContent = "Active — multi-stage pipeline with parallel module generation";
192
+ sub.textContent = "Active — multi-stage pipeline with parallel section generation";
193
193
  sub.style.color = "var(--success)";
194
194
  } else if (agenticMode === false) {
195
195
  sub.textContent = "Disabled — using single-call mode";
@@ -551,7 +551,7 @@ function renderFigmaTab(body, data) {
551
551
 
552
552
  const section = el("section", "settings__section");
553
553
  section.appendChild(sectionTitle("Personal Access Token"));
554
- section.appendChild(desc("Connect your Figma account to import designs directly into HubSpot CMS modules. Tokens are stored locally and only used to call the Figma API."));
554
+ section.appendChild(desc("Connect your Figma account to import designs directly into HubSpot CMS sections. Tokens are stored locally and only used to call the Figma API."));
555
555
 
556
556
  const figmaToken = config.figmaToken;
557
557
  const figmaKeyInfo = {
@@ -1394,36 +1394,47 @@ function getModelsForEngine(engine) {
1394
1394
  switch (engine) {
1395
1395
  case "claude-code":
1396
1396
  return [
1397
- { id: "sonnet", label: "Claude Sonnet (default)" },
1398
- { id: "opus", label: "Claude Opus" },
1399
- { id: "haiku", label: "Claude Haiku" },
1397
+ { id: "claude-opus-4-7", label: "Claude Opus 4.7" },
1398
+ { id: "claude-opus-4-6", label: "Claude Opus 4.6" },
1399
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (default)" },
1400
+ { id: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
1401
+ { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
1400
1402
  ];
1401
1403
  case "anthropic-api":
1402
1404
  case "claude-oauth":
1403
1405
  return [
1404
- { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
1406
+ { id: "claude-opus-4-7", label: "Claude Opus 4.7" },
1405
1407
  { id: "claude-opus-4-6", label: "Claude Opus 4.6" },
1408
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (default)" },
1409
+ { id: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
1406
1410
  { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
1407
1411
  ];
1408
1412
  case "openai-api":
1409
1413
  return [
1410
- { id: "gpt-4o", label: "GPT-4o (default)" },
1411
- { id: "gpt-4o-mini", label: "GPT-4o Mini" },
1412
- { id: "o3", label: "o3" },
1413
- { id: "o4-mini", label: "o4 Mini" },
1414
+ { id: "gpt-5.5", label: "GPT-5.5 (default)" },
1415
+ { id: "gpt-5.5-pro", label: "GPT-5.5 Pro" },
1416
+ { id: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
1417
+ { id: "gpt-5.4-nano", label: "GPT-5.4 Nano" },
1418
+ { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
1414
1419
  ];
1415
1420
  case "gemini-cli":
1416
1421
  case "gemini-api":
1417
1422
  return [
1418
- { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash (default)" },
1419
- { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
1423
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro (default)" },
1424
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
1420
1425
  { id: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
1421
1426
  ];
1422
1427
  case "codex-cli":
1423
1428
  return [
1424
- { id: "o4-mini", label: "o4 Mini (default)" },
1425
- { id: "o3", label: "o3" },
1426
- { id: "gpt-4o", label: "GPT-4o" },
1429
+ { id: "gpt-5.5", label: "GPT-5.5 (default)" },
1430
+ { id: "gpt-5.5-pro", label: "GPT-5.5 Pro" },
1431
+ { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
1432
+ { id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
1433
+ { id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
1434
+ { id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" },
1435
+ { id: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
1436
+ { id: "gpt-5.4-nano", label: "GPT-5.4 Nano" },
1437
+ { id: "codex-mini-latest", label: "Codex Mini (latest)" },
1427
1438
  ];
1428
1439
  default:
1429
1440
  return [];
@@ -1432,10 +1443,13 @@ function getModelsForEngine(engine) {
1432
1443
 
1433
1444
  function getCurrentModel(engine, config) {
1434
1445
  switch (engine) {
1435
- case "claude-code": return config.claudeCodeModel || "sonnet";
1446
+ case "claude-code": return config.claudeCodeModel || "claude-sonnet-4-6";
1436
1447
  case "anthropic-api":
1437
1448
  case "claude-oauth": return config.anthropicApiModel || "claude-sonnet-4-6";
1438
- case "openai-api": return config.openaiApiModel || "gpt-4o";
1449
+ case "openai-api": return config.openaiApiModel || "gpt-5.5";
1450
+ case "codex-cli": return config.codexCliModel || "gpt-5.5";
1451
+ case "gemini-cli": return config.geminiCliModel || "gemini-2.5-pro";
1452
+ case "gemini-api": return config.geminiApiModel || "gemini-2.5-pro";
1439
1453
  default: return null;
1440
1454
  }
1441
1455
  }
@@ -1501,7 +1515,7 @@ document.getElementById("settings-overlay").addEventListener("click", (e) => {
1501
1515
  if (e.target.id === "settings-overlay") closeSettings();
1502
1516
  });
1503
1517
 
1504
- document.getElementById("btn-setup-settings").addEventListener("click", openSettings);
1518
+ document.getElementById("btn-setup-settings").addEventListener("click", () => openSettings());
1505
1519
 
1506
1520
  document.addEventListener("keydown", (e) => {
1507
1521
  if (e.key === "Escape" && !document.getElementById("settings-overlay").classList.contains("hidden")) {