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/README.md +2 -1
- package/dist/index.js +386 -244
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/chat.js +39 -10
- package/ui/preview.js +148 -0
- package/ui/settings.js +73 -1
- package/ui/styles.css +8 -0
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
stats.textContent
|
|
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
|
-
|
|
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
|
-
//
|
|
731
|
-
|
|
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)
|
|
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":
|
|
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);
|