vibespot 1.6.5 → 1.7.7
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 +13 -3
- package/assets/readme/02-checkpoints.png +0 -0
- package/dist/index.js +356 -292
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/ui/chat.js +651 -2
- package/ui/docs/index.html +48 -2
- package/ui/docs/screenshots/checkpoint-card.png +0 -0
- package/ui/index.html +4 -1
- package/ui/plan.js +6 -3
- package/ui/styles.css +247 -0
package/ui/chat.js
CHANGED
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
let ws = null;
|
|
10
10
|
let isStreaming = false;
|
|
11
|
+
// True while an agentic pipeline is actively building (Stage 3) — the window in
|
|
12
|
+
// which a new message barges in to cancel-and-replan (VIB-1880). False when
|
|
13
|
+
// parked at a checkpoint gate or running single-call mode.
|
|
14
|
+
let agenticRunning = false;
|
|
15
|
+
// Softened barge-in (VIB-1876): a message sent during an active build is QUEUED
|
|
16
|
+
// by default and dispatched when the build finishes. The queued chip carries an
|
|
17
|
+
// "Interrupt now" button that triggers the old cancel-and-replan barge-in.
|
|
18
|
+
let queuedMessage = null; // { text, opts }
|
|
19
|
+
let queuedChipEl = null;
|
|
11
20
|
let streamingMsgEl = null;
|
|
12
21
|
let streamBuffer = "";
|
|
13
22
|
let streamStartTime = 0;
|
|
@@ -36,6 +45,9 @@ let lastGenerationBubbleEl = null;
|
|
|
36
45
|
const messagesEl = document.getElementById("chat-messages");
|
|
37
46
|
const inputEl = document.getElementById("chat-input");
|
|
38
47
|
const sendBtn = document.getElementById("chat-send");
|
|
48
|
+
const oneShotBtn = document.getElementById("chat-send-oneshot");
|
|
49
|
+
// Remembered so the barge-in hint placeholder can be restored after a run.
|
|
50
|
+
const chatInputDefaultPlaceholder = (inputEl && inputEl.placeholder) || "";
|
|
39
51
|
const statusText = document.getElementById("status-text");
|
|
40
52
|
const statusEngine = document.getElementById("status-engine");
|
|
41
53
|
const statusTheme = document.getElementById("status-theme");
|
|
@@ -662,6 +674,13 @@ function handleWsMessage(msg) {
|
|
|
662
674
|
}
|
|
663
675
|
}
|
|
664
676
|
|
|
677
|
+
// Rehydrate a parked checkpoint after a refresh / device sleep (VIB-1876):
|
|
678
|
+
// the server kept the gate, so re-render the card and let the user resume
|
|
679
|
+
// from where they were. Plan-mode parks are rehydrated by planController.
|
|
680
|
+
if (msg.pendingCheckpoint && msg.pendingCheckpoint.preview && msg.pendingCheckpoint.kind !== "plan") {
|
|
681
|
+
handleCheckpointRequested({ preview: msg.pendingCheckpoint.preview });
|
|
682
|
+
}
|
|
683
|
+
|
|
665
684
|
// If setup handed us an initial prompt (describe-it path), send it now
|
|
666
685
|
// that the session is live. Skip if the project already has history
|
|
667
686
|
// (e.g. resumed session) to avoid double-submitting.
|
|
@@ -691,6 +710,7 @@ function handleWsMessage(msg) {
|
|
|
691
710
|
highlightOnNextModulesUpdated = true;
|
|
692
711
|
// Refresh the Library tab's module list so it stays in sync
|
|
693
712
|
if (typeof refreshDashboard === "function") refreshDashboard();
|
|
713
|
+
flushQueuedMessage();
|
|
694
714
|
break;
|
|
695
715
|
|
|
696
716
|
case "modules_updated":
|
|
@@ -743,14 +763,26 @@ function handleWsMessage(msg) {
|
|
|
743
763
|
handleAgentDecision(msg);
|
|
744
764
|
break;
|
|
745
765
|
case "module_progress":
|
|
766
|
+
enableBargeIn();
|
|
746
767
|
handleModuleProgress(msg);
|
|
747
768
|
break;
|
|
769
|
+
case "generation_superseded":
|
|
770
|
+
// The server confirmed our barge-in cancelled the prior run; the fresh
|
|
771
|
+
// run's events follow. Nothing to do — supersedeCurrentRun already reset.
|
|
772
|
+
setStatus("Redirecting…");
|
|
773
|
+
break;
|
|
748
774
|
case "pipeline_complete":
|
|
749
775
|
handlePipelineComplete(msg);
|
|
750
776
|
break;
|
|
751
777
|
case "pipeline_partial":
|
|
752
778
|
handlePipelinePartial(msg);
|
|
753
779
|
break;
|
|
780
|
+
case "checkpoint_requested":
|
|
781
|
+
handleCheckpointRequested(msg);
|
|
782
|
+
break;
|
|
783
|
+
case "checkpoint_cancelled":
|
|
784
|
+
handleCheckpointCancelled();
|
|
785
|
+
break;
|
|
754
786
|
case "generation_cost":
|
|
755
787
|
handleGenerationCost(msg);
|
|
756
788
|
break;
|
|
@@ -972,6 +1004,8 @@ function setPipelineDetail(text) {
|
|
|
972
1004
|
}
|
|
973
1005
|
|
|
974
1006
|
function handleAgentStep(msg) {
|
|
1007
|
+
// The expensive build stage is the barge-in window (VIB-1880).
|
|
1008
|
+
if (msg.step === "developing") enableBargeIn();
|
|
975
1009
|
ensurePipelineBubble();
|
|
976
1010
|
|
|
977
1011
|
const incoming = msg.step;
|
|
@@ -1285,6 +1319,7 @@ function handlePipelineComplete(msg) {
|
|
|
1285
1319
|
lastGenerationBubbleEl = bubble;
|
|
1286
1320
|
|
|
1287
1321
|
resetPipelineState();
|
|
1322
|
+
flushQueuedMessage();
|
|
1288
1323
|
}
|
|
1289
1324
|
|
|
1290
1325
|
function handlePipelinePartial(msg) {
|
|
@@ -1315,6 +1350,7 @@ function handlePipelinePartial(msg) {
|
|
|
1315
1350
|
lastGenerationBubbleEl = bubble;
|
|
1316
1351
|
|
|
1317
1352
|
resetPipelineState();
|
|
1353
|
+
flushQueuedMessage();
|
|
1318
1354
|
}
|
|
1319
1355
|
|
|
1320
1356
|
function resetPipelineState() {
|
|
@@ -1745,9 +1781,21 @@ fileInputEl.addEventListener("change", () => {
|
|
|
1745
1781
|
// Sending messages
|
|
1746
1782
|
// ---------------------------------------------------------------------------
|
|
1747
1783
|
|
|
1748
|
-
async function sendMessage(text) {
|
|
1784
|
+
async function sendMessage(text, opts = {}) {
|
|
1749
1785
|
const hasFiles = pendingFiles.length > 0;
|
|
1750
|
-
if (
|
|
1786
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
1787
|
+
if (!text.trim() && !hasFiles) return;
|
|
1788
|
+
// Block sends while busy — EXCEPT during an active agentic build. There, a new
|
|
1789
|
+
// message is QUEUED by default and runs when the build finishes (VIB-1876);
|
|
1790
|
+
// an explicit Interrupt (opts.interrupt) triggers the cancel-and-replan
|
|
1791
|
+
// barge-in (VIB-1880). Single-call streaming and checkpoint gates stay locked.
|
|
1792
|
+
if (isStreaming && !agenticRunning) return;
|
|
1793
|
+
const buildActive = isStreaming && agenticRunning;
|
|
1794
|
+
if (buildActive && !opts.interrupt) {
|
|
1795
|
+
queueMessage(text, opts);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (buildActive && opts.interrupt) supersedeCurrentRun();
|
|
1751
1799
|
|
|
1752
1800
|
// Remove welcome screen
|
|
1753
1801
|
const welcome = messagesEl.querySelector(".chat__welcome");
|
|
@@ -1794,6 +1842,8 @@ async function sendMessage(text) {
|
|
|
1794
1842
|
if (uploadedFiles.length > 0) {
|
|
1795
1843
|
payload.fileIds = uploadedFiles.map((f) => f.id);
|
|
1796
1844
|
}
|
|
1845
|
+
// One-shot: skip the design checkpoint and build the whole page (VIB-1877).
|
|
1846
|
+
if (opts.oneShot) payload.oneShot = true;
|
|
1797
1847
|
ws.send(JSON.stringify(payload));
|
|
1798
1848
|
|
|
1799
1849
|
// Clear input
|
|
@@ -1836,6 +1886,7 @@ function startStreaming() {
|
|
|
1836
1886
|
streamBuffer = "";
|
|
1837
1887
|
lastStreamStatus = "";
|
|
1838
1888
|
sendBtn.disabled = true;
|
|
1889
|
+
if (oneShotBtn) oneShotBtn.disabled = true;
|
|
1839
1890
|
streamStartTime = Date.now();
|
|
1840
1891
|
if (typeof window.setSelectModeDisabled === "function") {
|
|
1841
1892
|
window.setSelectModeDisabled(true);
|
|
@@ -1984,10 +2035,107 @@ function clearStreamStatus() {
|
|
|
1984
2035
|
if (statusEl) statusEl.remove();
|
|
1985
2036
|
}
|
|
1986
2037
|
|
|
2038
|
+
// Open the barge-in window: the build is running, so re-enable the composer and
|
|
2039
|
+
// let the user redirect the run mid-flight (VIB-1880).
|
|
2040
|
+
function enableBargeIn() {
|
|
2041
|
+
if (agenticRunning) return;
|
|
2042
|
+
agenticRunning = true;
|
|
2043
|
+
sendBtn.disabled = false;
|
|
2044
|
+
if (oneShotBtn) oneShotBtn.disabled = false;
|
|
2045
|
+
if (inputEl) inputEl.placeholder = "Building… your message will queue (or Interrupt to redirect now)";
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// --- Softened barge-in: queue-by-default with an Interrupt button (VIB-1876) ---
|
|
2049
|
+
|
|
2050
|
+
// Queue a message sent mid-build instead of cancelling the run. Latest wins.
|
|
2051
|
+
function queueMessage(text, opts) {
|
|
2052
|
+
if (!text.trim() && pendingFiles.length === 0) return;
|
|
2053
|
+
queuedMessage = { text, opts: { ...opts } };
|
|
2054
|
+
if (inputEl) {
|
|
2055
|
+
inputEl.value = "";
|
|
2056
|
+
inputEl.dispatchEvent(new Event("input"));
|
|
2057
|
+
}
|
|
2058
|
+
renderQueuedChip();
|
|
2059
|
+
setStatus("Message queued");
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
function clearQueuedMessage() {
|
|
2063
|
+
queuedMessage = null;
|
|
2064
|
+
if (queuedChipEl) {
|
|
2065
|
+
queuedChipEl.remove();
|
|
2066
|
+
queuedChipEl = null;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// Trigger the real barge-in (cancel-and-replan) with the queued message.
|
|
2071
|
+
function interruptWithQueued() {
|
|
2072
|
+
if (!queuedMessage) return;
|
|
2073
|
+
const { text, opts } = queuedMessage;
|
|
2074
|
+
clearQueuedMessage();
|
|
2075
|
+
sendMessage(text, { ...opts, interrupt: true });
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// Dispatch the queued message once the current build has finished.
|
|
2079
|
+
function flushQueuedMessage() {
|
|
2080
|
+
if (!queuedMessage) return;
|
|
2081
|
+
const { text, opts } = queuedMessage;
|
|
2082
|
+
clearQueuedMessage();
|
|
2083
|
+
// Defer so the just-finished run's UI settles before the next one starts.
|
|
2084
|
+
setTimeout(() => sendMessage(text, opts), 0);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
function renderQueuedChip() {
|
|
2088
|
+
if (!queuedMessage) return clearQueuedMessage();
|
|
2089
|
+
const area = inputEl && inputEl.closest(".chat__input-area");
|
|
2090
|
+
if (!area) return;
|
|
2091
|
+
if (!queuedChipEl) {
|
|
2092
|
+
queuedChipEl = document.createElement("div");
|
|
2093
|
+
queuedChipEl.className = "queued-msg";
|
|
2094
|
+
queuedChipEl.style.cssText =
|
|
2095
|
+
"display:flex;align-items:center;gap:8px;margin:0 0 8px;padding:8px 10px;border:1px solid var(--border,#e2e2e2);border-radius:8px;background:var(--surface-2,#faf7f5);font-size:13px;";
|
|
2096
|
+
area.insertBefore(queuedChipEl, area.firstChild);
|
|
2097
|
+
}
|
|
2098
|
+
const t = queuedMessage.text || "(files attached)";
|
|
2099
|
+
const preview = t.length > 90 ? t.slice(0, 90) + "…" : t;
|
|
2100
|
+
queuedChipEl.innerHTML =
|
|
2101
|
+
'<span aria-hidden="true">⏳</span>' +
|
|
2102
|
+
'<span style="opacity:.7;white-space:nowrap;">Queued — runs next:</span>' +
|
|
2103
|
+
'<span class="queued-msg__text" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>' +
|
|
2104
|
+
'<button type="button" class="queued-msg__interrupt" style="flex:none;cursor:pointer;border:1px solid var(--border,#e2e2e2);background:var(--surface,#fff);border-radius:6px;padding:3px 8px;font-size:12px;">Interrupt now</button>' +
|
|
2105
|
+
'<button type="button" class="queued-msg__cancel" aria-label="Cancel queued message" style="flex:none;cursor:pointer;border:1px solid var(--border,#e2e2e2);background:var(--surface,#fff);border-radius:6px;padding:3px 8px;font-size:12px;">✕ Cancel</button>';
|
|
2106
|
+
queuedChipEl.querySelector(".queued-msg__text").textContent = preview;
|
|
2107
|
+
queuedChipEl.querySelector(".queued-msg__interrupt").onclick = interruptWithQueued;
|
|
2108
|
+
queuedChipEl.querySelector(".queued-msg__cancel").onclick = clearQueuedMessage;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Tear down the superseded run's transient UI so the replacement run renders
|
|
2112
|
+
// fresh (called when a barge-in send cancels the active build).
|
|
2113
|
+
function supersedeCurrentRun() {
|
|
2114
|
+
stopStreamTimer();
|
|
2115
|
+
clearStreamStatus();
|
|
2116
|
+
const streamingEl = messagesEl.querySelector(".chat-msg--streaming");
|
|
2117
|
+
if (streamingEl) {
|
|
2118
|
+
streamingEl.classList.remove("chat-msg--streaming");
|
|
2119
|
+
const bubble = streamingEl.querySelector(".chat-msg__bubble");
|
|
2120
|
+
if (bubble && !bubble.textContent.trim()) {
|
|
2121
|
+
bubble.innerHTML = '<em class="message__placeholder">Superseded by your next message.</em>';
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
removeCheckpointCard();
|
|
2125
|
+
isStreaming = false;
|
|
2126
|
+
agenticRunning = false;
|
|
2127
|
+
streamingMsgEl = null;
|
|
2128
|
+
streamBuffer = "";
|
|
2129
|
+
resetPipelineState();
|
|
2130
|
+
}
|
|
2131
|
+
|
|
1987
2132
|
function finishStreaming() {
|
|
1988
2133
|
if (!isStreaming) return;
|
|
1989
2134
|
isStreaming = false;
|
|
2135
|
+
agenticRunning = false;
|
|
2136
|
+
if (inputEl) inputEl.placeholder = chatInputDefaultPlaceholder;
|
|
1990
2137
|
sendBtn.disabled = false;
|
|
2138
|
+
if (oneShotBtn) oneShotBtn.disabled = false;
|
|
1991
2139
|
if (typeof window.setSelectModeDisabled === "function") {
|
|
1992
2140
|
window.setSelectModeDisabled(false);
|
|
1993
2141
|
}
|
|
@@ -2008,6 +2156,24 @@ function finishStreaming() {
|
|
|
2008
2156
|
if (streamingEl) {
|
|
2009
2157
|
streamingEl.classList.remove("chat-msg--streaming");
|
|
2010
2158
|
|
|
2159
|
+
// Agentic seams (a checkpoint gate, or a resume that re-parks / just builds
|
|
2160
|
+
// modules) leave a streaming bubble that never received any text or progress
|
|
2161
|
+
// stepper. Don't finalize it into an empty bubble with a lone duration —
|
|
2162
|
+
// drop it (VIB-1876 follow-up). A bubble carrying the pipeline stepper or
|
|
2163
|
+
// any rendered content has child nodes, so it's kept.
|
|
2164
|
+
const bubbleEl = streamingEl.querySelector(".chat-msg__bubble");
|
|
2165
|
+
const hasContent =
|
|
2166
|
+
!!bubbleEl && (bubbleEl.children.length > 0 || bubbleEl.textContent.trim().length > 0);
|
|
2167
|
+
const willRenderText = !!streamBuffer && streamBuffer.trim().length > 0;
|
|
2168
|
+
if (!hasContent && !willRenderText) {
|
|
2169
|
+
streamingEl.remove();
|
|
2170
|
+
streamingMsgEl = null;
|
|
2171
|
+
streamBuffer = "";
|
|
2172
|
+
setStatus("Ready");
|
|
2173
|
+
scrollToBottom();
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2011
2177
|
// Add duration metadata beneath the bubble (inside .chat-msg__content)
|
|
2012
2178
|
const meta = document.createElement("div");
|
|
2013
2179
|
meta.className = "chat-msg__meta";
|
|
@@ -2246,6 +2412,482 @@ function appendSystemMessage(text) {
|
|
|
2246
2412
|
scrollToBottom();
|
|
2247
2413
|
}
|
|
2248
2414
|
|
|
2415
|
+
// ---------------------------------------------------------------------------
|
|
2416
|
+
// Checkpoint gate card (VIB-1877) — the design preview the user approves /
|
|
2417
|
+
// steers / skips / cancels before the expensive module build.
|
|
2418
|
+
// ---------------------------------------------------------------------------
|
|
2419
|
+
|
|
2420
|
+
let checkpointCardEl = null;
|
|
2421
|
+
let currentCheckpointKind = null;
|
|
2422
|
+
let currentCheckpointData = null;
|
|
2423
|
+
|
|
2424
|
+
function setSendDisabled(disabled) {
|
|
2425
|
+
sendBtn.disabled = disabled;
|
|
2426
|
+
if (oneShotBtn) oneShotBtn.disabled = disabled;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
function removeCheckpointCard() {
|
|
2430
|
+
if (checkpointCardEl && checkpointCardEl.parentNode) {
|
|
2431
|
+
checkpointCardEl.parentNode.removeChild(checkpointCardEl);
|
|
2432
|
+
}
|
|
2433
|
+
checkpointCardEl = null;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// One-line record of what the user decided at a checkpoint (VIB-1876). Boris:
|
|
2437
|
+
// keep the decision in the transcript rather than dropping the turn entirely.
|
|
2438
|
+
function summarizeDecision(kind, action, note, extra) {
|
|
2439
|
+
const label =
|
|
2440
|
+
kind === "brand_intake" ? "Brand" : kind === "structure" ? "Structure" : kind === "plan" ? "Plan" : "Design";
|
|
2441
|
+
if (action === "cancel") return `${label}: cancelled — nothing was built.`;
|
|
2442
|
+
if (action === "steer") return `${label}: steered${note ? " — " + note : ""}.`;
|
|
2443
|
+
if (kind === "brand_intake") {
|
|
2444
|
+
if (action !== "approve") return "Brand: surprise me — generating a design system.";
|
|
2445
|
+
const ch =
|
|
2446
|
+
extra && extra.brandIntake
|
|
2447
|
+
? Object.keys(extra.brandIntake).filter((k) => extra.brandIntake[k])
|
|
2448
|
+
: [];
|
|
2449
|
+
return `Brand: bringing your brand${ch.length ? " (" + ch.join(", ") + ")" : ""}.`;
|
|
2450
|
+
}
|
|
2451
|
+
if (action === "skip") return `${label}: skipped remaining checkpoints — building now.`;
|
|
2452
|
+
return `${label}: approved.`;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// The substantive content/facts behind a decision (VIB-1876): the palette/type
|
|
2456
|
+
// approved, the module outline kept, or the brand inputs provided — so the
|
|
2457
|
+
// transcript records *what*, not just the verb. Returns "" when there's nothing.
|
|
2458
|
+
function decisionFacts(kind, action, note, extra, data) {
|
|
2459
|
+
if (action === "cancel") return "";
|
|
2460
|
+
if (action === "steer") return note ? "Note: " + note : "";
|
|
2461
|
+
const parts = [];
|
|
2462
|
+
const clip = (s) => String(s).replace(/\s+/g, " ").trim().slice(0, 80);
|
|
2463
|
+
if (kind === "design" || kind === "plan") {
|
|
2464
|
+
const pal = Array.isArray(data && data.palette)
|
|
2465
|
+
? data.palette.map((p) => p && p.value).filter(Boolean).slice(0, 5)
|
|
2466
|
+
: [];
|
|
2467
|
+
if (pal.length) parts.push("Palette: " + pal.join(", "));
|
|
2468
|
+
const typo = (data && data.typography) || {};
|
|
2469
|
+
if (typo.heading || typo.body) parts.push("Type: " + [typo.heading, typo.body].filter(Boolean).join(" / "));
|
|
2470
|
+
} else if (kind === "structure") {
|
|
2471
|
+
const rows =
|
|
2472
|
+
extra && Array.isArray(extra.outline) && extra.outline.length
|
|
2473
|
+
? extra.outline
|
|
2474
|
+
: data && Array.isArray(data.modules)
|
|
2475
|
+
? data.modules
|
|
2476
|
+
: [];
|
|
2477
|
+
const names = rows.map((r) => r && (r.name || r.label)).filter(Boolean);
|
|
2478
|
+
if (names.length) parts.push(`Sections (${names.length}): ` + names.join(", "));
|
|
2479
|
+
} else if (kind === "brand_intake") {
|
|
2480
|
+
const bi = (extra && extra.brandIntake) || {};
|
|
2481
|
+
if (bi.colors) parts.push("Colors: " + clip(bi.colors));
|
|
2482
|
+
if (bi.siteUrl) parts.push("Site: " + clip(bi.siteUrl));
|
|
2483
|
+
if (bi.themePath) parts.push("Theme: " + clip(bi.themePath));
|
|
2484
|
+
if (bi.code) parts.push("Pasted code");
|
|
2485
|
+
if (bi.voice) parts.push("Voice: " + clip(bi.voice));
|
|
2486
|
+
}
|
|
2487
|
+
return parts.join(" · ");
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// Convert the live checkpoint card into a static decision record so the turn
|
|
2491
|
+
// keeps meaning in the transcript instead of leaving an empty bubble (VIB-1876).
|
|
2492
|
+
// Rebuilds a standard assistant bubble (the card renderers use bespoke markup).
|
|
2493
|
+
function finalizeCheckpointCard(action, note, extra) {
|
|
2494
|
+
if (!checkpointCardEl) return;
|
|
2495
|
+
const kind = currentCheckpointKind || "design";
|
|
2496
|
+
const text = summarizeDecision(kind, action, note, extra);
|
|
2497
|
+
const facts = decisionFacts(kind, action, note, extra, currentCheckpointData);
|
|
2498
|
+
const glyph = action === "cancel" ? "✕" : "✓";
|
|
2499
|
+
const time = formatMessageTime(Date.now());
|
|
2500
|
+
checkpointCardEl.className = "chat-msg chat-msg--assistant chat-msg--checkpoint-resolved";
|
|
2501
|
+
checkpointCardEl.innerHTML =
|
|
2502
|
+
'<div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>' +
|
|
2503
|
+
'<div class="chat-msg__content">' +
|
|
2504
|
+
'<div class="chat-msg__header"><span class="chat-msg__sender">vibeSpot AI</span>' +
|
|
2505
|
+
'<span class="chat-msg__time">' + time + "</span></div>" +
|
|
2506
|
+
'<div class="chat-msg__bubble">' +
|
|
2507
|
+
'<div class="checkpoint-decision" style="opacity:.85;"></div>' +
|
|
2508
|
+
'<div class="checkpoint-decision__facts" style="opacity:.6;font-size:12px;margin-top:2px;"></div>' +
|
|
2509
|
+
"</div></div>";
|
|
2510
|
+
checkpointCardEl.querySelector(".checkpoint-decision").textContent = glyph + " " + text;
|
|
2511
|
+
const factsEl = checkpointCardEl.querySelector(".checkpoint-decision__facts");
|
|
2512
|
+
if (facts) factsEl.textContent = facts;
|
|
2513
|
+
else factsEl.remove();
|
|
2514
|
+
checkpointCardEl = null;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// When a stepper card is left behind (the run parks at a gate), mark the stages
|
|
2518
|
+
// it actually reached as done so the older card reads "complete up to here"
|
|
2519
|
+
// rather than frozen mid-stage (VIB-1876).
|
|
2520
|
+
function finalizeStepperProgress() {
|
|
2521
|
+
if (!pipelineStepperEl) return;
|
|
2522
|
+
pipelineStepperEl.querySelectorAll(".pipeline-stage--active").forEach((el) => {
|
|
2523
|
+
const step = el.getAttribute("data-stage");
|
|
2524
|
+
if (step) setStageStatus(step, "done");
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function handleCheckpointRequested(msg) {
|
|
2529
|
+
// Settle the in-flight progress bubble; the gate now waits on the user.
|
|
2530
|
+
clearStreamStatus();
|
|
2531
|
+
finalizeStepperProgress();
|
|
2532
|
+
finishStreaming();
|
|
2533
|
+
removeCheckpointCard();
|
|
2534
|
+
|
|
2535
|
+
const preview = msg.preview || {};
|
|
2536
|
+
const data = preview.data || {};
|
|
2537
|
+
currentCheckpointKind = preview.kind || "design";
|
|
2538
|
+
currentCheckpointData = data;
|
|
2539
|
+
checkpointCardEl = renderCheckpointCard(preview, data, msg.estCostNext);
|
|
2540
|
+
messagesEl.appendChild(checkpointCardEl);
|
|
2541
|
+
scrollToBottom();
|
|
2542
|
+
|
|
2543
|
+
// Hold the send buttons until the gate is resolved.
|
|
2544
|
+
setSendDisabled(true);
|
|
2545
|
+
setStatus("Awaiting your call");
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
function handleCheckpointCancelled() {
|
|
2549
|
+
removeCheckpointCard();
|
|
2550
|
+
setSendDisabled(false);
|
|
2551
|
+
appendSystemMessage("Cancelled — nothing was built.");
|
|
2552
|
+
flushQueuedMessage();
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function resolveCheckpoint(action, note, extra) {
|
|
2556
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
2557
|
+
// Leave the decision in the transcript (card → one-line record), not an empty
|
|
2558
|
+
// bubble (VIB-1876).
|
|
2559
|
+
finalizeCheckpointCard(action, note, extra);
|
|
2560
|
+
if (action === "cancel") {
|
|
2561
|
+
setSendDisabled(false);
|
|
2562
|
+
} else {
|
|
2563
|
+
// Work resumes. Don't pre-create an empty bubble — clear the prior turn's
|
|
2564
|
+
// pipeline refs so the resume's first progress event builds its OWN stepper
|
|
2565
|
+
// bubble (ensurePipelineBubble returns early while pipelineBubbleEl is set).
|
|
2566
|
+
isStreaming = true;
|
|
2567
|
+
setSendDisabled(true);
|
|
2568
|
+
pipelineBubbleEl = null;
|
|
2569
|
+
pipelineStepperEl = null;
|
|
2570
|
+
pipelineDetailEl = null;
|
|
2571
|
+
pipelineModulesEl = null;
|
|
2572
|
+
pipelineEstimateEl = null;
|
|
2573
|
+
streamingMsgEl = null;
|
|
2574
|
+
streamStartTime = Date.now();
|
|
2575
|
+
setStatus("Generating...");
|
|
2576
|
+
}
|
|
2577
|
+
ws.send(JSON.stringify({
|
|
2578
|
+
type: "checkpoint_resolve",
|
|
2579
|
+
action,
|
|
2580
|
+
note: note || undefined,
|
|
2581
|
+
// Structure checkpoint (VIB-1879) outline; brand-intake channels (VIB-1878).
|
|
2582
|
+
outline: extra && extra.outline ? extra.outline : undefined,
|
|
2583
|
+
brandIntake: extra && extra.brandIntake ? extra.brandIntake : undefined,
|
|
2584
|
+
}));
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
function renderCheckpointCard(preview, data, estCostNext) {
|
|
2588
|
+
// Brand-intake gate (VIB-1878) — the front-of-flow ask-back; different card.
|
|
2589
|
+
if (preview && preview.kind === "brand_intake") {
|
|
2590
|
+
return renderBrandIntakeCard(preview, data);
|
|
2591
|
+
}
|
|
2592
|
+
// Structure checkpoint (VIB-1879) renders an editable module outline instead
|
|
2593
|
+
// of the design palette/hero.
|
|
2594
|
+
if (preview && preview.kind === "structure") {
|
|
2595
|
+
return renderStructureCheckpointCard(preview, data, estCostNext);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
const div = document.createElement("div");
|
|
2599
|
+
div.className = "chat-msg chat-msg--assistant";
|
|
2600
|
+
|
|
2601
|
+
const palette = Array.isArray(data.palette) ? data.palette : [];
|
|
2602
|
+
const swatches = palette.map((p) =>
|
|
2603
|
+
`<div class="checkpoint__swatch" title="${escapeHtml(p.label)}: ${escapeHtml(p.value)}">
|
|
2604
|
+
<span class="checkpoint__swatch-chip" style="background:${escapeHtml(p.value)}"></span>
|
|
2605
|
+
<span class="checkpoint__swatch-label">${escapeHtml(p.label)}</span>
|
|
2606
|
+
</div>`).join("");
|
|
2607
|
+
|
|
2608
|
+
const typo = data.typography || {};
|
|
2609
|
+
const heroSrc = typeof data.heroHtml === "string" ? data.heroHtml : "";
|
|
2610
|
+
const costLine = (typeof estCostNext === "number" && estCostNext > 0)
|
|
2611
|
+
? `<span class="checkpoint__cost">Building all modules will cost ~$${estCostNext.toFixed(2)}</span>`
|
|
2612
|
+
: "";
|
|
2613
|
+
|
|
2614
|
+
div.innerHTML = `
|
|
2615
|
+
<div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
|
|
2616
|
+
<div class="chat-msg__content">
|
|
2617
|
+
<div class="checkpoint">
|
|
2618
|
+
<div class="checkpoint__head">
|
|
2619
|
+
<span class="checkpoint__badge">Design checkpoint</span>
|
|
2620
|
+
<span class="checkpoint__headline">${escapeHtml(preview.headline || "Review the design before building")}</span>
|
|
2621
|
+
</div>
|
|
2622
|
+
<div class="checkpoint__hero">
|
|
2623
|
+
<iframe class="checkpoint__hero-frame" title="Hero preview" sandbox="" srcdoc="${escapeHtml(heroSrc)}"></iframe>
|
|
2624
|
+
</div>
|
|
2625
|
+
<div class="checkpoint__tokens">
|
|
2626
|
+
<div class="checkpoint__palette">${swatches || '<span class="checkpoint__muted">No palette tokens</span>'}</div>
|
|
2627
|
+
<div class="checkpoint__type">
|
|
2628
|
+
<div class="checkpoint__type-row" style="font-family:${escapeHtml(typo.heading || "")}">Aa — ${escapeHtml(typo.heading || "Heading font")}</div>
|
|
2629
|
+
<div class="checkpoint__type-row checkpoint__type-row--body" style="font-family:${escapeHtml(typo.body || "")}">Body — ${escapeHtml(typo.body || "Body font")}</div>
|
|
2630
|
+
</div>
|
|
2631
|
+
</div>
|
|
2632
|
+
${costLine ? `<div class="checkpoint__meta">${costLine}</div>` : ""}
|
|
2633
|
+
<div class="checkpoint__steer hidden">
|
|
2634
|
+
<textarea class="checkpoint__steer-input" rows="2" placeholder="What should change? e.g. warmer palette, bigger headings..."></textarea>
|
|
2635
|
+
</div>
|
|
2636
|
+
<div class="checkpoint__actions">
|
|
2637
|
+
<button class="btn btn--primary checkpoint__btn" data-action="approve">Looks good — build it</button>
|
|
2638
|
+
<button class="btn checkpoint__btn" data-action="steer">Tweak the design</button>
|
|
2639
|
+
<button class="btn checkpoint__btn" data-action="skip" title="Build now and don't ask again this run">Skip checkpoints</button>
|
|
2640
|
+
<button class="btn btn--ghost checkpoint__btn" data-action="cancel">Cancel</button>
|
|
2641
|
+
</div>
|
|
2642
|
+
</div>
|
|
2643
|
+
</div>`;
|
|
2644
|
+
|
|
2645
|
+
const steerWrap = div.querySelector(".checkpoint__steer");
|
|
2646
|
+
const steerInput = div.querySelector(".checkpoint__steer-input");
|
|
2647
|
+
const steerBtn = div.querySelector('.checkpoint__btn[data-action="steer"]');
|
|
2648
|
+
let steerArmed = false;
|
|
2649
|
+
|
|
2650
|
+
div.querySelectorAll(".checkpoint__btn").forEach((btn) => {
|
|
2651
|
+
btn.addEventListener("click", () => {
|
|
2652
|
+
const action = btn.dataset.action;
|
|
2653
|
+
// Steer is two-step: first click reveals the note box, second submits.
|
|
2654
|
+
if (action === "steer" && !steerArmed) {
|
|
2655
|
+
steerArmed = true;
|
|
2656
|
+
steerWrap.classList.remove("hidden");
|
|
2657
|
+
steerBtn.textContent = "Apply tweak";
|
|
2658
|
+
if (steerInput) steerInput.focus();
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
const note = action === "steer" && steerInput ? steerInput.value.trim() : undefined;
|
|
2662
|
+
div.querySelectorAll(".checkpoint__btn").forEach((b) => (b.disabled = true));
|
|
2663
|
+
resolveCheckpoint(action, note);
|
|
2664
|
+
});
|
|
2665
|
+
});
|
|
2666
|
+
|
|
2667
|
+
return div;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// Structure checkpoint card (VIB-1879) — the planned module skeleton as an
|
|
2671
|
+
// editable outline (reorder / cut / add / rename) before the parallel build.
|
|
2672
|
+
function renderStructureCheckpointCard(preview, data, estCostNext) {
|
|
2673
|
+
const div = document.createElement("div");
|
|
2674
|
+
div.className = "chat-msg chat-msg--assistant";
|
|
2675
|
+
|
|
2676
|
+
const modules = Array.isArray(data.modules) ? data.modules : [];
|
|
2677
|
+
const narrative = typeof data.narrative === "string" ? data.narrative : "";
|
|
2678
|
+
const costLine = (typeof estCostNext === "number" && estCostNext > 0)
|
|
2679
|
+
? `<span class="checkpoint__cost">Building all modules will cost ~$${estCostNext.toFixed(2)}</span>`
|
|
2680
|
+
: "";
|
|
2681
|
+
|
|
2682
|
+
div.innerHTML = `
|
|
2683
|
+
<div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
|
|
2684
|
+
<div class="chat-msg__content">
|
|
2685
|
+
<div class="checkpoint checkpoint--structure">
|
|
2686
|
+
<div class="checkpoint__head">
|
|
2687
|
+
<span class="checkpoint__badge">Structure checkpoint</span>
|
|
2688
|
+
<span class="checkpoint__headline">${escapeHtml(preview.headline || "Review the structure before building")}</span>
|
|
2689
|
+
</div>
|
|
2690
|
+
${narrative ? `<div class="checkpoint__narrative">${escapeHtml(narrative)}</div>` : ""}
|
|
2691
|
+
<div class="checkpoint__outline"></div>
|
|
2692
|
+
<button type="button" class="btn btn--ghost checkpoint__add">+ Add section</button>
|
|
2693
|
+
${costLine ? `<div class="checkpoint__meta">${costLine}</div>` : ""}
|
|
2694
|
+
<div class="checkpoint__steer hidden">
|
|
2695
|
+
<textarea class="checkpoint__steer-input" rows="2" placeholder="What should change? e.g. add a pricing section, drop the FAQ..."></textarea>
|
|
2696
|
+
</div>
|
|
2697
|
+
<div class="checkpoint__actions">
|
|
2698
|
+
<button class="btn btn--primary checkpoint__btn" data-action="approve">Build these sections</button>
|
|
2699
|
+
<button class="btn checkpoint__btn" data-action="steer">Re-plan structure</button>
|
|
2700
|
+
<button class="btn checkpoint__btn" data-action="skip" title="Build now and don't ask again this run">Skip checkpoints</button>
|
|
2701
|
+
<button class="btn btn--ghost checkpoint__btn" data-action="cancel">Cancel</button>
|
|
2702
|
+
</div>
|
|
2703
|
+
</div>
|
|
2704
|
+
</div>`;
|
|
2705
|
+
|
|
2706
|
+
const listEl = div.querySelector(".checkpoint__outline");
|
|
2707
|
+
|
|
2708
|
+
function makeRow(name, description, sourceIndex) {
|
|
2709
|
+
const row = document.createElement("div");
|
|
2710
|
+
row.className = "checkpoint__row";
|
|
2711
|
+
if (sourceIndex != null) row.dataset.sourceIndex = String(sourceIndex);
|
|
2712
|
+
if (description) row.dataset.desc = description;
|
|
2713
|
+
row.innerHTML = `
|
|
2714
|
+
<div class="checkpoint__row-move">
|
|
2715
|
+
<button type="button" class="checkpoint__row-btn" data-move="up" title="Move up" aria-label="Move up">▲</button>
|
|
2716
|
+
<button type="button" class="checkpoint__row-btn" data-move="down" title="Move down" aria-label="Move down">▼</button>
|
|
2717
|
+
</div>
|
|
2718
|
+
<div class="checkpoint__row-main">
|
|
2719
|
+
<input class="checkpoint__row-name" value="${escapeHtml(name || "")}" placeholder="section-name" spellcheck="false" />
|
|
2720
|
+
<span class="checkpoint__row-desc">${escapeHtml(description || "")}</span>
|
|
2721
|
+
</div>
|
|
2722
|
+
<button type="button" class="checkpoint__row-remove" data-remove title="Remove section" aria-label="Remove section">✕</button>`;
|
|
2723
|
+
|
|
2724
|
+
row.querySelector('[data-move="up"]').addEventListener("click", () => {
|
|
2725
|
+
const prev = row.previousElementSibling;
|
|
2726
|
+
if (prev) listEl.insertBefore(row, prev);
|
|
2727
|
+
});
|
|
2728
|
+
row.querySelector('[data-move="down"]').addEventListener("click", () => {
|
|
2729
|
+
const next = row.nextElementSibling;
|
|
2730
|
+
if (next) listEl.insertBefore(next, row);
|
|
2731
|
+
});
|
|
2732
|
+
row.querySelector("[data-remove]").addEventListener("click", () => {
|
|
2733
|
+
row.remove();
|
|
2734
|
+
});
|
|
2735
|
+
return row;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
modules.forEach((m) =>
|
|
2739
|
+
listEl.appendChild(makeRow(m.name, m.description, m.sourceIndex)),
|
|
2740
|
+
);
|
|
2741
|
+
|
|
2742
|
+
div.querySelector(".checkpoint__add").addEventListener("click", () => {
|
|
2743
|
+
const row = makeRow("", "", null);
|
|
2744
|
+
listEl.appendChild(row);
|
|
2745
|
+
const input = row.querySelector(".checkpoint__row-name");
|
|
2746
|
+
if (input) input.focus();
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
function collectOutline() {
|
|
2750
|
+
return [...listEl.querySelectorAll(".checkpoint__row")]
|
|
2751
|
+
.map((row) => {
|
|
2752
|
+
const name = (row.querySelector(".checkpoint__row-name").value || "").trim();
|
|
2753
|
+
const si = row.dataset.sourceIndex;
|
|
2754
|
+
return {
|
|
2755
|
+
name,
|
|
2756
|
+
description: row.dataset.desc || undefined,
|
|
2757
|
+
sourceIndex: si !== undefined && si !== "" ? Number(si) : undefined,
|
|
2758
|
+
};
|
|
2759
|
+
})
|
|
2760
|
+
.filter((it) => it.name);
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const steerWrap = div.querySelector(".checkpoint__steer");
|
|
2764
|
+
const steerInput = div.querySelector(".checkpoint__steer-input");
|
|
2765
|
+
const steerBtn = div.querySelector('.checkpoint__btn[data-action="steer"]');
|
|
2766
|
+
let steerArmed = false;
|
|
2767
|
+
|
|
2768
|
+
div.querySelectorAll(".checkpoint__btn").forEach((btn) => {
|
|
2769
|
+
btn.addEventListener("click", () => {
|
|
2770
|
+
const action = btn.dataset.action;
|
|
2771
|
+
// Steer is two-step: first click reveals the note box, second submits.
|
|
2772
|
+
if (action === "steer" && !steerArmed) {
|
|
2773
|
+
steerArmed = true;
|
|
2774
|
+
steerWrap.classList.remove("hidden");
|
|
2775
|
+
steerBtn.textContent = "Apply re-plan";
|
|
2776
|
+
if (steerInput) steerInput.focus();
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
const note = action === "steer" && steerInput ? steerInput.value.trim() : undefined;
|
|
2780
|
+
// approve/skip carry the edited outline; steer/cancel don't.
|
|
2781
|
+
const outline = action === "approve" || action === "skip" ? collectOutline() : undefined;
|
|
2782
|
+
div.querySelectorAll(".checkpoint__btn").forEach((b) => (b.disabled = true));
|
|
2783
|
+
resolveCheckpoint(action, note, { outline });
|
|
2784
|
+
});
|
|
2785
|
+
});
|
|
2786
|
+
|
|
2787
|
+
return div;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// ---------------------------------------------------------------------------
|
|
2791
|
+
// Brand-intake gate card (VIB-1878) — the front-of-flow ask-back shown when a
|
|
2792
|
+
// new page has no style system yet: "Surprise me" (AI invents) vs "Bring your
|
|
2793
|
+
// brand" (paste colors/code/voice, or point at a site/theme → seed the design).
|
|
2794
|
+
// ---------------------------------------------------------------------------
|
|
2795
|
+
|
|
2796
|
+
function renderBrandIntakeCard(preview, data) {
|
|
2797
|
+
const div = document.createElement("div");
|
|
2798
|
+
div.className = "chat-msg chat-msg--assistant";
|
|
2799
|
+
|
|
2800
|
+
const channels = Array.isArray(data.channels) ? data.channels : [];
|
|
2801
|
+
const fields = channels.map((c) => {
|
|
2802
|
+
const id = `brand-${escapeHtml(c.key)}`;
|
|
2803
|
+
const input = c.multiline
|
|
2804
|
+
? `<textarea class="brand-intake__input" data-key="${escapeHtml(c.key)}" rows="3" placeholder="${escapeHtml(c.placeholder || "")}"></textarea>`
|
|
2805
|
+
: `<input class="brand-intake__input" data-key="${escapeHtml(c.key)}" type="text" placeholder="${escapeHtml(c.placeholder || "")}" />`;
|
|
2806
|
+
return `<label class="brand-intake__field" for="${id}">
|
|
2807
|
+
<span class="brand-intake__label">${escapeHtml(c.label || c.key)}</span>
|
|
2808
|
+
${input}
|
|
2809
|
+
</label>`;
|
|
2810
|
+
}).join("");
|
|
2811
|
+
|
|
2812
|
+
div.innerHTML = `
|
|
2813
|
+
<div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
|
|
2814
|
+
<div class="chat-msg__content">
|
|
2815
|
+
<div class="checkpoint brand-intake">
|
|
2816
|
+
<div class="checkpoint__head">
|
|
2817
|
+
<span class="checkpoint__badge">Brand checkpoint</span>
|
|
2818
|
+
<span class="checkpoint__headline">${escapeHtml(preview.headline || "Match your brand, or surprise you?")}</span>
|
|
2819
|
+
</div>
|
|
2820
|
+
<div class="brand-intake__choices">
|
|
2821
|
+
<button class="btn btn--primary brand-intake__choice" data-choice="surprise">
|
|
2822
|
+
<span class="brand-intake__choice-title">${escapeHtml(data.surpriseLabel || "Surprise me")}</span>
|
|
2823
|
+
<span class="brand-intake__choice-hint">${escapeHtml(data.surpriseHint || "")}</span>
|
|
2824
|
+
</button>
|
|
2825
|
+
<button class="btn brand-intake__choice" data-choice="bring">
|
|
2826
|
+
<span class="brand-intake__choice-title">${escapeHtml(data.bringLabel || "Bring your brand")}</span>
|
|
2827
|
+
<span class="brand-intake__choice-hint">${escapeHtml(data.bringHint || "")}</span>
|
|
2828
|
+
</button>
|
|
2829
|
+
</div>
|
|
2830
|
+
<div class="brand-intake__form hidden">
|
|
2831
|
+
${fields}
|
|
2832
|
+
<div class="brand-intake__form-actions">
|
|
2833
|
+
<button class="btn btn--primary brand-intake__submit">Use my brand</button>
|
|
2834
|
+
<button class="btn btn--ghost brand-intake__back">Back</button>
|
|
2835
|
+
</div>
|
|
2836
|
+
</div>
|
|
2837
|
+
<div class="checkpoint__actions brand-intake__top-actions">
|
|
2838
|
+
<button class="btn btn--ghost checkpoint__btn" data-action="cancel">Cancel</button>
|
|
2839
|
+
</div>
|
|
2840
|
+
</div>
|
|
2841
|
+
</div>`;
|
|
2842
|
+
|
|
2843
|
+
const form = div.querySelector(".brand-intake__form");
|
|
2844
|
+
const choices = div.querySelector(".brand-intake__choices");
|
|
2845
|
+
|
|
2846
|
+
div.querySelector('[data-choice="surprise"]').addEventListener("click", () => {
|
|
2847
|
+
div.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
2848
|
+
// "Surprise me" = skip the brand step; the AI invents the design system.
|
|
2849
|
+
resolveCheckpoint("skip");
|
|
2850
|
+
});
|
|
2851
|
+
|
|
2852
|
+
div.querySelector('[data-choice="bring"]').addEventListener("click", () => {
|
|
2853
|
+
if (choices) choices.classList.add("hidden");
|
|
2854
|
+
if (form) form.classList.remove("hidden");
|
|
2855
|
+
const first = div.querySelector(".brand-intake__input");
|
|
2856
|
+
if (first) first.focus();
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2859
|
+
const backBtn = div.querySelector(".brand-intake__back");
|
|
2860
|
+
if (backBtn) backBtn.addEventListener("click", () => {
|
|
2861
|
+
if (form) form.classList.add("hidden");
|
|
2862
|
+
if (choices) choices.classList.remove("hidden");
|
|
2863
|
+
});
|
|
2864
|
+
|
|
2865
|
+
const submitBtn = div.querySelector(".brand-intake__submit");
|
|
2866
|
+
if (submitBtn) submitBtn.addEventListener("click", () => {
|
|
2867
|
+
const brandIntake = {};
|
|
2868
|
+
div.querySelectorAll(".brand-intake__input").forEach((el) => {
|
|
2869
|
+
const v = (el.value || "").trim();
|
|
2870
|
+
if (v) brandIntake[el.dataset.key] = v;
|
|
2871
|
+
});
|
|
2872
|
+
if (Object.keys(brandIntake).length === 0) {
|
|
2873
|
+
// Nothing filled in → treat as "surprise me" rather than a no-op.
|
|
2874
|
+
div.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
2875
|
+
resolveCheckpoint("skip");
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
div.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
2879
|
+
resolveCheckpoint("approve", undefined, { brandIntake });
|
|
2880
|
+
});
|
|
2881
|
+
|
|
2882
|
+
const cancelBtn = div.querySelector('.checkpoint__btn[data-action="cancel"]');
|
|
2883
|
+
if (cancelBtn) cancelBtn.addEventListener("click", () => {
|
|
2884
|
+
div.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
2885
|
+
resolveCheckpoint("cancel");
|
|
2886
|
+
});
|
|
2887
|
+
|
|
2888
|
+
return div;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2249
2891
|
// ---------------------------------------------------------------------------
|
|
2250
2892
|
// Version history panel
|
|
2251
2893
|
// ---------------------------------------------------------------------------
|
|
@@ -3137,6 +3779,13 @@ sendBtn.addEventListener("click", () => {
|
|
|
3137
3779
|
sendMessage(inputEl.value);
|
|
3138
3780
|
});
|
|
3139
3781
|
|
|
3782
|
+
// One-shot button — build the whole page, skipping the design checkpoint.
|
|
3783
|
+
if (oneShotBtn) {
|
|
3784
|
+
oneShotBtn.addEventListener("click", () => {
|
|
3785
|
+
sendMessage(inputEl.value, { oneShot: true });
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3140
3789
|
// Enter to send (Shift+Enter for newline)
|
|
3141
3790
|
inputEl.addEventListener("keydown", (e) => {
|
|
3142
3791
|
if (e.key === "Enter" && !e.shiftKey) {
|