vibespot 1.1.1 → 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/LICENSE +103 -33
- package/README.md +11 -1
- package/assets/plan-templates/agency-services.md +42 -0
- package/assets/plan-templates/blog-content-hub.md +41 -0
- package/assets/plan-templates/ecommerce-product.md +42 -0
- package/assets/plan-templates/event-registration.md +42 -0
- package/assets/plan-templates/portfolio.md +41 -0
- package/assets/plan-templates/restaurant.md +42 -0
- package/assets/plan-templates/saas-landing.md +42 -0
- package/dist/index.js +240 -225
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
- package/starters/01-saas-landing.json +43 -0
- package/starters/02-portfolio.json +39 -0
- package/starters/03-restaurant.json +39 -0
- package/starters/04-event.json +39 -0
- package/starters/05-coming-soon.json +32 -0
- package/ui/chat.js +865 -130
- package/ui/dashboard.js +194 -12
- package/ui/docs/index.html +89 -10
- package/ui/field-editor.js +1 -1
- package/ui/index.html +156 -37
- package/ui/marketplace.js +218 -0
- package/ui/plan.js +0 -0
- package/ui/preview.js +316 -1
- package/ui/settings.js +35 -21
- package/ui/setup.js +291 -3
- package/ui/styles.css +1305 -120
package/ui/chat.js
CHANGED
|
@@ -18,12 +18,38 @@ let currentTemplateId = "";
|
|
|
18
18
|
let renderScheduled = false;
|
|
19
19
|
let scrollScheduled = false;
|
|
20
20
|
|
|
21
|
+
// Change tracking for the in-flight generation. `modulesBeforeRun` snapshots
|
|
22
|
+
// the module list when the user submits a prompt so we can tell which modules
|
|
23
|
+
// are brand new when the run finishes. `changedModulesInRun` accumulates names
|
|
24
|
+
// from per-module `module_progress` events. The flag is set on
|
|
25
|
+
// `generation_complete` so the *next* `modules_updated` (the post-run one)
|
|
26
|
+
// triggers the highlight pass instead of a plain refresh.
|
|
27
|
+
let modulesBeforeRun = null;
|
|
28
|
+
let changedModulesInRun = new Set();
|
|
29
|
+
let highlightOnNextModulesUpdated = false;
|
|
30
|
+
let changedListClearTimer = null;
|
|
31
|
+
|
|
21
32
|
const messagesEl = document.getElementById("chat-messages");
|
|
22
33
|
const inputEl = document.getElementById("chat-input");
|
|
23
34
|
const sendBtn = document.getElementById("chat-send");
|
|
24
35
|
const statusText = document.getElementById("status-text");
|
|
25
36
|
const statusEngine = document.getElementById("status-engine");
|
|
26
37
|
|
|
38
|
+
// Snapshot the welcome section before any init can destroy it
|
|
39
|
+
const _welcomeHtml = document.getElementById("chat-welcome")?.outerHTML || "";
|
|
40
|
+
|
|
41
|
+
function restoreWelcome() {
|
|
42
|
+
if (!_welcomeHtml || messagesEl.querySelector(".chat__welcome")) return;
|
|
43
|
+
messagesEl.insertAdjacentHTML("afterbegin", _welcomeHtml);
|
|
44
|
+
const el = messagesEl.querySelector("#starter-templates");
|
|
45
|
+
if (el) {
|
|
46
|
+
el.addEventListener("click", (e) => {
|
|
47
|
+
const btn = e.target.closest(".starter-btn");
|
|
48
|
+
if (btn) sendMessage(btn.dataset.prompt);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
// ---------------------------------------------------------------------------
|
|
28
54
|
// WebSocket connection
|
|
29
55
|
// ---------------------------------------------------------------------------
|
|
@@ -82,6 +108,11 @@ function handleWsMessage(msg) {
|
|
|
82
108
|
messagesEl.innerHTML = "";
|
|
83
109
|
document.getElementById("module-items").innerHTML = "";
|
|
84
110
|
document.getElementById("module-count").textContent = "0";
|
|
111
|
+
hideChatSuggestions();
|
|
112
|
+
// Reset pipeline state — DOM nodes were detached by innerHTML clear
|
|
113
|
+
resetPipelineState();
|
|
114
|
+
stopPipelineTimer();
|
|
115
|
+
streamingMsgEl = null;
|
|
85
116
|
|
|
86
117
|
if (msg.modules && msg.modules.length > 0) {
|
|
87
118
|
updateModuleList(msg.modules);
|
|
@@ -106,6 +137,8 @@ function handleWsMessage(msg) {
|
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
scrollToBottom();
|
|
140
|
+
} else {
|
|
141
|
+
restoreWelcome();
|
|
109
142
|
}
|
|
110
143
|
|
|
111
144
|
// Show/hide version history button
|
|
@@ -114,10 +147,44 @@ function handleWsMessage(msg) {
|
|
|
114
147
|
historyBtn.style.display = msg.gitAvailable ? "" : "none";
|
|
115
148
|
}
|
|
116
149
|
|
|
150
|
+
// Initialize history timeline (compact strip above chat input)
|
|
151
|
+
historyGitAvailable = !!msg.gitAvailable;
|
|
152
|
+
if (historyGitAvailable) {
|
|
153
|
+
historyTimelineCursor = 0;
|
|
154
|
+
refreshHistoryTimeline();
|
|
155
|
+
} else {
|
|
156
|
+
const tl = document.getElementById("history-timeline");
|
|
157
|
+
if (tl) tl.classList.add("hidden");
|
|
158
|
+
}
|
|
159
|
+
|
|
117
160
|
// Hydrate plan-mode state (toggle + Plan pane content)
|
|
118
161
|
if (window.planController) {
|
|
119
162
|
window.planController.setInitialState(msg);
|
|
120
163
|
}
|
|
164
|
+
|
|
165
|
+
// If a pipeline is actively running (reconnect scenario), lock the
|
|
166
|
+
// input so the user can't double-submit. Replayed pipeline events
|
|
167
|
+
// that follow this init message will rebuild the progress UI.
|
|
168
|
+
if (msg.isGenerating) {
|
|
169
|
+
isStreaming = true;
|
|
170
|
+
sendBtn.disabled = true;
|
|
171
|
+
streamStartTime = Date.now();
|
|
172
|
+
if (typeof window.setSelectModeDisabled === "function") {
|
|
173
|
+
window.setSelectModeDisabled(true);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// If setup handed us an initial prompt (describe-it path), send it now
|
|
178
|
+
// that the session is live. Skip if the project already has history
|
|
179
|
+
// (e.g. resumed session) to avoid double-submitting.
|
|
180
|
+
if (window.__pendingInitialPrompt) {
|
|
181
|
+
const pendingPrompt = window.__pendingInitialPrompt;
|
|
182
|
+
window.__pendingInitialPrompt = null;
|
|
183
|
+
const hasHistory = msg.messages && msg.messages.length > 0;
|
|
184
|
+
if (!hasHistory) {
|
|
185
|
+
setTimeout(() => sendMessage(pendingPrompt), 50);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
121
188
|
break;
|
|
122
189
|
|
|
123
190
|
case "stream":
|
|
@@ -131,30 +198,40 @@ function handleWsMessage(msg) {
|
|
|
131
198
|
case "generation_complete":
|
|
132
199
|
clearStreamStatus();
|
|
133
200
|
finishStreaming();
|
|
201
|
+
// The next `modules_updated` is the terminal one for this run — let it
|
|
202
|
+
// fire the change-highlight pass on the preview iframe.
|
|
203
|
+
highlightOnNextModulesUpdated = true;
|
|
134
204
|
break;
|
|
135
205
|
|
|
136
206
|
case "modules_updated":
|
|
137
207
|
if (msg.modules) {
|
|
138
208
|
updateModuleList(msg.modules);
|
|
139
209
|
}
|
|
140
|
-
|
|
210
|
+
if (highlightOnNextModulesUpdated) {
|
|
211
|
+
highlightOnNextModulesUpdated = false;
|
|
212
|
+
flushChangeHighlights(msg.modules || []);
|
|
213
|
+
} else {
|
|
214
|
+
refreshPreview();
|
|
215
|
+
}
|
|
141
216
|
break;
|
|
142
217
|
|
|
143
218
|
case "version_created":
|
|
144
219
|
if (historyPanelOpen) refreshHistoryPanel();
|
|
220
|
+
// New generation: reset timeline cursor to head and refresh.
|
|
221
|
+
historyTimelineCursor = 0;
|
|
222
|
+
refreshHistoryTimeline();
|
|
145
223
|
break;
|
|
146
224
|
|
|
147
225
|
case "parse_warning":
|
|
148
|
-
appendSystemMessage(msg.message || "
|
|
226
|
+
appendSystemMessage(msg.message || "Section changes could not be applied.");
|
|
149
227
|
break;
|
|
150
228
|
|
|
151
229
|
case "error":
|
|
152
230
|
stopPipelineTimer();
|
|
231
|
+
stopPipelineEstimate();
|
|
153
232
|
if (typeof clearAllModulesWorking === "function") clearAllModulesWorking();
|
|
154
233
|
finishStreaming();
|
|
155
|
-
|
|
156
|
-
pipelineStepsEl = null;
|
|
157
|
-
pipelineModulesEl = null;
|
|
234
|
+
resetPipelineState();
|
|
158
235
|
appendAssistantError(msg.message);
|
|
159
236
|
setStatus("Error");
|
|
160
237
|
break;
|
|
@@ -246,14 +323,73 @@ function handleWsMessage(msg) {
|
|
|
246
323
|
const STEP_LABELS = {
|
|
247
324
|
analyzing: "Analyzing",
|
|
248
325
|
designing: "Designing",
|
|
249
|
-
developing: "
|
|
326
|
+
developing: "Building",
|
|
250
327
|
quality_check: "Quality Check",
|
|
251
328
|
};
|
|
252
329
|
const STEP_ORDER = ["analyzing", "designing", "developing", "quality_check"];
|
|
253
330
|
|
|
331
|
+
const STEP_ICONS = {
|
|
332
|
+
analyzing:
|
|
333
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
|
334
|
+
designing:
|
|
335
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a10 10 0 1 0 0 20c1.4 0 2-.9 2-2 0-.5-.2-1-.5-1.4a1.5 1.5 0 0 1 1.2-2.4H17a5 5 0 0 0 5-5c0-5.5-4.5-9.2-10-9.2z"/><circle cx="7.5" cy="10.5" r="1.4"/><circle cx="12" cy="7.5" r="1.4"/><circle cx="16.5" cy="10.5" r="1.4"/></svg>',
|
|
336
|
+
developing:
|
|
337
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
338
|
+
quality_check:
|
|
339
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2 4 6v6c0 5 3.5 9.4 8 10 4.5-.6 8-5 8-10V6l-8-4z"/></svg>',
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const CHECK_ICON =
|
|
343
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="5 13 10 18 20 7"/></svg>';
|
|
344
|
+
|
|
345
|
+
// Per-module wall-clock seconds. Rough heuristic for time-remaining display.
|
|
346
|
+
const ESTIMATED_SECONDS_PER_MODULE = 14;
|
|
347
|
+
|
|
254
348
|
let pipelineBubbleEl = null;
|
|
255
|
-
let
|
|
349
|
+
let pipelineStepperEl = null;
|
|
350
|
+
let pipelineDetailEl = null;
|
|
256
351
|
let pipelineModulesEl = null;
|
|
352
|
+
let pipelineEstimateEl = null;
|
|
353
|
+
let pipelineCurrentStep = null;
|
|
354
|
+
let pipelineEstimateInterval = null;
|
|
355
|
+
|
|
356
|
+
function buildStepperHtml() {
|
|
357
|
+
const parts = [];
|
|
358
|
+
for (let i = 0; i < STEP_ORDER.length; i++) {
|
|
359
|
+
const step = STEP_ORDER[i];
|
|
360
|
+
parts.push(`
|
|
361
|
+
<div class="pipeline-stage pipeline-stage--pending" data-stage="${step}">
|
|
362
|
+
<div class="pipeline-stage__indicator">
|
|
363
|
+
<span class="pipeline-stage__icon">${STEP_ICONS[step]}</span>
|
|
364
|
+
<span class="pipeline-stage__check">${CHECK_ICON}</span>
|
|
365
|
+
</div>
|
|
366
|
+
<div class="pipeline-stage__label">${STEP_LABELS[step]}</div>
|
|
367
|
+
</div>`);
|
|
368
|
+
if (i < STEP_ORDER.length - 1) {
|
|
369
|
+
parts.push('<div class="pipeline-stage__connector" data-after="' + step + '"></div>');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return `<div class="pipeline-stepper" role="progressbar" aria-label="Generation progress">${parts.join("")}</div>`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildPipelineBodyHtml() {
|
|
376
|
+
return `
|
|
377
|
+
${buildStepperHtml()}
|
|
378
|
+
<div class="pipeline-detail" hidden></div>
|
|
379
|
+
<div class="pipeline-modules"></div>
|
|
380
|
+
<div class="pipeline-footer">
|
|
381
|
+
<div class="pipeline-timer"></div>
|
|
382
|
+
<div class="pipeline-estimate" hidden></div>
|
|
383
|
+
</div>`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function captureBubbleRefs(bubbleEl, container) {
|
|
387
|
+
pipelineBubbleEl = container;
|
|
388
|
+
pipelineStepperEl = bubbleEl.querySelector(".pipeline-stepper");
|
|
389
|
+
pipelineDetailEl = bubbleEl.querySelector(".pipeline-detail");
|
|
390
|
+
pipelineModulesEl = bubbleEl.querySelector(".pipeline-modules");
|
|
391
|
+
pipelineEstimateEl = bubbleEl.querySelector(".pipeline-estimate");
|
|
392
|
+
}
|
|
257
393
|
|
|
258
394
|
function ensurePipelineBubble() {
|
|
259
395
|
if (pipelineBubbleEl) return;
|
|
@@ -270,13 +406,8 @@ function ensurePipelineBubble() {
|
|
|
270
406
|
if (streamingMsgEl) {
|
|
271
407
|
const existingDiv = streamingMsgEl.closest(".chat-msg");
|
|
272
408
|
if (existingDiv) {
|
|
273
|
-
streamingMsgEl.innerHTML =
|
|
274
|
-
|
|
275
|
-
<div class="pipeline-modules"></div>
|
|
276
|
-
<div class="pipeline-timer"></div>`;
|
|
277
|
-
pipelineBubbleEl = existingDiv;
|
|
278
|
-
pipelineStepsEl = streamingMsgEl.querySelector(".pipeline-steps");
|
|
279
|
-
pipelineModulesEl = streamingMsgEl.querySelector(".pipeline-modules");
|
|
409
|
+
streamingMsgEl.innerHTML = buildPipelineBodyHtml();
|
|
410
|
+
captureBubbleRefs(streamingMsgEl, existingDiv);
|
|
280
411
|
startPipelineTimer(streamingMsgEl.querySelector(".pipeline-timer"));
|
|
281
412
|
scrollToBottom();
|
|
282
413
|
return;
|
|
@@ -294,64 +425,80 @@ function ensurePipelineBubble() {
|
|
|
294
425
|
<span class="chat-msg__time">${time}</span>
|
|
295
426
|
</div>
|
|
296
427
|
<div class="chat-msg__bubble">
|
|
297
|
-
|
|
298
|
-
<div class="pipeline-modules"></div>
|
|
299
|
-
<div class="pipeline-timer"></div>
|
|
428
|
+
${buildPipelineBodyHtml()}
|
|
300
429
|
</div>
|
|
301
430
|
</div>`;
|
|
302
431
|
messagesEl.appendChild(div);
|
|
303
432
|
|
|
304
|
-
pipelineBubbleEl = div;
|
|
305
|
-
pipelineStepsEl = div.querySelector(".pipeline-steps");
|
|
306
|
-
pipelineModulesEl = div.querySelector(".pipeline-modules");
|
|
307
433
|
streamingMsgEl = div.querySelector(".chat-msg__bubble");
|
|
434
|
+
captureBubbleRefs(streamingMsgEl, div);
|
|
308
435
|
startPipelineTimer(div.querySelector(".pipeline-timer"));
|
|
309
436
|
scrollToBottom();
|
|
310
437
|
}
|
|
311
438
|
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
439
|
+
function setStageStatus(step, status) {
|
|
440
|
+
if (!pipelineStepperEl) return;
|
|
441
|
+
const stage = pipelineStepperEl.querySelector(`[data-stage="${step}"]`);
|
|
442
|
+
if (!stage) return;
|
|
443
|
+
stage.classList.remove(
|
|
444
|
+
"pipeline-stage--pending",
|
|
445
|
+
"pipeline-stage--active",
|
|
446
|
+
"pipeline-stage--done",
|
|
447
|
+
"pipeline-stage--failed",
|
|
448
|
+
);
|
|
449
|
+
stage.classList.add("pipeline-stage--" + status);
|
|
450
|
+
|
|
451
|
+
// Fill the connector behind this stage when it becomes active or done.
|
|
452
|
+
const idx = STEP_ORDER.indexOf(step);
|
|
453
|
+
if (idx > 0) {
|
|
454
|
+
const prev = STEP_ORDER[idx - 1];
|
|
455
|
+
const conn = pipelineStepperEl.querySelector(`[data-after="${prev}"]`);
|
|
456
|
+
if (conn) conn.classList.add("pipeline-stage__connector--filled");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function setPipelineDetail(text) {
|
|
461
|
+
if (!pipelineDetailEl) return;
|
|
462
|
+
if (!text) {
|
|
463
|
+
pipelineDetailEl.hidden = true;
|
|
464
|
+
pipelineDetailEl.textContent = "";
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
pipelineDetailEl.hidden = false;
|
|
468
|
+
pipelineDetailEl.classList.remove("pipeline-detail--in");
|
|
469
|
+
// Force reflow so the animation restarts when text changes
|
|
470
|
+
void pipelineDetailEl.offsetWidth;
|
|
471
|
+
pipelineDetailEl.classList.add("pipeline-detail--in");
|
|
472
|
+
pipelineDetailEl.textContent = text;
|
|
317
473
|
}
|
|
318
474
|
|
|
319
475
|
function handleAgentStep(msg) {
|
|
320
476
|
ensurePipelineBubble();
|
|
321
477
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
existing.forEach((el) => {
|
|
332
|
-
if (!el.classList.contains("pipeline-step--done")) {
|
|
333
|
-
markStepDone(el);
|
|
478
|
+
const incoming = msg.step;
|
|
479
|
+
const incomingIdx = STEP_ORDER.indexOf(incoming);
|
|
480
|
+
|
|
481
|
+
// Mark all prior stages done (handles forward jumps + repeated firings)
|
|
482
|
+
if (incomingIdx >= 0) {
|
|
483
|
+
for (let i = 0; i < incomingIdx; i++) {
|
|
484
|
+
const prevStage = pipelineStepperEl.querySelector(`[data-stage="${STEP_ORDER[i]}"]`);
|
|
485
|
+
if (prevStage && !prevStage.classList.contains("pipeline-stage--done")) {
|
|
486
|
+
setStageStatus(STEP_ORDER[i], "done");
|
|
334
487
|
}
|
|
335
|
-
}
|
|
488
|
+
}
|
|
336
489
|
}
|
|
337
490
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
step.className = "pipeline-step pipeline-step--active";
|
|
341
|
-
step.dataset.step = msg.step;
|
|
342
|
-
step.innerHTML = `<span class="pipeline-step__icon">⟳</span> <span class="pipeline-step__label">${msg.label || STEP_LABELS[msg.step] || msg.step}</span>`;
|
|
491
|
+
setStageStatus(incoming, "active");
|
|
492
|
+
pipelineCurrentStep = incoming;
|
|
343
493
|
|
|
344
|
-
|
|
345
|
-
// developing → module cards → quality check
|
|
346
|
-
if (msg.step === "quality_check" && pipelineModulesEl) {
|
|
347
|
-
pipelineModulesEl.after(step);
|
|
348
|
-
} else {
|
|
349
|
-
pipelineStepsEl.appendChild(step);
|
|
350
|
-
}
|
|
494
|
+
setPipelineDetail(msg.label || STEP_LABELS[incoming] || incoming);
|
|
351
495
|
|
|
352
|
-
|
|
353
|
-
if (msg.step === "developing") {
|
|
496
|
+
if (incoming === "developing") {
|
|
354
497
|
pipelineModulesEl.innerHTML = "";
|
|
498
|
+
startPipelineEstimate();
|
|
499
|
+
} else {
|
|
500
|
+
stopPipelineEstimate();
|
|
501
|
+
if (pipelineEstimateEl) pipelineEstimateEl.hidden = true;
|
|
355
502
|
}
|
|
356
503
|
|
|
357
504
|
scrollToBottom();
|
|
@@ -359,17 +506,7 @@ function handleAgentStep(msg) {
|
|
|
359
506
|
|
|
360
507
|
function handleAgentDecision(msg) {
|
|
361
508
|
if (!pipelineBubbleEl) return;
|
|
362
|
-
|
|
363
|
-
// Find the step element — may be inside pipelineStepsEl or after pipelineModulesEl
|
|
364
|
-
const bubble = streamingMsgEl || pipelineBubbleEl;
|
|
365
|
-
const steps = bubble.querySelectorAll(".pipeline-step");
|
|
366
|
-
const lastStep = steps[steps.length - 1];
|
|
367
|
-
if (lastStep) {
|
|
368
|
-
const detail = document.createElement("div");
|
|
369
|
-
detail.className = "pipeline-step__decision";
|
|
370
|
-
detail.textContent = msg.decision;
|
|
371
|
-
lastStep.appendChild(detail);
|
|
372
|
-
}
|
|
509
|
+
setPipelineDetail(msg.decision);
|
|
373
510
|
scrollToBottom();
|
|
374
511
|
}
|
|
375
512
|
|
|
@@ -379,9 +516,18 @@ function handleModuleProgress(msg) {
|
|
|
379
516
|
let card = pipelineModulesEl.querySelector(`[data-module="${CSS.escape(msg.module)}"]`);
|
|
380
517
|
if (!card) {
|
|
381
518
|
card = document.createElement("div");
|
|
382
|
-
card.className = "pipeline-module-card";
|
|
383
519
|
card.dataset.module = msg.module;
|
|
384
|
-
card.innerHTML =
|
|
520
|
+
card.innerHTML = `
|
|
521
|
+
<div class="pipeline-module-card__head">
|
|
522
|
+
<span class="pipeline-module-card__name">${escapeHtml(msg.module)}</span>
|
|
523
|
+
<span class="pipeline-module-card__status"></span>
|
|
524
|
+
</div>
|
|
525
|
+
<div class="pipeline-module-card__progress"><div class="pipeline-module-card__progress-bar"></div></div>
|
|
526
|
+
<div class="pipeline-module-card__skeleton">
|
|
527
|
+
<div class="pipeline-module-card__skeleton-row pipeline-module-card__skeleton-row--lg"></div>
|
|
528
|
+
<div class="pipeline-module-card__skeleton-row pipeline-module-card__skeleton-row--md"></div>
|
|
529
|
+
<div class="pipeline-module-card__skeleton-row pipeline-module-card__skeleton-row--sm"></div>
|
|
530
|
+
</div>`;
|
|
385
531
|
pipelineModulesEl.appendChild(card);
|
|
386
532
|
}
|
|
387
533
|
|
|
@@ -390,39 +536,99 @@ function handleModuleProgress(msg) {
|
|
|
390
536
|
|
|
391
537
|
const statusLabels = {
|
|
392
538
|
queued: "queued",
|
|
393
|
-
generating: "generating
|
|
394
|
-
validating: "validating
|
|
395
|
-
retrying: "retrying
|
|
396
|
-
complete: "
|
|
397
|
-
failed: "
|
|
539
|
+
generating: "generating",
|
|
540
|
+
validating: "validating",
|
|
541
|
+
retrying: "retrying",
|
|
542
|
+
complete: "done",
|
|
543
|
+
failed: "failed",
|
|
398
544
|
};
|
|
399
|
-
statusEl.textContent = statusLabels[msg.status] || msg.status;
|
|
545
|
+
if (statusEl) statusEl.textContent = statusLabels[msg.status] || msg.status;
|
|
400
546
|
|
|
401
|
-
// Mark/clear working overlay in the preview
|
|
402
547
|
if (msg.status === "generating" && typeof markModulesWorking === "function") {
|
|
403
548
|
markModulesWorking([msg.module]);
|
|
404
549
|
} else if ((msg.status === "complete" || msg.status === "failed") && typeof clearModuleWorking === "function") {
|
|
405
550
|
clearModuleWorking(msg.module);
|
|
406
551
|
}
|
|
407
552
|
|
|
553
|
+
updatePipelineEstimate();
|
|
554
|
+
|
|
555
|
+
if (msg.status === "complete" && msg.module) {
|
|
556
|
+
changedModulesInRun.add(msg.module);
|
|
557
|
+
}
|
|
558
|
+
|
|
408
559
|
scrollToBottom();
|
|
409
560
|
}
|
|
410
561
|
|
|
562
|
+
function startPipelineEstimate() {
|
|
563
|
+
stopPipelineEstimate();
|
|
564
|
+
if (!pipelineEstimateEl) return;
|
|
565
|
+
pipelineEstimateEl.hidden = false;
|
|
566
|
+
updatePipelineEstimate();
|
|
567
|
+
pipelineEstimateInterval = setInterval(updatePipelineEstimate, 1000);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function stopPipelineEstimate() {
|
|
571
|
+
if (pipelineEstimateInterval) {
|
|
572
|
+
clearInterval(pipelineEstimateInterval);
|
|
573
|
+
pipelineEstimateInterval = null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function updatePipelineEstimate() {
|
|
578
|
+
if (!pipelineEstimateEl || pipelineCurrentStep !== "developing" || !pipelineModulesEl) return;
|
|
579
|
+
const cards = pipelineModulesEl.querySelectorAll(".pipeline-module-card");
|
|
580
|
+
if (!cards.length) {
|
|
581
|
+
pipelineEstimateEl.textContent = "Estimating…";
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
let remaining = 0;
|
|
585
|
+
let inFlight = 0;
|
|
586
|
+
cards.forEach((c) => {
|
|
587
|
+
if (c.classList.contains("pipeline-module-card--complete") || c.classList.contains("pipeline-module-card--failed")) return;
|
|
588
|
+
remaining++;
|
|
589
|
+
if (
|
|
590
|
+
c.classList.contains("pipeline-module-card--generating") ||
|
|
591
|
+
c.classList.contains("pipeline-module-card--validating") ||
|
|
592
|
+
c.classList.contains("pipeline-module-card--retrying")
|
|
593
|
+
) {
|
|
594
|
+
inFlight++;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
if (remaining === 0) {
|
|
598
|
+
pipelineEstimateEl.textContent = "Wrapping up…";
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
// Default concurrency limiter is 20 modules, but most chats only have a
|
|
602
|
+
// handful — assume 4-way effective parallelism for the estimate.
|
|
603
|
+
const parallel = Math.max(1, Math.min(4, inFlight || 1));
|
|
604
|
+
const seconds = Math.max(5, Math.ceil((remaining * ESTIMATED_SECONDS_PER_MODULE) / parallel));
|
|
605
|
+
pipelineEstimateEl.textContent = `~${formatEstimate(seconds)} remaining`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function formatEstimate(totalSec) {
|
|
609
|
+
if (totalSec < 60) return totalSec + "s";
|
|
610
|
+
const m = Math.floor(totalSec / 60);
|
|
611
|
+
const s = totalSec % 60;
|
|
612
|
+
if (s === 0) return m + "m";
|
|
613
|
+
return m + "m " + s + "s";
|
|
614
|
+
}
|
|
615
|
+
|
|
411
616
|
function handlePipelineComplete(msg) {
|
|
412
617
|
if (!pipelineBubbleEl) return;
|
|
413
618
|
|
|
414
619
|
stopPipelineTimer();
|
|
620
|
+
stopPipelineEstimate();
|
|
415
621
|
if (typeof clearAllModulesWorking === "function") clearAllModulesWorking();
|
|
416
622
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
623
|
+
STEP_ORDER.forEach((s) => setStageStatus(s, "done"));
|
|
624
|
+
pipelineCurrentStep = null;
|
|
625
|
+
setPipelineDetail("");
|
|
626
|
+
|
|
627
|
+
const footer = pipelineBubbleEl.querySelector(".pipeline-footer");
|
|
628
|
+
if (footer) footer.remove();
|
|
420
629
|
|
|
421
|
-
// Mark all steps as done (search whole bubble since quality_check is outside pipelineStepsEl)
|
|
422
630
|
const bubble = streamingMsgEl || pipelineBubbleEl;
|
|
423
|
-
bubble.querySelectorAll(".pipeline-step").forEach((el) => markStepDone(el));
|
|
424
631
|
|
|
425
|
-
// Show answer text for question intents
|
|
426
632
|
if (msg.answer) {
|
|
427
633
|
const answerEl = document.createElement("div");
|
|
428
634
|
answerEl.className = "pipeline-answer";
|
|
@@ -430,69 +636,162 @@ function handlePipelineComplete(msg) {
|
|
|
430
636
|
bubble.appendChild(answerEl);
|
|
431
637
|
}
|
|
432
638
|
|
|
433
|
-
// Add completion stats after the last element in the bubble
|
|
434
639
|
const stats = document.createElement("div");
|
|
435
640
|
stats.className = "pipeline-stats";
|
|
436
641
|
const duration = formatDuration(msg.durationMs);
|
|
437
642
|
if (msg.answer) {
|
|
438
|
-
// For questions, just show duration
|
|
439
643
|
stats.textContent = `Answered in ${duration}`;
|
|
440
644
|
} else {
|
|
441
|
-
stats.textContent = `Generated ${msg.modulesGenerated}
|
|
645
|
+
stats.textContent = `Generated ${msg.modulesGenerated} section${msg.modulesGenerated === 1 ? "" : "s"} in ${duration}`;
|
|
442
646
|
if (msg.modulesUnchanged > 0) {
|
|
443
647
|
stats.textContent += ` (${msg.modulesUnchanged} unchanged)`;
|
|
444
648
|
}
|
|
445
649
|
}
|
|
446
|
-
|
|
447
|
-
const qualityStep = bubble.querySelector('[data-step="quality_check"]');
|
|
448
|
-
if (qualityStep) {
|
|
449
|
-
qualityStep.after(stats);
|
|
450
|
-
} else if (pipelineModulesEl) {
|
|
451
|
-
pipelineModulesEl.after(stats);
|
|
452
|
-
} else {
|
|
453
|
-
bubble.appendChild(stats);
|
|
454
|
-
}
|
|
650
|
+
bubble.appendChild(stats);
|
|
455
651
|
|
|
456
652
|
clearStreamStatus();
|
|
457
653
|
finishStreaming();
|
|
458
654
|
|
|
459
|
-
|
|
460
|
-
pipelineBubbleEl = null;
|
|
461
|
-
pipelineStepsEl = null;
|
|
462
|
-
pipelineModulesEl = null;
|
|
655
|
+
resetPipelineState();
|
|
463
656
|
}
|
|
464
657
|
|
|
465
658
|
function handlePipelinePartial(msg) {
|
|
466
659
|
if (!pipelineBubbleEl) return;
|
|
467
660
|
|
|
468
661
|
stopPipelineTimer();
|
|
662
|
+
stopPipelineEstimate();
|
|
469
663
|
if (typeof clearAllModulesWorking === "function") clearAllModulesWorking();
|
|
470
664
|
|
|
471
|
-
|
|
472
|
-
|
|
665
|
+
STEP_ORDER.forEach((s) => setStageStatus(s, "done"));
|
|
666
|
+
pipelineCurrentStep = null;
|
|
667
|
+
setPipelineDetail("");
|
|
668
|
+
|
|
669
|
+
const footer = pipelineBubbleEl.querySelector(".pipeline-footer");
|
|
670
|
+
if (footer) footer.remove();
|
|
473
671
|
|
|
474
672
|
const bubble = streamingMsgEl || pipelineBubbleEl;
|
|
475
|
-
bubble.querySelectorAll(".pipeline-step").forEach((el) => markStepDone(el));
|
|
476
673
|
|
|
477
674
|
const stats = document.createElement("div");
|
|
478
675
|
stats.className = "pipeline-stats pipeline-stats--partial";
|
|
479
676
|
const duration = formatDuration(msg.durationMs);
|
|
480
|
-
stats.textContent = `${msg.succeeded.length}
|
|
481
|
-
|
|
482
|
-
if (qualityStep) {
|
|
483
|
-
qualityStep.after(stats);
|
|
484
|
-
} else if (pipelineModulesEl) {
|
|
485
|
-
pipelineModulesEl.after(stats);
|
|
486
|
-
} else {
|
|
487
|
-
pipelineStepsEl.appendChild(stats);
|
|
488
|
-
}
|
|
677
|
+
stats.textContent = `${msg.succeeded.length} sections succeeded, ${msg.failed.length} failed in ${duration}`;
|
|
678
|
+
bubble.appendChild(stats);
|
|
489
679
|
|
|
490
680
|
clearStreamStatus();
|
|
491
681
|
finishStreaming();
|
|
492
682
|
|
|
683
|
+
resetPipelineState();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function resetPipelineState() {
|
|
493
687
|
pipelineBubbleEl = null;
|
|
494
|
-
|
|
688
|
+
pipelineStepperEl = null;
|
|
689
|
+
pipelineDetailEl = null;
|
|
495
690
|
pipelineModulesEl = null;
|
|
691
|
+
pipelineEstimateEl = null;
|
|
692
|
+
pipelineCurrentStep = null;
|
|
693
|
+
|
|
694
|
+
renderChatSuggestions();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
// Smart chat suggestions — contextual next-step chips after generation
|
|
699
|
+
// ---------------------------------------------------------------------------
|
|
700
|
+
|
|
701
|
+
let suggestionsVisible = false;
|
|
702
|
+
|
|
703
|
+
function getCurrentModuleNames() {
|
|
704
|
+
return Array.from(document.querySelectorAll("#module-items .module-item"))
|
|
705
|
+
.map((el) => (el.dataset.module || "").toLowerCase());
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function pickContextualSuggestions(moduleNames) {
|
|
709
|
+
const has = (kw) => moduleNames.some((n) => n.includes(kw));
|
|
710
|
+
const additive = [];
|
|
711
|
+
|
|
712
|
+
if (!has("testimonial") && !has("review") && !has("quote")) {
|
|
713
|
+
additive.push("Add a testimonials section");
|
|
714
|
+
}
|
|
715
|
+
if (!has("pricing") && !has("plan") && !has("tier")) {
|
|
716
|
+
additive.push("Add a pricing table");
|
|
717
|
+
}
|
|
718
|
+
if (!has("faq") && !has("question")) {
|
|
719
|
+
additive.push("Add an FAQ section");
|
|
720
|
+
}
|
|
721
|
+
if (!has("contact") && !has("form")) {
|
|
722
|
+
additive.push("Add a contact form");
|
|
723
|
+
}
|
|
724
|
+
if (!has("hero") && !has("banner")) {
|
|
725
|
+
additive.push("Add a hero section");
|
|
726
|
+
}
|
|
727
|
+
if (!has("feature") && !has("benefit")) {
|
|
728
|
+
additive.push("Add a features grid with icons");
|
|
729
|
+
}
|
|
730
|
+
if (!has("footer")) {
|
|
731
|
+
additive.push("Add a footer with social links");
|
|
732
|
+
}
|
|
733
|
+
if (!has("stat") && !has("metric") && !has("number")) {
|
|
734
|
+
additive.push("Add a stats section with key numbers");
|
|
735
|
+
}
|
|
736
|
+
if (!has("cta") && !has("call")) {
|
|
737
|
+
additive.push("Add a call-to-action section");
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const refinements = [
|
|
741
|
+
has("hero") || has("banner")
|
|
742
|
+
? "Make the hero CTA more prominent"
|
|
743
|
+
: "Strengthen the opening hook",
|
|
744
|
+
"Change the color scheme to something bolder",
|
|
745
|
+
"Refine the typography for a more modern feel",
|
|
746
|
+
"Add subtle animations to bring it to life",
|
|
747
|
+
];
|
|
748
|
+
|
|
749
|
+
const out = [];
|
|
750
|
+
for (const s of additive) {
|
|
751
|
+
if (out.length >= 3) break;
|
|
752
|
+
out.push(s);
|
|
753
|
+
}
|
|
754
|
+
for (const r of refinements) {
|
|
755
|
+
if (out.length >= 3) break;
|
|
756
|
+
out.push(r);
|
|
757
|
+
}
|
|
758
|
+
return out;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function renderChatSuggestions() {
|
|
762
|
+
const container = document.getElementById("chat-suggestions");
|
|
763
|
+
if (!container) return;
|
|
764
|
+
|
|
765
|
+
if (isStreaming || (inputEl && inputEl.value && inputEl.value.length > 0)) {
|
|
766
|
+
hideChatSuggestions();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const suggestions = pickContextualSuggestions(getCurrentModuleNames());
|
|
771
|
+
if (!suggestions.length) {
|
|
772
|
+
hideChatSuggestions();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
container.innerHTML = "";
|
|
777
|
+
for (const text of suggestions) {
|
|
778
|
+
const btn = document.createElement("button");
|
|
779
|
+
btn.type = "button";
|
|
780
|
+
btn.className = "chat__suggestion-chip";
|
|
781
|
+
btn.dataset.suggestion = text;
|
|
782
|
+
btn.textContent = text;
|
|
783
|
+
container.appendChild(btn);
|
|
784
|
+
}
|
|
785
|
+
container.classList.remove("hidden");
|
|
786
|
+
suggestionsVisible = true;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function hideChatSuggestions() {
|
|
790
|
+
const container = document.getElementById("chat-suggestions");
|
|
791
|
+
if (!container) return;
|
|
792
|
+
container.classList.add("hidden");
|
|
793
|
+
container.innerHTML = "";
|
|
794
|
+
suggestionsVisible = false;
|
|
496
795
|
}
|
|
497
796
|
|
|
498
797
|
async function handleAgenticPrompt() {
|
|
@@ -504,12 +803,12 @@ async function handleAgenticPrompt() {
|
|
|
504
803
|
"vibeSpot can decompose AI generation into specialized agents:\n\n" +
|
|
505
804
|
"• Intent Analyzer — classifies your request\n" +
|
|
506
805
|
"• Page Architect — designs the page structure\n" +
|
|
507
|
-
"•
|
|
806
|
+
"• Section Developer — generates each section in parallel\n" +
|
|
508
807
|
"• Validator — checks and auto-fixes errors\n\n" +
|
|
509
808
|
"Tradeoffs:\n" +
|
|
510
809
|
"✓ Better quality — each agent is focused on one task\n" +
|
|
511
810
|
"✓ Structured output — eliminates JSON parsing failures\n" +
|
|
512
|
-
"✓ Only changed
|
|
811
|
+
"✓ Only changed sections regenerated on edits\n" +
|
|
513
812
|
"✗ Uses more calls per request (API calls or CLI subprocess calls)\n\n" +
|
|
514
813
|
"You can change this anytime in Settings.",
|
|
515
814
|
"Use Agentic Pipeline",
|
|
@@ -757,6 +1056,15 @@ async function sendMessage(text) {
|
|
|
757
1056
|
// Show user message with file chips
|
|
758
1057
|
appendUserMessage(text, null, uploadedFiles);
|
|
759
1058
|
|
|
1059
|
+
// Snapshot the current module list so we can detect new vs. modified modules
|
|
1060
|
+
// when the generation finishes. Clears stale change-indicator dots from any
|
|
1061
|
+
// previous run so the user only sees indicators for the run they just kicked.
|
|
1062
|
+
modulesBeforeRun = new Set(
|
|
1063
|
+
Array.from(document.querySelectorAll(".module-item")).map((el) => el.dataset.module),
|
|
1064
|
+
);
|
|
1065
|
+
changedModulesInRun = new Set();
|
|
1066
|
+
clearModuleListChanged();
|
|
1067
|
+
|
|
760
1068
|
// Start streaming indicator
|
|
761
1069
|
startStreaming();
|
|
762
1070
|
|
|
@@ -808,6 +1116,12 @@ function startStreaming() {
|
|
|
808
1116
|
lastStreamStatus = "";
|
|
809
1117
|
sendBtn.disabled = true;
|
|
810
1118
|
streamStartTime = Date.now();
|
|
1119
|
+
if (typeof window.setSelectModeDisabled === "function") {
|
|
1120
|
+
window.setSelectModeDisabled(true);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
hideChatSuggestions();
|
|
1124
|
+
if (typeof updateHistoryTimelineNavState === "function") updateHistoryTimelineNavState();
|
|
811
1125
|
|
|
812
1126
|
// Don't show generating preview here — agentic mode keeps the page visible.
|
|
813
1127
|
// For single-call mode, showGeneratingPreview() is called on first "stream" event.
|
|
@@ -950,6 +1264,10 @@ function finishStreaming() {
|
|
|
950
1264
|
if (!isStreaming) return;
|
|
951
1265
|
isStreaming = false;
|
|
952
1266
|
sendBtn.disabled = false;
|
|
1267
|
+
if (typeof window.setSelectModeDisabled === "function") {
|
|
1268
|
+
window.setSelectModeDisabled(false);
|
|
1269
|
+
}
|
|
1270
|
+
if (typeof updateHistoryTimelineNavState === "function") updateHistoryTimelineNavState();
|
|
953
1271
|
|
|
954
1272
|
// Stop the timer and capture duration
|
|
955
1273
|
stopStreamTimer();
|
|
@@ -975,7 +1293,7 @@ function finishStreaming() {
|
|
|
975
1293
|
if (streamingMsgEl && streamBuffer) {
|
|
976
1294
|
const rendered = renderMarkdown(streamBuffer);
|
|
977
1295
|
const visibleText = rendered.replace(/<[^>]*>/g, "").trim();
|
|
978
|
-
streamingMsgEl.innerHTML = visibleText ? rendered : "<em>
|
|
1296
|
+
streamingMsgEl.innerHTML = visibleText ? rendered : "<em>Sections applied.</em>";
|
|
979
1297
|
}
|
|
980
1298
|
|
|
981
1299
|
streamingMsgEl = null;
|
|
@@ -1081,25 +1399,44 @@ function appendRestoredAssistantMessage(text, timestamp, pipeline) {
|
|
|
1081
1399
|
div.className = "chat-msg chat-msg--assistant";
|
|
1082
1400
|
|
|
1083
1401
|
if (pipeline && pipeline.steps && pipeline.steps.length > 0) {
|
|
1084
|
-
//
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1402
|
+
// Finalized stepper — every visited stage shown as done
|
|
1403
|
+
const stagesSeen = new Set(pipeline.steps.map((s) => s.step));
|
|
1404
|
+
const stepperParts = [];
|
|
1405
|
+
for (let i = 0; i < STEP_ORDER.length; i++) {
|
|
1406
|
+
const step = STEP_ORDER[i];
|
|
1407
|
+
const visited = stagesSeen.has(step);
|
|
1408
|
+
const stateClass = visited ? "pipeline-stage--done" : "pipeline-stage--pending";
|
|
1409
|
+
stepperParts.push(`
|
|
1410
|
+
<div class="pipeline-stage ${stateClass}" data-stage="${step}">
|
|
1411
|
+
<div class="pipeline-stage__indicator">
|
|
1412
|
+
<span class="pipeline-stage__icon">${STEP_ICONS[step]}</span>
|
|
1413
|
+
<span class="pipeline-stage__check">${CHECK_ICON}</span>
|
|
1414
|
+
</div>
|
|
1415
|
+
<div class="pipeline-stage__label">${STEP_LABELS[step]}</div>
|
|
1416
|
+
</div>`);
|
|
1417
|
+
if (i < STEP_ORDER.length - 1) {
|
|
1418
|
+
const filled = visited && stagesSeen.has(STEP_ORDER[i + 1]) ? "pipeline-stage__connector--filled" : "";
|
|
1419
|
+
stepperParts.push(`<div class="pipeline-stage__connector ${filled}" data-after="${step}"></div>`);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const allDecisions = pipeline.steps.flatMap((s) => s.decisions || []);
|
|
1424
|
+
const decisionHtml = allDecisions.length
|
|
1425
|
+
? `<details class="pipeline-detail-summary"><summary>${allDecisions.length} decision${allDecisions.length === 1 ? "" : "s"} logged</summary>${allDecisions.map((d) => `<div class="pipeline-detail-summary__row">${escapeHtml(d)}</div>`).join("")}</details>`
|
|
1426
|
+
: "";
|
|
1092
1427
|
|
|
1093
1428
|
const modulesHtml = pipeline.modules && pipeline.modules.length > 0
|
|
1094
|
-
? pipeline
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1429
|
+
? `<div class="pipeline-modules pipeline-modules--restored">${pipeline.modules
|
|
1430
|
+
.map((m) => {
|
|
1431
|
+
const statusClass = m.status === "failed" ? "pipeline-module-card--failed" : "pipeline-module-card--complete";
|
|
1432
|
+
const statusLabel = m.status === "failed" ? "failed" : "done";
|
|
1433
|
+
return `<div class="pipeline-module-card ${statusClass}"><div class="pipeline-module-card__head"><span class="pipeline-module-card__name">${escapeHtml(m.name)}</span><span class="pipeline-module-card__status">${statusLabel}</span></div></div>`;
|
|
1434
|
+
})
|
|
1435
|
+
.join("")}</div>`
|
|
1099
1436
|
: "";
|
|
1100
1437
|
|
|
1101
1438
|
const duration = formatDuration(pipeline.stats.durationMs);
|
|
1102
|
-
let statsText = `Generated ${pipeline.stats.modulesGenerated}
|
|
1439
|
+
let statsText = `Generated ${pipeline.stats.modulesGenerated} section${pipeline.stats.modulesGenerated === 1 ? "" : "s"} in ${duration}`;
|
|
1103
1440
|
if (pipeline.stats.modulesUnchanged > 0) {
|
|
1104
1441
|
statsText += ` (${pipeline.stats.modulesUnchanged} unchanged)`;
|
|
1105
1442
|
}
|
|
@@ -1110,7 +1447,10 @@ function appendRestoredAssistantMessage(text, timestamp, pipeline) {
|
|
|
1110
1447
|
<div class="chat-msg__content">
|
|
1111
1448
|
${time ? `<div class="chat-msg__header"><span class="chat-msg__sender">vibeSpot AI</span><span class="chat-msg__time">${time}</span></div>` : ""}
|
|
1112
1449
|
<div class="chat-msg__bubble">
|
|
1113
|
-
<div class="pipeline-
|
|
1450
|
+
<div class="pipeline-stepper">${stepperParts.join("")}</div>
|
|
1451
|
+
${decisionHtml}
|
|
1452
|
+
${modulesHtml}
|
|
1453
|
+
<div class="${statsClass}">${statsText}</div>
|
|
1114
1454
|
</div>
|
|
1115
1455
|
</div>`;
|
|
1116
1456
|
} else {
|
|
@@ -1234,7 +1574,7 @@ function attachHistoryToggle() {
|
|
|
1234
1574
|
async function doRollback(hash) {
|
|
1235
1575
|
const scoped = currentTemplateId && !historyShowAll;
|
|
1236
1576
|
const msg = scoped
|
|
1237
|
-
? "This template's
|
|
1577
|
+
? "This template's sections will be restored to the selected version. Other templates are not affected."
|
|
1238
1578
|
: "All theme files will be replaced, but chat history is preserved.";
|
|
1239
1579
|
const ok = await vibeConfirm("Restore this version?", msg, { confirmLabel: "Restore", confirmClass: "btn--primary" });
|
|
1240
1580
|
if (!ok) return;
|
|
@@ -1278,6 +1618,290 @@ function timeAgoShort(timestamp) {
|
|
|
1278
1618
|
return days + "d";
|
|
1279
1619
|
}
|
|
1280
1620
|
|
|
1621
|
+
// ---------------------------------------------------------------------------
|
|
1622
|
+
// History timeline — compact strip above chat input with undo/redo support
|
|
1623
|
+
// ---------------------------------------------------------------------------
|
|
1624
|
+
|
|
1625
|
+
let historyGitAvailable = false;
|
|
1626
|
+
// Index into historyTimelineEntries (filtered list, newest first) representing
|
|
1627
|
+
// the version the user is currently viewing. 0 = head, N = N steps back.
|
|
1628
|
+
let historyTimelineCursor = 0;
|
|
1629
|
+
let historyTimelineEntries = [];
|
|
1630
|
+
let historyTimelineRestoring = false;
|
|
1631
|
+
|
|
1632
|
+
function shortenCommitMessage(msg) {
|
|
1633
|
+
if (!msg) return "Update";
|
|
1634
|
+
let s = msg;
|
|
1635
|
+
// Strip [templateId] prefix
|
|
1636
|
+
const prefix = s.match(/^\[[^\]]+\]\s*/);
|
|
1637
|
+
if (prefix) s = s.slice(prefix[0].length);
|
|
1638
|
+
// Strip leading "Rollback to: "
|
|
1639
|
+
s = s.replace(/^Rollback to:\s*/i, "");
|
|
1640
|
+
if (s.length > 32) s = s.slice(0, 31) + "…";
|
|
1641
|
+
return s;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function stripCommitPrefix(msg) {
|
|
1645
|
+
return (msg || "").replace(/^\[[^\]]+\]\s*/, "");
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// Compute which timeline entry represents the *currently visible* version.
|
|
1649
|
+
// HEAD may be a "Rollback to: <original>" commit (an internal marker we filter
|
|
1650
|
+
// out of the visible timeline); in that case, we map back to the original
|
|
1651
|
+
// entry by message. Otherwise the cursor sits at HEAD itself.
|
|
1652
|
+
function computeCursorFromHead(commits, entries) {
|
|
1653
|
+
if (!commits.length || !entries.length) return 0;
|
|
1654
|
+
const headMsg = stripCommitPrefix(commits[0].message);
|
|
1655
|
+
if (/^rollback to:\s*/i.test(headMsg)) {
|
|
1656
|
+
const orig = headMsg.replace(/^rollback to:\s*/i, "").trim().replace(/\.{3}$/, "");
|
|
1657
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1658
|
+
const em = stripCommitPrefix(entries[i].message);
|
|
1659
|
+
if (em === orig || em.startsWith(orig)) return i;
|
|
1660
|
+
}
|
|
1661
|
+
return 0;
|
|
1662
|
+
}
|
|
1663
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1664
|
+
if (entries[i].fullHash === commits[0].fullHash) return i;
|
|
1665
|
+
}
|
|
1666
|
+
return 0;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
async function refreshHistoryTimeline() {
|
|
1670
|
+
const tl = document.getElementById("history-timeline");
|
|
1671
|
+
const track = document.getElementById("history-timeline-track");
|
|
1672
|
+
if (!tl || !track) return;
|
|
1673
|
+
|
|
1674
|
+
if (!historyGitAvailable) {
|
|
1675
|
+
tl.classList.add("hidden");
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
try {
|
|
1680
|
+
const useFilter = currentTemplateId;
|
|
1681
|
+
const url = useFilter
|
|
1682
|
+
? `/api/history?templateId=${encodeURIComponent(currentTemplateId)}`
|
|
1683
|
+
: "/api/history";
|
|
1684
|
+
const res = await fetch(url);
|
|
1685
|
+
const data = await res.json();
|
|
1686
|
+
if (!data.available) {
|
|
1687
|
+
tl.classList.add("hidden");
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const allCommits = data.commits || [];
|
|
1692
|
+
// Filter out automatic "Rollback to:" commits — they're internal markers.
|
|
1693
|
+
// The user navigates the conceptual history of generation events.
|
|
1694
|
+
const entries = allCommits.filter((c) => {
|
|
1695
|
+
return !/^rollback to:\s*/i.test(stripCommitPrefix(c.message));
|
|
1696
|
+
});
|
|
1697
|
+
historyTimelineEntries = entries;
|
|
1698
|
+
|
|
1699
|
+
if (entries.length === 0) {
|
|
1700
|
+
tl.classList.add("hidden");
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
historyTimelineCursor = computeCursorFromHead(allCommits, entries);
|
|
1705
|
+
|
|
1706
|
+
tl.classList.remove("hidden");
|
|
1707
|
+
track.innerHTML = "";
|
|
1708
|
+
const frag = document.createDocumentFragment();
|
|
1709
|
+
entries.forEach((commit, idx) => {
|
|
1710
|
+
const isInitial = (commit.message || "").startsWith("Initial ");
|
|
1711
|
+
const item = document.createElement("button");
|
|
1712
|
+
item.type = "button";
|
|
1713
|
+
item.className = "history-timeline__entry"
|
|
1714
|
+
+ (idx === historyTimelineCursor ? " history-timeline__entry--current" : "")
|
|
1715
|
+
+ (isInitial ? " history-timeline__entry--initial" : "");
|
|
1716
|
+
item.dataset.hash = commit.fullHash;
|
|
1717
|
+
item.dataset.index = String(idx);
|
|
1718
|
+
item.innerHTML = `<span class="history-timeline__entry-dot"></span>`
|
|
1719
|
+
+ `<span class="history-timeline__entry-label"></span>`;
|
|
1720
|
+
item.querySelector(".history-timeline__entry-label").textContent = shortenCommitMessage(commit.message);
|
|
1721
|
+
item.title = ""; // we use a custom tooltip
|
|
1722
|
+
frag.appendChild(item);
|
|
1723
|
+
});
|
|
1724
|
+
track.appendChild(frag);
|
|
1725
|
+
|
|
1726
|
+
updateHistoryTimelineNavState();
|
|
1727
|
+
|
|
1728
|
+
// Scroll the current entry into view
|
|
1729
|
+
requestAnimationFrame(() => {
|
|
1730
|
+
const cur = track.querySelector(".history-timeline__entry--current");
|
|
1731
|
+
if (cur && typeof cur.scrollIntoView === "function") {
|
|
1732
|
+
cur.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
} catch {
|
|
1736
|
+
tl.classList.add("hidden");
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function updateHistoryTimelineNavState() {
|
|
1741
|
+
const undoBtn = document.getElementById("history-timeline-undo");
|
|
1742
|
+
const redoBtn = document.getElementById("history-timeline-redo");
|
|
1743
|
+
const max = historyTimelineEntries.length - 1;
|
|
1744
|
+
const busy = historyTimelineRestoring || (typeof isStreaming !== "undefined" && isStreaming);
|
|
1745
|
+
if (undoBtn) undoBtn.disabled = busy || historyTimelineCursor >= max;
|
|
1746
|
+
if (redoBtn) redoBtn.disabled = busy || historyTimelineCursor <= 0;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
async function restoreToTimelineIndex(idx) {
|
|
1750
|
+
if (historyTimelineRestoring) return;
|
|
1751
|
+
if (idx < 0 || idx >= historyTimelineEntries.length) return;
|
|
1752
|
+
if (typeof isStreaming !== "undefined" && isStreaming) return;
|
|
1753
|
+
const target = historyTimelineEntries[idx];
|
|
1754
|
+
if (!target) return;
|
|
1755
|
+
|
|
1756
|
+
historyTimelineRestoring = true;
|
|
1757
|
+
updateHistoryTimelineNavState();
|
|
1758
|
+
setStatus("Restoring…");
|
|
1759
|
+
|
|
1760
|
+
try {
|
|
1761
|
+
const payload = { hash: target.fullHash };
|
|
1762
|
+
if (currentTemplateId) payload.templateId = currentTemplateId;
|
|
1763
|
+
const res = await fetch("/api/rollback", {
|
|
1764
|
+
method: "POST",
|
|
1765
|
+
headers: { "Content-Type": "application/json" },
|
|
1766
|
+
body: JSON.stringify(payload),
|
|
1767
|
+
});
|
|
1768
|
+
const data = await res.json();
|
|
1769
|
+
if (data.error) {
|
|
1770
|
+
await vibeAlert(data.error, "Restore failed");
|
|
1771
|
+
setStatus("Ready");
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (data.modules) updateModuleList(data.modules);
|
|
1775
|
+
refreshPreview();
|
|
1776
|
+
// Refresh from server — the new HEAD is a "Rollback to:" commit pointing
|
|
1777
|
+
// at the entry we just restored; computeCursorFromHead resolves it back.
|
|
1778
|
+
await refreshHistoryTimeline();
|
|
1779
|
+
if (historyPanelOpen) refreshHistoryPanel();
|
|
1780
|
+
setStatus("Ready");
|
|
1781
|
+
} catch (err) {
|
|
1782
|
+
await vibeAlert(err.message || "Restore failed", "Restore failed");
|
|
1783
|
+
setStatus("Ready");
|
|
1784
|
+
} finally {
|
|
1785
|
+
historyTimelineRestoring = false;
|
|
1786
|
+
updateHistoryTimelineNavState();
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function timelineUndo() {
|
|
1791
|
+
restoreToTimelineIndex(historyTimelineCursor + 1);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function timelineRedo() {
|
|
1795
|
+
restoreToTimelineIndex(historyTimelineCursor - 1);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// ---- Tooltip on hover ------------------------------------------------------
|
|
1799
|
+
|
|
1800
|
+
function showTimelineTooltip(entryEl) {
|
|
1801
|
+
const tooltip = document.getElementById("history-timeline-tooltip");
|
|
1802
|
+
const tl = document.getElementById("history-timeline");
|
|
1803
|
+
if (!tooltip || !tl || !entryEl) return;
|
|
1804
|
+
const idx = parseInt(entryEl.dataset.index || "-1", 10);
|
|
1805
|
+
const commit = historyTimelineEntries[idx];
|
|
1806
|
+
if (!commit) return;
|
|
1807
|
+
|
|
1808
|
+
const modules = (commit.changedModules || []).slice(0, 5);
|
|
1809
|
+
const more = (commit.changedModules || []).length - modules.length;
|
|
1810
|
+
const modulesLine = modules.length
|
|
1811
|
+
? modules.join(", ") + (more > 0 ? ` +${more} more` : "")
|
|
1812
|
+
: "No section changes";
|
|
1813
|
+
|
|
1814
|
+
// Strip [templateId] prefix for display; keep "Rollback to:" prefix because
|
|
1815
|
+
// that case is filtered out earlier and won't reach here.
|
|
1816
|
+
let displayMsg = commit.message || "";
|
|
1817
|
+
const prefix = displayMsg.match(/^\[[^\]]+\]\s*/);
|
|
1818
|
+
if (prefix) displayMsg = displayMsg.slice(prefix[0].length);
|
|
1819
|
+
|
|
1820
|
+
tooltip.innerHTML = `
|
|
1821
|
+
<div class="history-timeline__tooltip-title"></div>
|
|
1822
|
+
<div class="history-timeline__tooltip-meta"></div>
|
|
1823
|
+
<div class="history-timeline__tooltip-modules"></div>
|
|
1824
|
+
`;
|
|
1825
|
+
tooltip.querySelector(".history-timeline__tooltip-title").textContent = displayMsg || "Update";
|
|
1826
|
+
tooltip.querySelector(".history-timeline__tooltip-meta").textContent =
|
|
1827
|
+
`${commit.hash} · ${timeAgoShort(commit.timestamp)} ago`;
|
|
1828
|
+
tooltip.querySelector(".history-timeline__tooltip-modules").textContent = modulesLine;
|
|
1829
|
+
|
|
1830
|
+
// Position above the entry, clamped within the timeline strip.
|
|
1831
|
+
tooltip.classList.remove("hidden");
|
|
1832
|
+
const tlRect = tl.getBoundingClientRect();
|
|
1833
|
+
const entryRect = entryEl.getBoundingClientRect();
|
|
1834
|
+
const left = Math.max(8, entryRect.left - tlRect.left);
|
|
1835
|
+
const tooltipW = tooltip.offsetWidth;
|
|
1836
|
+
const maxLeft = Math.max(8, tl.clientWidth - tooltipW - 8);
|
|
1837
|
+
tooltip.style.left = Math.min(left, maxLeft) + "px";
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function hideTimelineTooltip() {
|
|
1841
|
+
const tooltip = document.getElementById("history-timeline-tooltip");
|
|
1842
|
+
if (tooltip) tooltip.classList.add("hidden");
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// ---- Wire up DOM events ----------------------------------------------------
|
|
1846
|
+
|
|
1847
|
+
(function wireHistoryTimeline() {
|
|
1848
|
+
const track = document.getElementById("history-timeline-track");
|
|
1849
|
+
const undoBtn = document.getElementById("history-timeline-undo");
|
|
1850
|
+
const redoBtn = document.getElementById("history-timeline-redo");
|
|
1851
|
+
|
|
1852
|
+
if (track) {
|
|
1853
|
+
track.addEventListener("click", (e) => {
|
|
1854
|
+
const entry = e.target.closest(".history-timeline__entry");
|
|
1855
|
+
if (!entry) return;
|
|
1856
|
+
const idx = parseInt(entry.dataset.index || "-1", 10);
|
|
1857
|
+
if (idx >= 0) restoreToTimelineIndex(idx);
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
let hoverTimer = null;
|
|
1861
|
+
track.addEventListener("mouseover", (e) => {
|
|
1862
|
+
const entry = e.target.closest(".history-timeline__entry");
|
|
1863
|
+
if (!entry) return;
|
|
1864
|
+
clearTimeout(hoverTimer);
|
|
1865
|
+
hoverTimer = setTimeout(() => showTimelineTooltip(entry), 200);
|
|
1866
|
+
});
|
|
1867
|
+
track.addEventListener("mouseout", (e) => {
|
|
1868
|
+
const entry = e.target.closest(".history-timeline__entry");
|
|
1869
|
+
if (!entry) return;
|
|
1870
|
+
clearTimeout(hoverTimer);
|
|
1871
|
+
hideTimelineTooltip();
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
if (undoBtn) undoBtn.addEventListener("click", timelineUndo);
|
|
1876
|
+
if (redoBtn) redoBtn.addEventListener("click", timelineRedo);
|
|
1877
|
+
|
|
1878
|
+
// Global Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z keyboard shortcuts. Skip when the
|
|
1879
|
+
// user is editing text so we never hijack native undo in inputs/editors.
|
|
1880
|
+
document.addEventListener("keydown", (e) => {
|
|
1881
|
+
const meta = e.metaKey || e.ctrlKey;
|
|
1882
|
+
if (!meta) return;
|
|
1883
|
+
const key = e.key.toLowerCase();
|
|
1884
|
+
if (key !== "z" && key !== "y") return;
|
|
1885
|
+
|
|
1886
|
+
const active = document.activeElement;
|
|
1887
|
+
if (active) {
|
|
1888
|
+
const tag = (active.tagName || "").toLowerCase();
|
|
1889
|
+
if (tag === "input" || tag === "textarea" || tag === "select") return;
|
|
1890
|
+
if (active.isContentEditable) return;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (!historyGitAvailable) return;
|
|
1894
|
+
|
|
1895
|
+
if (key === "z" && !e.shiftKey) {
|
|
1896
|
+
e.preventDefault();
|
|
1897
|
+
timelineUndo();
|
|
1898
|
+
} else if ((key === "z" && e.shiftKey) || key === "y") {
|
|
1899
|
+
e.preventDefault();
|
|
1900
|
+
timelineRedo();
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
})();
|
|
1904
|
+
|
|
1281
1905
|
// ---------------------------------------------------------------------------
|
|
1282
1906
|
// Module list
|
|
1283
1907
|
// ---------------------------------------------------------------------------
|
|
@@ -1289,17 +1913,26 @@ function updateModuleList(moduleNames) {
|
|
|
1289
1913
|
|
|
1290
1914
|
if (barCountEl) barCountEl.textContent = moduleNames.length;
|
|
1291
1915
|
if (slideoutCountEl) slideoutCountEl.textContent = moduleNames.length;
|
|
1916
|
+
|
|
1917
|
+
// Preserve which items were marked as recently changed across re-renders so
|
|
1918
|
+
// the dots survive the per-module `modules_updated` events that happen
|
|
1919
|
+
// mid-run.
|
|
1920
|
+
const previouslyChanged = new Set(
|
|
1921
|
+
Array.from(itemsEl.querySelectorAll(".module-item--changed")).map((el) => el.dataset.module),
|
|
1922
|
+
);
|
|
1292
1923
|
itemsEl.innerHTML = "";
|
|
1293
1924
|
|
|
1294
1925
|
for (const name of moduleNames) {
|
|
1295
1926
|
const item = document.createElement("div");
|
|
1296
1927
|
item.className = "module-item";
|
|
1928
|
+
if (previouslyChanged.has(name)) item.classList.add("module-item--changed");
|
|
1297
1929
|
item.dataset.module = name;
|
|
1298
1930
|
item.innerHTML = `
|
|
1299
1931
|
<span class="module-item__drag">⠿</span>
|
|
1932
|
+
<span class="module-item__changed-dot" aria-hidden="true"></span>
|
|
1300
1933
|
<span class="module-item__name">${escapeHtml(name)}</span>
|
|
1301
1934
|
<span class="module-item__edit" title="Edit fields">⚙</span>
|
|
1302
|
-
<span class="module-item__delete" title="Delete
|
|
1935
|
+
<span class="module-item__delete" title="Delete section">×</span>
|
|
1303
1936
|
`;
|
|
1304
1937
|
|
|
1305
1938
|
item.querySelector(".module-item__edit").addEventListener("click", (e) => {
|
|
@@ -1325,6 +1958,72 @@ function highlightModuleItem(name) {
|
|
|
1325
1958
|
});
|
|
1326
1959
|
}
|
|
1327
1960
|
|
|
1961
|
+
/**
|
|
1962
|
+
* Flush change highlights at the end of a generation run: refresh the preview
|
|
1963
|
+
* with the changed/new module sets, mark the sidebar items, and schedule the
|
|
1964
|
+
* 30-second auto-clear of the dots.
|
|
1965
|
+
*/
|
|
1966
|
+
function flushChangeHighlights(latestModules) {
|
|
1967
|
+
const changed = Array.from(changedModulesInRun);
|
|
1968
|
+
const before = modulesBeforeRun;
|
|
1969
|
+
changedModulesInRun = new Set();
|
|
1970
|
+
modulesBeforeRun = null;
|
|
1971
|
+
|
|
1972
|
+
let newModules = [];
|
|
1973
|
+
if (before) {
|
|
1974
|
+
if (changed.length > 0) {
|
|
1975
|
+
// Agentic pipeline: we know exactly which modules ran. New = ran AND
|
|
1976
|
+
// not in the pre-run snapshot.
|
|
1977
|
+
newModules = changed.filter((m) => !before.has(m));
|
|
1978
|
+
} else if (Array.isArray(latestModules)) {
|
|
1979
|
+
// Single-call mode emits no per-module events. Fall back to "names that
|
|
1980
|
+
// appeared since the run started" — we can't know which existing
|
|
1981
|
+
// modules were rewritten without server-side support.
|
|
1982
|
+
newModules = latestModules.filter((m) => !before.has(m));
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
refreshPreview({
|
|
1987
|
+
changedModules: changed.length > 0 ? changed : newModules,
|
|
1988
|
+
newModules,
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
// Sidebar dots only fire when we have explicit per-module change info; the
|
|
1992
|
+
// single-call fallback above only knows about new modules, which already
|
|
1993
|
+
// get the slide-in animation in the preview.
|
|
1994
|
+
if (changed.length > 0) {
|
|
1995
|
+
markModuleListChanged(changed);
|
|
1996
|
+
} else if (newModules.length > 0) {
|
|
1997
|
+
markModuleListChanged(newModules);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/** Apply the change indicator dot to each named module in the sidebar. */
|
|
2002
|
+
function markModuleListChanged(names) {
|
|
2003
|
+
if (!names || names.length === 0) return;
|
|
2004
|
+
const itemsEl = document.getElementById("module-items");
|
|
2005
|
+
if (!itemsEl) return;
|
|
2006
|
+
const set = new Set(names);
|
|
2007
|
+
itemsEl.querySelectorAll(".module-item").forEach((el) => {
|
|
2008
|
+
if (set.has(el.dataset.module)) el.classList.add("module-item--changed");
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
// Auto-clear after 30s per spec; next generation also clears via sendMessage.
|
|
2012
|
+
if (changedListClearTimer) clearTimeout(changedListClearTimer);
|
|
2013
|
+
changedListClearTimer = setTimeout(clearModuleListChanged, 30000);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/** Remove all change indicator dots from the sidebar. */
|
|
2017
|
+
function clearModuleListChanged() {
|
|
2018
|
+
if (changedListClearTimer) {
|
|
2019
|
+
clearTimeout(changedListClearTimer);
|
|
2020
|
+
changedListClearTimer = null;
|
|
2021
|
+
}
|
|
2022
|
+
document.querySelectorAll(".module-item--changed").forEach((el) => {
|
|
2023
|
+
el.classList.remove("module-item--changed");
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1328
2027
|
// ---------------------------------------------------------------------------
|
|
1329
2028
|
// Module slideout
|
|
1330
2029
|
// ---------------------------------------------------------------------------
|
|
@@ -1374,7 +2073,7 @@ async function toggleModuleLibraryDropdown() {
|
|
|
1374
2073
|
);
|
|
1375
2074
|
|
|
1376
2075
|
if (available.length === 0) {
|
|
1377
|
-
dropdown.innerHTML = `<div class="module-library-dropdown__empty">No other
|
|
2076
|
+
dropdown.innerHTML = `<div class="module-library-dropdown__empty">No other sections available</div>`;
|
|
1378
2077
|
} else {
|
|
1379
2078
|
dropdown.innerHTML = available.map((m) =>
|
|
1380
2079
|
`<button class="module-library-dropdown__item" data-name="${escapeHtml(m.moduleName)}">
|
|
@@ -1460,7 +2159,7 @@ function confirmDeleteModule(moduleName) {
|
|
|
1460
2159
|
overlay.innerHTML = `
|
|
1461
2160
|
<div class="confirm-dialog">
|
|
1462
2161
|
<div class="confirm-dialog__title">Remove "${escapeHtml(moduleName)}"?</div>
|
|
1463
|
-
<p class="confirm-dialog__detail">
|
|
2162
|
+
<p class="confirm-dialog__detail">Section will be removed from this page but kept in your library.</p>
|
|
1464
2163
|
<label class="confirm-dialog__toggle">
|
|
1465
2164
|
<span class="confirm-dialog__toggle-switch">
|
|
1466
2165
|
<input type="checkbox" data-role="toggle" />
|
|
@@ -1654,8 +2353,40 @@ inputEl.addEventListener("keydown", (e) => {
|
|
|
1654
2353
|
inputEl.addEventListener("input", () => {
|
|
1655
2354
|
inputEl.style.height = "auto";
|
|
1656
2355
|
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + "px";
|
|
2356
|
+
|
|
2357
|
+
// Hide suggestion chips as soon as the user starts typing
|
|
2358
|
+
if (suggestionsVisible && inputEl.value.length > 0) {
|
|
2359
|
+
hideChatSuggestions();
|
|
2360
|
+
}
|
|
1657
2361
|
});
|
|
1658
2362
|
|
|
2363
|
+
// Suggestion chip click — pre-fill input and focus, do not auto-send so the
|
|
2364
|
+
// user can edit before pressing Enter.
|
|
2365
|
+
document.getElementById("chat-suggestions")?.addEventListener("click", (e) => {
|
|
2366
|
+
const chip = e.target.closest(".chat__suggestion-chip");
|
|
2367
|
+
if (!chip) return;
|
|
2368
|
+
const text = chip.dataset.suggestion || chip.textContent || "";
|
|
2369
|
+
inputEl.value = text;
|
|
2370
|
+
inputEl.focus();
|
|
2371
|
+
inputEl.setSelectionRange(text.length, text.length);
|
|
2372
|
+
// Trigger the input event so the textarea resizes; this also collapses chips
|
|
2373
|
+
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2374
|
+
hideChatSuggestions();
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
// Pre-fill chat input from preview select-mode click
|
|
2378
|
+
window.prefillChatInput = function (text) {
|
|
2379
|
+
if (!text) return;
|
|
2380
|
+
const existing = inputEl.value;
|
|
2381
|
+
const prefix = existing.trim() ? existing.replace(/\s+$/, "") + "\n\n" : "";
|
|
2382
|
+
inputEl.value = prefix + text;
|
|
2383
|
+
inputEl.style.height = "auto";
|
|
2384
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + "px";
|
|
2385
|
+
inputEl.focus();
|
|
2386
|
+
const end = inputEl.value.length;
|
|
2387
|
+
inputEl.setSelectionRange(end, end);
|
|
2388
|
+
};
|
|
2389
|
+
|
|
1659
2390
|
// Starter template buttons
|
|
1660
2391
|
document.getElementById("starter-templates").addEventListener("click", (e) => {
|
|
1661
2392
|
const btn = e.target.closest(".starter-btn");
|
|
@@ -1664,7 +2395,11 @@ document.getElementById("starter-templates").addEventListener("click", (e) => {
|
|
|
1664
2395
|
|
|
1665
2396
|
// Templates icon in input area — toggle welcome section visibility
|
|
1666
2397
|
document.getElementById("btn-starter-templates")?.addEventListener("click", () => {
|
|
1667
|
-
|
|
2398
|
+
let welcome = document.getElementById("chat-welcome");
|
|
2399
|
+
if (!welcome) {
|
|
2400
|
+
restoreWelcome();
|
|
2401
|
+
welcome = document.getElementById("chat-welcome");
|
|
2402
|
+
}
|
|
1668
2403
|
if (welcome) welcome.classList.toggle("hidden");
|
|
1669
2404
|
});
|
|
1670
2405
|
|