vibespot 1.0.4 → 1.0.6

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": "1.0.4",
3
+ "version": "1.0.6",
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
@@ -145,6 +145,7 @@ function handleWsMessage(msg) {
145
145
 
146
146
  case "error":
147
147
  stopPipelineTimer();
148
+ if (typeof clearAllModulesWorking === "function") clearAllModulesWorking();
148
149
  finishStreaming();
149
150
  pipelineBubbleEl = null;
150
151
  pipelineStepsEl = null;
@@ -213,7 +214,8 @@ function ensurePipelineBubble() {
213
214
  isStreaming = true;
214
215
  sendBtn.disabled = true;
215
216
  streamStartTime = Date.now();
216
- if (typeof showGeneratingPreview === "function") showGeneratingPreview();
217
+ // Don't replace the preview with a spinner — agentic mode uses
218
+ // incremental placeholders and keeps the existing page visible.
217
219
  }
218
220
 
219
221
  // If startStreaming() already created an empty bubble, repurpose it
@@ -347,6 +349,14 @@ function handleModuleProgress(msg) {
347
349
  failed: "✗",
348
350
  };
349
351
  statusEl.textContent = statusLabels[msg.status] || msg.status;
352
+
353
+ // Mark/clear working overlay in the preview
354
+ if (msg.status === "generating" && typeof markModulesWorking === "function") {
355
+ markModulesWorking([msg.module]);
356
+ } else if ((msg.status === "complete" || msg.status === "failed") && typeof clearModuleWorking === "function") {
357
+ clearModuleWorking(msg.module);
358
+ }
359
+
350
360
  scrollToBottom();
351
361
  }
352
362
 
@@ -354,6 +364,7 @@ function handlePipelineComplete(msg) {
354
364
  if (!pipelineBubbleEl) return;
355
365
 
356
366
  stopPipelineTimer();
367
+ if (typeof clearAllModulesWorking === "function") clearAllModulesWorking();
357
368
 
358
369
  // Remove the live timer element
359
370
  const timerEl = pipelineBubbleEl.querySelector(".pipeline-timer");
@@ -363,13 +374,26 @@ function handlePipelineComplete(msg) {
363
374
  const bubble = streamingMsgEl || pipelineBubbleEl;
364
375
  bubble.querySelectorAll(".pipeline-step").forEach((el) => markStepDone(el));
365
376
 
377
+ // Show answer text for question intents
378
+ if (msg.answer) {
379
+ const answerEl = document.createElement("div");
380
+ answerEl.className = "pipeline-answer";
381
+ answerEl.textContent = msg.answer;
382
+ bubble.appendChild(answerEl);
383
+ }
384
+
366
385
  // Add completion stats after the last element in the bubble
367
386
  const stats = document.createElement("div");
368
387
  stats.className = "pipeline-stats";
369
388
  const duration = formatDuration(msg.durationMs);
370
- stats.textContent = `Generated ${msg.modulesGenerated} module${msg.modulesGenerated === 1 ? "" : "s"} in ${duration}`;
371
- if (msg.modulesUnchanged > 0) {
372
- stats.textContent += ` (${msg.modulesUnchanged} unchanged)`;
389
+ if (msg.answer) {
390
+ // For questions, just show duration
391
+ stats.textContent = `Answered in ${duration}`;
392
+ } else {
393
+ stats.textContent = `Generated ${msg.modulesGenerated} module${msg.modulesGenerated === 1 ? "" : "s"} in ${duration}`;
394
+ if (msg.modulesUnchanged > 0) {
395
+ stats.textContent += ` (${msg.modulesUnchanged} unchanged)`;
396
+ }
373
397
  }
374
398
  // Place stats after quality_check step (or after modules if no quality step)
375
399
  const qualityStep = bubble.querySelector('[data-step="quality_check"]');
@@ -378,7 +402,7 @@ function handlePipelineComplete(msg) {
378
402
  } else if (pipelineModulesEl) {
379
403
  pipelineModulesEl.after(stats);
380
404
  } else {
381
- pipelineStepsEl.appendChild(stats);
405
+ bubble.appendChild(stats);
382
406
  }
383
407
 
384
408
  clearStreamStatus();
@@ -394,6 +418,7 @@ function handlePipelinePartial(msg) {
394
418
  if (!pipelineBubbleEl) return;
395
419
 
396
420
  stopPipelineTimer();
421
+ if (typeof clearAllModulesWorking === "function") clearAllModulesWorking();
397
422
 
398
423
  const timerEl = pipelineBubbleEl.querySelector(".pipeline-timer");
399
424
  if (timerEl) timerEl.remove();
@@ -727,10 +752,8 @@ function startStreaming() {
727
752
  sendBtn.disabled = true;
728
753
  streamStartTime = Date.now();
729
754
 
730
- // Show generating preview with spinner + fun messages
731
- if (typeof showGeneratingPreview === "function") {
732
- showGeneratingPreview();
733
- }
755
+ // Don't show generating preview here agentic mode keeps the page visible.
756
+ // For single-call mode, showGeneratingPreview() is called on first "stream" event.
734
757
 
735
758
  const time = formatMessageTime(streamStartTime);
736
759
  const div = document.createElement("div");
@@ -788,7 +811,13 @@ function flushStreamRender() {
788
811
  }
789
812
 
790
813
  function handleStreamStatus(status) {
791
- if (!streamingMsgEl) startStreaming();
814
+ if (!streamingMsgEl) {
815
+ startStreaming();
816
+ // Single-call mode — show generating preview (agentic mode never sends stream_status)
817
+ if (typeof showGeneratingPreview === "function" && !pipelineBubbleEl) {
818
+ showGeneratingPreview();
819
+ }
820
+ }
792
821
 
793
822
  lastStreamStatus = status;
794
823
 
package/ui/preview.js CHANGED
@@ -128,5 +128,153 @@ function showGeneratingPreview() {
128
128
  previewFrame.srcdoc = html;
129
129
  }
130
130
 
131
+ // ---------------------------------------------------------------------------
132
+ // "Working on it" overlay for modules being regenerated
133
+ // ---------------------------------------------------------------------------
134
+
135
+ const WORKING_MESSAGES = [
136
+ "Rethinking this section\u2026",
137
+ "Rewriting the code\u2026",
138
+ "Giving this a fresh coat of paint\u2026",
139
+ "Making it better\u2026",
140
+ "Tweaking the layout\u2026",
141
+ "Polishing the details\u2026",
142
+ "Almost there\u2026",
143
+ "Updating the design\u2026",
144
+ "Improving the copy\u2026",
145
+ "Refining the structure\u2026",
146
+ ];
147
+
148
+ /**
149
+ * Inject the overlay CSS into the preview iframe (once).
150
+ */
151
+ function ensureOverlayStyles(doc) {
152
+ if (doc.getElementById("vibespot-working-css")) return;
153
+ const style = doc.createElement("style");
154
+ style.id = "vibespot-working-css";
155
+ style.textContent = `
156
+ .vibespot-module--working { position: relative; }
157
+ .vibespot-module--working > *:not(.vibespot-working-overlay) {
158
+ filter: blur(3px) saturate(0.4);
159
+ opacity: 0.5;
160
+ transition: filter 0.4s ease, opacity 0.4s ease;
161
+ pointer-events: none;
162
+ }
163
+ .vibespot-working-overlay {
164
+ position: absolute; inset: 0;
165
+ display: flex; flex-direction: column;
166
+ align-items: center; justify-content: center;
167
+ z-index: 9999;
168
+ pointer-events: none;
169
+ }
170
+ .vibespot-working-overlay__spinner {
171
+ width: 36px; height: 36px;
172
+ border: 3px solid rgba(232,97,58,0.15);
173
+ border-top-color: rgba(232,97,58,0.8);
174
+ border-radius: 50%;
175
+ animation: vw-spin 0.8s linear infinite;
176
+ margin-bottom: 12px;
177
+ }
178
+ .vibespot-working-overlay__text {
179
+ font-family: system-ui, -apple-system, sans-serif;
180
+ font-size: 14px; font-weight: 500;
181
+ color: rgba(255,255,255,0.7);
182
+ text-align: center;
183
+ max-width: 280px;
184
+ transition: opacity 0.5s ease;
185
+ }
186
+ .vibespot-working-overlay__text.fade { opacity: 0; }
187
+ @keyframes vw-spin { to { transform: rotate(360deg); } }
188
+ `;
189
+ doc.head.appendChild(style);
190
+ }
191
+
192
+ /** Active carousel intervals keyed by module name */
193
+ const workingIntervals = new Map();
194
+
195
+ /**
196
+ * Mark modules as "being worked on" — adds blur overlay with rotating messages.
197
+ */
198
+ function markModulesWorking(moduleNames) {
199
+ try {
200
+ const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
201
+ if (!doc || !doc.body) return;
202
+ ensureOverlayStyles(doc);
203
+
204
+ for (const name of moduleNames) {
205
+ const el = doc.querySelector(`[data-module="${name}"]`);
206
+ if (!el || el.classList.contains("vibespot-module--working")) continue;
207
+
208
+ el.classList.add("vibespot-module--working");
209
+
210
+ const overlay = doc.createElement("div");
211
+ overlay.className = "vibespot-working-overlay";
212
+ overlay.innerHTML = `
213
+ <div class="vibespot-working-overlay__spinner"></div>
214
+ <div class="vibespot-working-overlay__text"></div>
215
+ `;
216
+ el.appendChild(overlay);
217
+
218
+ // Scroll to first working module
219
+ if (name === moduleNames[0]) {
220
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
221
+ }
222
+
223
+ // Start message carousel
224
+ const textEl = overlay.querySelector(".vibespot-working-overlay__text");
225
+ let idx = Math.floor(Math.random() * WORKING_MESSAGES.length);
226
+ textEl.textContent = WORKING_MESSAGES[idx];
227
+
228
+ const interval = setInterval(() => {
229
+ textEl.classList.add("fade");
230
+ setTimeout(() => {
231
+ idx = (idx + 1) % WORKING_MESSAGES.length;
232
+ textEl.textContent = WORKING_MESSAGES[idx];
233
+ textEl.classList.remove("fade");
234
+ }, 500);
235
+ }, 3500);
236
+ workingIntervals.set(name, interval);
237
+ }
238
+ } catch { /* cross-origin */ }
239
+ }
240
+
241
+ /**
242
+ * Clear working overlay from a specific module (when it completes).
243
+ */
244
+ function clearModuleWorking(moduleName) {
245
+ const interval = workingIntervals.get(moduleName);
246
+ if (interval) { clearInterval(interval); workingIntervals.delete(moduleName); }
247
+
248
+ try {
249
+ const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
250
+ if (!doc) return;
251
+ const el = doc.querySelector(`[data-module="${moduleName}"]`);
252
+ if (!el) return;
253
+ el.classList.remove("vibespot-module--working");
254
+ const overlay = el.querySelector(".vibespot-working-overlay");
255
+ if (overlay) overlay.remove();
256
+ } catch { /* cross-origin */ }
257
+ }
258
+
259
+ /**
260
+ * Clear all working overlays.
261
+ */
262
+ function clearAllModulesWorking() {
263
+ for (const [name, interval] of workingIntervals) {
264
+ clearInterval(interval);
265
+ }
266
+ workingIntervals.clear();
267
+
268
+ try {
269
+ const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
270
+ if (!doc) return;
271
+ doc.querySelectorAll(".vibespot-module--working").forEach((el) => {
272
+ el.classList.remove("vibespot-module--working");
273
+ const overlay = el.querySelector(".vibespot-working-overlay");
274
+ if (overlay) overlay.remove();
275
+ });
276
+ } catch { /* cross-origin */ }
277
+ }
278
+
131
279
  // Preview refresh is triggered by setup.js after a session is created.
132
280
  // Do NOT auto-refresh here.
package/ui/settings.js CHANGED
@@ -13,6 +13,7 @@ const activePolls = {};
13
13
  const ENGINE_LABELS = {
14
14
  "claude-code": "Claude Code",
15
15
  "anthropic-api": "Anthropic API",
16
+ "claude-oauth": "Claude (OAuth)",
16
17
  "openai-api": "OpenAI API",
17
18
  "gemini-cli": "Gemini CLI",
18
19
  "gemini-api": "Gemini API",
@@ -99,6 +100,7 @@ function renderAITab(body, data) {
99
100
  const allEngines = [
100
101
  { id: "claude-code", label: "Claude Code" },
101
102
  { id: "anthropic-api", label: "Anthropic API" },
103
+ { id: "claude-oauth", label: "Claude (OAuth)" },
102
104
  { id: "openai-api", label: "OpenAI API" },
103
105
  { id: "gemini-cli", label: "Gemini CLI" },
104
106
  { id: "gemini-api", label: "Gemini API" },
@@ -224,6 +226,74 @@ function renderAITab(body, data) {
224
226
  }
225
227
  body.appendChild(keysSection);
226
228
 
229
+ // Claude OAuth section
230
+ const oauthSection = el("section", "settings__section");
231
+ oauthSection.appendChild(sectionTitle("Claude OAuth"));
232
+ oauthSection.appendChild(desc("Use your Claude Pro/Max subscription. Run `claude setup-token` in your terminal to get a token, then paste it below."));
233
+
234
+ const oauthCard = el("div", "settings__card");
235
+ const oauthInfo = env.tools?.claudeOAuth;
236
+
237
+ if (oauthInfo?.authenticated) {
238
+ const statusRow = el("div", "settings__card-row");
239
+ const statusLabel = el("span", "settings__card-label");
240
+ statusLabel.textContent = "Connected";
241
+ statusLabel.style.color = "var(--vibe-green)";
242
+ if (oauthInfo.expiresAt) {
243
+ const hrs = Math.max(0, Math.round((new Date(oauthInfo.expiresAt) - Date.now()) / 3600000));
244
+ statusLabel.textContent += ` (token expires in ${hrs}h)`;
245
+ }
246
+ statusRow.appendChild(statusLabel);
247
+
248
+ const disconnectBtn = el("button", "btn btn--sm btn--danger");
249
+ disconnectBtn.textContent = "Disconnect";
250
+ disconnectBtn.addEventListener("click", async () => {
251
+ await fetch("/api/settings/claude-oauth/logout", { method: "POST" });
252
+ refreshSettings();
253
+ });
254
+ statusRow.appendChild(disconnectBtn);
255
+ oauthCard.appendChild(statusRow);
256
+ }
257
+
258
+ const inputRow = el("div", "settings__card-row");
259
+ inputRow.style.gap = "8px";
260
+ const tokenInput = el("input", "settings__apikey-input");
261
+ tokenInput.type = "password";
262
+ tokenInput.placeholder = oauthInfo?.authenticated ? "••••••••••••• (saved)" : "sk-ant-oat01-...";
263
+ inputRow.appendChild(tokenInput);
264
+
265
+ const saveBtn = el("button", "btn btn--sm btn--primary");
266
+ saveBtn.textContent = "Save Token";
267
+ saveBtn.addEventListener("click", async () => {
268
+ const token = tokenInput.value.trim();
269
+ if (!token) return;
270
+ saveBtn.disabled = true;
271
+ saveBtn.textContent = "Saving...";
272
+ try {
273
+ const res = await fetch("/api/settings/claude-oauth/save", {
274
+ method: "POST",
275
+ headers: { "Content-Type": "application/json" },
276
+ body: JSON.stringify({ access_token: token }),
277
+ });
278
+ const data = await res.json();
279
+ if (data.ok) {
280
+ tokenInput.value = "";
281
+ refreshSettings();
282
+ } else {
283
+ alert(data.error || "Failed to save token");
284
+ }
285
+ } catch (err) {
286
+ alert("Failed: " + err.message);
287
+ }
288
+ saveBtn.disabled = false;
289
+ saveBtn.textContent = "Save Token";
290
+ });
291
+ inputRow.appendChild(saveBtn);
292
+ oauthCard.appendChild(inputRow);
293
+
294
+ oauthSection.appendChild(oauthCard);
295
+ body.appendChild(oauthSection);
296
+
227
297
  // CLI Tools section with toggles
228
298
  const cliSection = el("section", "settings__section");
229
299
  cliSection.appendChild(sectionTitle("CLI Tools"));
@@ -1102,6 +1172,7 @@ function getModelsForEngine(engine) {
1102
1172
  { id: "haiku", label: "Claude Haiku" },
1103
1173
  ];
1104
1174
  case "anthropic-api":
1175
+ case "claude-oauth":
1105
1176
  return [
1106
1177
  { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
1107
1178
  { id: "claude-opus-4-6", label: "Claude Opus 4.6" },
@@ -1135,7 +1206,8 @@ function getModelsForEngine(engine) {
1135
1206
  function getCurrentModel(engine, config) {
1136
1207
  switch (engine) {
1137
1208
  case "claude-code": return config.claudeCodeModel || "sonnet";
1138
- case "anthropic-api": return config.anthropicApiModel || "claude-sonnet-4-6";
1209
+ case "anthropic-api":
1210
+ case "claude-oauth": return config.anthropicApiModel || "claude-sonnet-4-6";
1139
1211
  case "openai-api": return config.openaiApiModel || "gpt-4o";
1140
1212
  default: return null;
1141
1213
  }
package/ui/styles.css CHANGED
@@ -2214,6 +2214,14 @@ body { display: flex; }
2214
2214
  opacity: 0.8;
2215
2215
  }
2216
2216
 
2217
+ .pipeline-answer {
2218
+ padding: 8px 0 4px;
2219
+ font-size: 13px;
2220
+ color: var(--text-primary, #e8e8e8);
2221
+ line-height: 1.5;
2222
+ white-space: pre-wrap;
2223
+ }
2224
+
2217
2225
  .pipeline-stats {
2218
2226
  font-size: 12px;
2219
2227
  color: var(--success, #4ade80);