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/README.md +25 -10
- package/dist/index.js +772 -216
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/chat.js +349 -9
- package/ui/dashboard.js +28 -24
- package/ui/dialog.js +3 -1
- package/ui/index.html +22 -10
- package/ui/settings.js +66 -0
- package/ui/styles.css +143 -11
package/package.json
CHANGED
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
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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 = "✔";
|
|
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" ? "✘" : "✔";
|
|
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
|
-
//
|
|
297
|
-
|
|
298
|
-
if (!
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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">×</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">×</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
|