vibespot 1.6.5 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/assets/whats-new.json +27 -0
- package/dist/index.js +579 -681
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/ui/chat.js +671 -9
- package/ui/dashboard.js +53 -12
- package/ui/docs/index.html +57 -2
- package/ui/email-preview.js +1 -5
- package/ui/escape-html.js +14 -0
- package/ui/field-editor.js +6 -12
- package/ui/field-save.js +82 -0
- package/ui/index.html +10 -4
- package/ui/inline-edit.js +116 -570
- package/ui/plan.js +22 -13
- package/ui/preview-agent.js +1050 -0
- package/ui/preview.js +248 -265
- package/ui/section-controls.js +16 -622
- package/ui/setup.js +73 -20
- package/ui/styles.css +424 -0
- package/ui/upload-panel.js +7 -8
- package/ui/whats-new.js +249 -0
- package/assets/readme/00-hero-banner.png +0 -0
- package/assets/readme/00-hero-banner.svg +0 -59
- package/assets/readme/01-vibe-coding-hero.png +0 -0
- package/assets/readme/02-plan-mode.png +0 -0
- package/assets/readme/03-figma-import.png +0 -0
- package/assets/readme/04-multi-page-sites.png +0 -0
- package/assets/readme/05-inline-wysiwyg.png +0 -0
- package/assets/readme/06-hubspot-upload.png +0 -0
- package/ui/docs/screenshots/brand-kit-preview.png +0 -0
- package/ui/docs/screenshots/content-type-dropdown.png +0 -0
- package/ui/docs/screenshots/editor-full-layout.png +0 -0
- package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
- package/ui/docs/screenshots/module-overview-slideout.png +0 -0
- package/ui/docs/screenshots/multi-page-tree.png +0 -0
- package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
- package/ui/docs/screenshots/split-pane-view.png +0 -0
- package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
- package/ui/docs/screenshots/workspace-tabs.png +0 -0
package/ui/chat.js
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
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
|
+
// True while a checkpoint gate card is parked awaiting the user's decision.
|
|
16
|
+
// finishStreaming() clears isStreaming at the gate, so this flag is what
|
|
17
|
+
// actually locks the composer (VIB-1898).
|
|
18
|
+
let checkpointGateActive = false;
|
|
19
|
+
// Softened barge-in (VIB-1876): a message sent during an active build is QUEUED
|
|
20
|
+
// by default and dispatched when the build finishes. The queued chip carries an
|
|
21
|
+
// "Interrupt now" button that triggers the old cancel-and-replan barge-in.
|
|
22
|
+
let queuedMessage = null; // { text, opts }
|
|
23
|
+
let queuedChipEl = null;
|
|
11
24
|
let streamingMsgEl = null;
|
|
12
25
|
let streamBuffer = "";
|
|
13
26
|
let streamStartTime = 0;
|
|
@@ -36,6 +49,9 @@ let lastGenerationBubbleEl = null;
|
|
|
36
49
|
const messagesEl = document.getElementById("chat-messages");
|
|
37
50
|
const inputEl = document.getElementById("chat-input");
|
|
38
51
|
const sendBtn = document.getElementById("chat-send");
|
|
52
|
+
const oneShotBtn = document.getElementById("chat-send-oneshot");
|
|
53
|
+
// Remembered so the barge-in hint placeholder can be restored after a run.
|
|
54
|
+
const chatInputDefaultPlaceholder = (inputEl && inputEl.placeholder) || "";
|
|
39
55
|
const statusText = document.getElementById("status-text");
|
|
40
56
|
const statusEngine = document.getElementById("status-engine");
|
|
41
57
|
const statusTheme = document.getElementById("status-theme");
|
|
@@ -662,6 +678,13 @@ function handleWsMessage(msg) {
|
|
|
662
678
|
}
|
|
663
679
|
}
|
|
664
680
|
|
|
681
|
+
// Rehydrate a parked checkpoint after a refresh / device sleep (VIB-1876):
|
|
682
|
+
// the server kept the gate, so re-render the card and let the user resume
|
|
683
|
+
// from where they were. Plan-mode parks are rehydrated by planController.
|
|
684
|
+
if (msg.pendingCheckpoint && msg.pendingCheckpoint.preview && msg.pendingCheckpoint.kind !== "plan") {
|
|
685
|
+
handleCheckpointRequested({ preview: msg.pendingCheckpoint.preview });
|
|
686
|
+
}
|
|
687
|
+
|
|
665
688
|
// If setup handed us an initial prompt (describe-it path), send it now
|
|
666
689
|
// that the session is live. Skip if the project already has history
|
|
667
690
|
// (e.g. resumed session) to avoid double-submitting.
|
|
@@ -691,6 +714,7 @@ function handleWsMessage(msg) {
|
|
|
691
714
|
highlightOnNextModulesUpdated = true;
|
|
692
715
|
// Refresh the Library tab's module list so it stays in sync
|
|
693
716
|
if (typeof refreshDashboard === "function") refreshDashboard();
|
|
717
|
+
flushQueuedMessage();
|
|
694
718
|
break;
|
|
695
719
|
|
|
696
720
|
case "modules_updated":
|
|
@@ -730,6 +754,9 @@ function handleWsMessage(msg) {
|
|
|
730
754
|
resetPipelineState();
|
|
731
755
|
appendAssistantError(msg.message);
|
|
732
756
|
setStatus("Error");
|
|
757
|
+
// The run this message was queued behind is over — dispatch it rather
|
|
758
|
+
// than stranding the chip forever (VIB-1898).
|
|
759
|
+
flushQueuedMessage();
|
|
733
760
|
break;
|
|
734
761
|
|
|
735
762
|
case "pong":
|
|
@@ -743,14 +770,26 @@ function handleWsMessage(msg) {
|
|
|
743
770
|
handleAgentDecision(msg);
|
|
744
771
|
break;
|
|
745
772
|
case "module_progress":
|
|
773
|
+
enableBargeIn();
|
|
746
774
|
handleModuleProgress(msg);
|
|
747
775
|
break;
|
|
776
|
+
case "generation_superseded":
|
|
777
|
+
// The server confirmed our barge-in cancelled the prior run; the fresh
|
|
778
|
+
// run's events follow. Nothing to do — supersedeCurrentRun already reset.
|
|
779
|
+
setStatus("Redirecting…");
|
|
780
|
+
break;
|
|
748
781
|
case "pipeline_complete":
|
|
749
782
|
handlePipelineComplete(msg);
|
|
750
783
|
break;
|
|
751
784
|
case "pipeline_partial":
|
|
752
785
|
handlePipelinePartial(msg);
|
|
753
786
|
break;
|
|
787
|
+
case "checkpoint_requested":
|
|
788
|
+
handleCheckpointRequested(msg);
|
|
789
|
+
break;
|
|
790
|
+
case "checkpoint_cancelled":
|
|
791
|
+
handleCheckpointCancelled();
|
|
792
|
+
break;
|
|
754
793
|
case "generation_cost":
|
|
755
794
|
handleGenerationCost(msg);
|
|
756
795
|
break;
|
|
@@ -972,6 +1011,8 @@ function setPipelineDetail(text) {
|
|
|
972
1011
|
}
|
|
973
1012
|
|
|
974
1013
|
function handleAgentStep(msg) {
|
|
1014
|
+
// The expensive build stage is the barge-in window (VIB-1880).
|
|
1015
|
+
if (msg.step === "developing") enableBargeIn();
|
|
975
1016
|
ensurePipelineBubble();
|
|
976
1017
|
|
|
977
1018
|
const incoming = msg.step;
|
|
@@ -1285,6 +1326,7 @@ function handlePipelineComplete(msg) {
|
|
|
1285
1326
|
lastGenerationBubbleEl = bubble;
|
|
1286
1327
|
|
|
1287
1328
|
resetPipelineState();
|
|
1329
|
+
flushQueuedMessage();
|
|
1288
1330
|
}
|
|
1289
1331
|
|
|
1290
1332
|
function handlePipelinePartial(msg) {
|
|
@@ -1315,6 +1357,7 @@ function handlePipelinePartial(msg) {
|
|
|
1315
1357
|
lastGenerationBubbleEl = bubble;
|
|
1316
1358
|
|
|
1317
1359
|
resetPipelineState();
|
|
1360
|
+
flushQueuedMessage();
|
|
1318
1361
|
}
|
|
1319
1362
|
|
|
1320
1363
|
function resetPipelineState() {
|
|
@@ -1745,9 +1788,25 @@ fileInputEl.addEventListener("change", () => {
|
|
|
1745
1788
|
// Sending messages
|
|
1746
1789
|
// ---------------------------------------------------------------------------
|
|
1747
1790
|
|
|
1748
|
-
async function sendMessage(text) {
|
|
1791
|
+
async function sendMessage(text, opts = {}) {
|
|
1749
1792
|
const hasFiles = pendingFiles.length > 0;
|
|
1750
|
-
if (
|
|
1793
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
1794
|
+
if (!text.trim() && !hasFiles) return;
|
|
1795
|
+
// A parked checkpoint gate must be resolved through its card. Only the send
|
|
1796
|
+
// buttons were locked before — Enter still reached here (the streaming flags
|
|
1797
|
+
// are reset at the gate) and started a second run mid-park (VIB-1898).
|
|
1798
|
+
if (checkpointGateActive) return;
|
|
1799
|
+
// Block sends while busy — EXCEPT during an active agentic build. There, a new
|
|
1800
|
+
// message is QUEUED by default and runs when the build finishes (VIB-1876);
|
|
1801
|
+
// an explicit Interrupt (opts.interrupt) triggers the cancel-and-replan
|
|
1802
|
+
// barge-in (VIB-1880). Single-call streaming and checkpoint gates stay locked.
|
|
1803
|
+
if (isStreaming && !agenticRunning) return;
|
|
1804
|
+
const buildActive = isStreaming && agenticRunning;
|
|
1805
|
+
if (buildActive && !opts.interrupt) {
|
|
1806
|
+
queueMessage(text, opts);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (buildActive && opts.interrupt) supersedeCurrentRun();
|
|
1751
1810
|
|
|
1752
1811
|
// Remove welcome screen
|
|
1753
1812
|
const welcome = messagesEl.querySelector(".chat__welcome");
|
|
@@ -1794,6 +1853,8 @@ async function sendMessage(text) {
|
|
|
1794
1853
|
if (uploadedFiles.length > 0) {
|
|
1795
1854
|
payload.fileIds = uploadedFiles.map((f) => f.id);
|
|
1796
1855
|
}
|
|
1856
|
+
// One-shot: skip the design checkpoint and build the whole page (VIB-1877).
|
|
1857
|
+
if (opts.oneShot) payload.oneShot = true;
|
|
1797
1858
|
ws.send(JSON.stringify(payload));
|
|
1798
1859
|
|
|
1799
1860
|
// Clear input
|
|
@@ -1836,6 +1897,7 @@ function startStreaming() {
|
|
|
1836
1897
|
streamBuffer = "";
|
|
1837
1898
|
lastStreamStatus = "";
|
|
1838
1899
|
sendBtn.disabled = true;
|
|
1900
|
+
if (oneShotBtn) oneShotBtn.disabled = true;
|
|
1839
1901
|
streamStartTime = Date.now();
|
|
1840
1902
|
if (typeof window.setSelectModeDisabled === "function") {
|
|
1841
1903
|
window.setSelectModeDisabled(true);
|
|
@@ -1984,10 +2046,111 @@ function clearStreamStatus() {
|
|
|
1984
2046
|
if (statusEl) statusEl.remove();
|
|
1985
2047
|
}
|
|
1986
2048
|
|
|
2049
|
+
// Open the barge-in window: the build is running, so re-enable the composer and
|
|
2050
|
+
// let the user redirect the run mid-flight (VIB-1880).
|
|
2051
|
+
function enableBargeIn() {
|
|
2052
|
+
if (agenticRunning) return;
|
|
2053
|
+
agenticRunning = true;
|
|
2054
|
+
sendBtn.disabled = false;
|
|
2055
|
+
if (oneShotBtn) oneShotBtn.disabled = false;
|
|
2056
|
+
if (inputEl) inputEl.placeholder = "Building… your message will queue (or Interrupt to redirect now)";
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// --- Softened barge-in: queue-by-default with an Interrupt button (VIB-1876) ---
|
|
2060
|
+
|
|
2061
|
+
// Queue a message sent mid-build instead of cancelling the run. Latest wins.
|
|
2062
|
+
function queueMessage(text, opts) {
|
|
2063
|
+
if (!text.trim() && pendingFiles.length === 0) return;
|
|
2064
|
+
queuedMessage = { text, opts: { ...opts } };
|
|
2065
|
+
if (inputEl) {
|
|
2066
|
+
inputEl.value = "";
|
|
2067
|
+
inputEl.dispatchEvent(new Event("input"));
|
|
2068
|
+
}
|
|
2069
|
+
renderQueuedChip();
|
|
2070
|
+
setStatus("Message queued");
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function clearQueuedMessage() {
|
|
2074
|
+
queuedMessage = null;
|
|
2075
|
+
if (queuedChipEl) {
|
|
2076
|
+
queuedChipEl.remove();
|
|
2077
|
+
queuedChipEl = null;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// Trigger the real barge-in (cancel-and-replan) with the queued message.
|
|
2082
|
+
function interruptWithQueued() {
|
|
2083
|
+
if (!queuedMessage) return;
|
|
2084
|
+
const { text, opts } = queuedMessage;
|
|
2085
|
+
clearQueuedMessage();
|
|
2086
|
+
sendMessage(text, { ...opts, interrupt: true });
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// Dispatch the queued message once the current build has finished.
|
|
2090
|
+
function flushQueuedMessage() {
|
|
2091
|
+
if (!queuedMessage) return;
|
|
2092
|
+
// A parked gate blocks sendMessage — keep the message queued; the gate's
|
|
2093
|
+
// resolution path flushes again (VIB-1898).
|
|
2094
|
+
if (checkpointGateActive) return;
|
|
2095
|
+
const { text, opts } = queuedMessage;
|
|
2096
|
+
clearQueuedMessage();
|
|
2097
|
+
// Defer so the just-finished run's UI settles before the next one starts.
|
|
2098
|
+
setTimeout(() => sendMessage(text, opts), 0);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function renderQueuedChip() {
|
|
2102
|
+
if (!queuedMessage) return clearQueuedMessage();
|
|
2103
|
+
const area = inputEl && inputEl.closest(".chat__input-area");
|
|
2104
|
+
if (!area) return;
|
|
2105
|
+
if (!queuedChipEl) {
|
|
2106
|
+
queuedChipEl = document.createElement("div");
|
|
2107
|
+
queuedChipEl.className = "queued-msg";
|
|
2108
|
+
queuedChipEl.style.cssText =
|
|
2109
|
+
"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;";
|
|
2110
|
+
area.insertBefore(queuedChipEl, area.firstChild);
|
|
2111
|
+
}
|
|
2112
|
+
const t = queuedMessage.text || "(files attached)";
|
|
2113
|
+
const preview = t.length > 90 ? t.slice(0, 90) + "…" : t;
|
|
2114
|
+
queuedChipEl.innerHTML =
|
|
2115
|
+
'<span aria-hidden="true">⏳</span>' +
|
|
2116
|
+
'<span style="opacity:.7;white-space:nowrap;">Queued — runs next:</span>' +
|
|
2117
|
+
'<span class="queued-msg__text" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>' +
|
|
2118
|
+
'<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>' +
|
|
2119
|
+
'<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>';
|
|
2120
|
+
queuedChipEl.querySelector(".queued-msg__text").textContent = preview;
|
|
2121
|
+
queuedChipEl.querySelector(".queued-msg__interrupt").onclick = interruptWithQueued;
|
|
2122
|
+
queuedChipEl.querySelector(".queued-msg__cancel").onclick = clearQueuedMessage;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// Tear down the superseded run's transient UI so the replacement run renders
|
|
2126
|
+
// fresh (called when a barge-in send cancels the active build).
|
|
2127
|
+
function supersedeCurrentRun() {
|
|
2128
|
+
stopStreamTimer();
|
|
2129
|
+
clearStreamStatus();
|
|
2130
|
+
const streamingEl = messagesEl.querySelector(".chat-msg--streaming");
|
|
2131
|
+
if (streamingEl) {
|
|
2132
|
+
streamingEl.classList.remove("chat-msg--streaming");
|
|
2133
|
+
const bubble = streamingEl.querySelector(".chat-msg__bubble");
|
|
2134
|
+
if (bubble && !bubble.textContent.trim()) {
|
|
2135
|
+
bubble.innerHTML = '<em class="message__placeholder">Superseded by your next message.</em>';
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
removeCheckpointCard();
|
|
2139
|
+
checkpointGateActive = false;
|
|
2140
|
+
isStreaming = false;
|
|
2141
|
+
agenticRunning = false;
|
|
2142
|
+
streamingMsgEl = null;
|
|
2143
|
+
streamBuffer = "";
|
|
2144
|
+
resetPipelineState();
|
|
2145
|
+
}
|
|
2146
|
+
|
|
1987
2147
|
function finishStreaming() {
|
|
1988
2148
|
if (!isStreaming) return;
|
|
1989
2149
|
isStreaming = false;
|
|
2150
|
+
agenticRunning = false;
|
|
2151
|
+
if (inputEl) inputEl.placeholder = chatInputDefaultPlaceholder;
|
|
1990
2152
|
sendBtn.disabled = false;
|
|
2153
|
+
if (oneShotBtn) oneShotBtn.disabled = false;
|
|
1991
2154
|
if (typeof window.setSelectModeDisabled === "function") {
|
|
1992
2155
|
window.setSelectModeDisabled(false);
|
|
1993
2156
|
}
|
|
@@ -2008,6 +2171,24 @@ function finishStreaming() {
|
|
|
2008
2171
|
if (streamingEl) {
|
|
2009
2172
|
streamingEl.classList.remove("chat-msg--streaming");
|
|
2010
2173
|
|
|
2174
|
+
// Agentic seams (a checkpoint gate, or a resume that re-parks / just builds
|
|
2175
|
+
// modules) leave a streaming bubble that never received any text or progress
|
|
2176
|
+
// stepper. Don't finalize it into an empty bubble with a lone duration —
|
|
2177
|
+
// drop it (VIB-1876 follow-up). A bubble carrying the pipeline stepper or
|
|
2178
|
+
// any rendered content has child nodes, so it's kept.
|
|
2179
|
+
const bubbleEl = streamingEl.querySelector(".chat-msg__bubble");
|
|
2180
|
+
const hasContent =
|
|
2181
|
+
!!bubbleEl && (bubbleEl.children.length > 0 || bubbleEl.textContent.trim().length > 0);
|
|
2182
|
+
const willRenderText = !!streamBuffer && streamBuffer.trim().length > 0;
|
|
2183
|
+
if (!hasContent && !willRenderText) {
|
|
2184
|
+
streamingEl.remove();
|
|
2185
|
+
streamingMsgEl = null;
|
|
2186
|
+
streamBuffer = "";
|
|
2187
|
+
setStatus("Ready");
|
|
2188
|
+
scrollToBottom();
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2011
2192
|
// Add duration metadata beneath the bubble (inside .chat-msg__content)
|
|
2012
2193
|
const meta = document.createElement("div");
|
|
2013
2194
|
meta.className = "chat-msg__meta";
|
|
@@ -2091,13 +2272,7 @@ function renderMarkdown(text) {
|
|
|
2091
2272
|
// Helpers
|
|
2092
2273
|
// ---------------------------------------------------------------------------
|
|
2093
2274
|
|
|
2094
|
-
|
|
2095
|
-
return str
|
|
2096
|
-
.replace(/&/g, "&")
|
|
2097
|
-
.replace(/</g, "<")
|
|
2098
|
-
.replace(/>/g, ">")
|
|
2099
|
-
.replace(/"/g, """);
|
|
2100
|
-
}
|
|
2275
|
+
// escapeHtml comes from the shared ui/escape-html.js (VIB-1902).
|
|
2101
2276
|
|
|
2102
2277
|
function scrollToBottom() {
|
|
2103
2278
|
if (scrollScheduled) return;
|
|
@@ -2246,6 +2421,486 @@ function appendSystemMessage(text) {
|
|
|
2246
2421
|
scrollToBottom();
|
|
2247
2422
|
}
|
|
2248
2423
|
|
|
2424
|
+
// ---------------------------------------------------------------------------
|
|
2425
|
+
// Checkpoint gate card (VIB-1877) — the design preview the user approves /
|
|
2426
|
+
// steers / skips / cancels before the expensive module build.
|
|
2427
|
+
// ---------------------------------------------------------------------------
|
|
2428
|
+
|
|
2429
|
+
let checkpointCardEl = null;
|
|
2430
|
+
let currentCheckpointKind = null;
|
|
2431
|
+
let currentCheckpointData = null;
|
|
2432
|
+
|
|
2433
|
+
function setSendDisabled(disabled) {
|
|
2434
|
+
sendBtn.disabled = disabled;
|
|
2435
|
+
if (oneShotBtn) oneShotBtn.disabled = disabled;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
function removeCheckpointCard() {
|
|
2439
|
+
if (checkpointCardEl && checkpointCardEl.parentNode) {
|
|
2440
|
+
checkpointCardEl.parentNode.removeChild(checkpointCardEl);
|
|
2441
|
+
}
|
|
2442
|
+
checkpointCardEl = null;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// One-line record of what the user decided at a checkpoint (VIB-1876). Boris:
|
|
2446
|
+
// keep the decision in the transcript rather than dropping the turn entirely.
|
|
2447
|
+
function summarizeDecision(kind, action, note, extra) {
|
|
2448
|
+
const label =
|
|
2449
|
+
kind === "brand_intake" ? "Brand" : kind === "structure" ? "Structure" : kind === "plan" ? "Plan" : "Design";
|
|
2450
|
+
if (action === "cancel") return `${label}: cancelled — nothing was built.`;
|
|
2451
|
+
if (action === "steer") return `${label}: steered${note ? " — " + note : ""}.`;
|
|
2452
|
+
if (kind === "brand_intake") {
|
|
2453
|
+
if (action !== "approve") return "Brand: surprise me — generating a design system.";
|
|
2454
|
+
const ch =
|
|
2455
|
+
extra && extra.brandIntake
|
|
2456
|
+
? Object.keys(extra.brandIntake).filter((k) => extra.brandIntake[k])
|
|
2457
|
+
: [];
|
|
2458
|
+
return `Brand: bringing your brand${ch.length ? " (" + ch.join(", ") + ")" : ""}.`;
|
|
2459
|
+
}
|
|
2460
|
+
if (action === "skip") return `${label}: skipped remaining checkpoints — building now.`;
|
|
2461
|
+
return `${label}: approved.`;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// The substantive content/facts behind a decision (VIB-1876): the palette/type
|
|
2465
|
+
// approved, the module outline kept, or the brand inputs provided — so the
|
|
2466
|
+
// transcript records *what*, not just the verb. Returns "" when there's nothing.
|
|
2467
|
+
function decisionFacts(kind, action, note, extra, data) {
|
|
2468
|
+
if (action === "cancel") return "";
|
|
2469
|
+
if (action === "steer") return note ? "Note: " + note : "";
|
|
2470
|
+
const parts = [];
|
|
2471
|
+
const clip = (s) => String(s).replace(/\s+/g, " ").trim().slice(0, 80);
|
|
2472
|
+
if (kind === "design" || kind === "plan") {
|
|
2473
|
+
const pal = Array.isArray(data && data.palette)
|
|
2474
|
+
? data.palette.map((p) => p && p.value).filter(Boolean).slice(0, 5)
|
|
2475
|
+
: [];
|
|
2476
|
+
if (pal.length) parts.push("Palette: " + pal.join(", "));
|
|
2477
|
+
const typo = (data && data.typography) || {};
|
|
2478
|
+
if (typo.heading || typo.body) parts.push("Type: " + [typo.heading, typo.body].filter(Boolean).join(" / "));
|
|
2479
|
+
} else if (kind === "structure") {
|
|
2480
|
+
const rows =
|
|
2481
|
+
extra && Array.isArray(extra.outline) && extra.outline.length
|
|
2482
|
+
? extra.outline
|
|
2483
|
+
: data && Array.isArray(data.modules)
|
|
2484
|
+
? data.modules
|
|
2485
|
+
: [];
|
|
2486
|
+
const names = rows.map((r) => r && (r.name || r.label)).filter(Boolean);
|
|
2487
|
+
if (names.length) parts.push(`Sections (${names.length}): ` + names.join(", "));
|
|
2488
|
+
} else if (kind === "brand_intake") {
|
|
2489
|
+
const bi = (extra && extra.brandIntake) || {};
|
|
2490
|
+
if (bi.colors) parts.push("Colors: " + clip(bi.colors));
|
|
2491
|
+
if (bi.siteUrl) parts.push("Site: " + clip(bi.siteUrl));
|
|
2492
|
+
if (bi.themePath) parts.push("Theme: " + clip(bi.themePath));
|
|
2493
|
+
if (bi.code) parts.push("Pasted code");
|
|
2494
|
+
if (bi.voice) parts.push("Voice: " + clip(bi.voice));
|
|
2495
|
+
}
|
|
2496
|
+
return parts.join(" · ");
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// Convert the live checkpoint card into a static decision record so the turn
|
|
2500
|
+
// keeps meaning in the transcript instead of leaving an empty bubble (VIB-1876).
|
|
2501
|
+
// Rebuilds a standard assistant bubble (the card renderers use bespoke markup).
|
|
2502
|
+
function finalizeCheckpointCard(action, note, extra) {
|
|
2503
|
+
if (!checkpointCardEl) return;
|
|
2504
|
+
const kind = currentCheckpointKind || "design";
|
|
2505
|
+
const text = summarizeDecision(kind, action, note, extra);
|
|
2506
|
+
const facts = decisionFacts(kind, action, note, extra, currentCheckpointData);
|
|
2507
|
+
const glyph = action === "cancel" ? "✕" : "✓";
|
|
2508
|
+
const time = formatMessageTime(Date.now());
|
|
2509
|
+
checkpointCardEl.className = "chat-msg chat-msg--assistant chat-msg--checkpoint-resolved";
|
|
2510
|
+
checkpointCardEl.innerHTML =
|
|
2511
|
+
'<div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>' +
|
|
2512
|
+
'<div class="chat-msg__content">' +
|
|
2513
|
+
'<div class="chat-msg__header"><span class="chat-msg__sender">vibeSpot AI</span>' +
|
|
2514
|
+
'<span class="chat-msg__time">' + time + "</span></div>" +
|
|
2515
|
+
'<div class="chat-msg__bubble">' +
|
|
2516
|
+
'<div class="checkpoint-decision" style="opacity:.85;"></div>' +
|
|
2517
|
+
'<div class="checkpoint-decision__facts" style="opacity:.6;font-size:12px;margin-top:2px;"></div>' +
|
|
2518
|
+
"</div></div>";
|
|
2519
|
+
checkpointCardEl.querySelector(".checkpoint-decision").textContent = glyph + " " + text;
|
|
2520
|
+
const factsEl = checkpointCardEl.querySelector(".checkpoint-decision__facts");
|
|
2521
|
+
if (facts) factsEl.textContent = facts;
|
|
2522
|
+
else factsEl.remove();
|
|
2523
|
+
checkpointCardEl = null;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// When a stepper card is left behind (the run parks at a gate), mark the stages
|
|
2527
|
+
// it actually reached as done so the older card reads "complete up to here"
|
|
2528
|
+
// rather than frozen mid-stage (VIB-1876).
|
|
2529
|
+
function finalizeStepperProgress() {
|
|
2530
|
+
if (!pipelineStepperEl) return;
|
|
2531
|
+
pipelineStepperEl.querySelectorAll(".pipeline-stage--active").forEach((el) => {
|
|
2532
|
+
const step = el.getAttribute("data-stage");
|
|
2533
|
+
if (step) setStageStatus(step, "done");
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
function handleCheckpointRequested(msg) {
|
|
2538
|
+
// Settle the in-flight progress bubble; the gate now waits on the user.
|
|
2539
|
+
clearStreamStatus();
|
|
2540
|
+
finalizeStepperProgress();
|
|
2541
|
+
finishStreaming();
|
|
2542
|
+
removeCheckpointCard();
|
|
2543
|
+
|
|
2544
|
+
const preview = msg.preview || {};
|
|
2545
|
+
const data = preview.data || {};
|
|
2546
|
+
currentCheckpointKind = preview.kind || "design";
|
|
2547
|
+
currentCheckpointData = data;
|
|
2548
|
+
checkpointCardEl = renderCheckpointCard(preview, data, msg.estCostNext);
|
|
2549
|
+
messagesEl.appendChild(checkpointCardEl);
|
|
2550
|
+
scrollToBottom();
|
|
2551
|
+
|
|
2552
|
+
// Hold the composer until the gate is resolved — buttons AND the Enter path
|
|
2553
|
+
// through sendMessage (VIB-1898).
|
|
2554
|
+
checkpointGateActive = true;
|
|
2555
|
+
setSendDisabled(true);
|
|
2556
|
+
setStatus("Awaiting your call");
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
function handleCheckpointCancelled() {
|
|
2560
|
+
removeCheckpointCard();
|
|
2561
|
+
checkpointGateActive = false;
|
|
2562
|
+
setSendDisabled(false);
|
|
2563
|
+
appendSystemMessage("Cancelled — nothing was built.");
|
|
2564
|
+
flushQueuedMessage();
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function resolveCheckpoint(action, note, extra) {
|
|
2568
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
2569
|
+
checkpointGateActive = false;
|
|
2570
|
+
// Leave the decision in the transcript (card → one-line record), not an empty
|
|
2571
|
+
// bubble (VIB-1876).
|
|
2572
|
+
finalizeCheckpointCard(action, note, extra);
|
|
2573
|
+
if (action === "cancel") {
|
|
2574
|
+
setSendDisabled(false);
|
|
2575
|
+
} else {
|
|
2576
|
+
// Work resumes. Don't pre-create an empty bubble — clear the prior turn's
|
|
2577
|
+
// pipeline refs so the resume's first progress event builds its OWN stepper
|
|
2578
|
+
// bubble (ensurePipelineBubble returns early while pipelineBubbleEl is set).
|
|
2579
|
+
isStreaming = true;
|
|
2580
|
+
setSendDisabled(true);
|
|
2581
|
+
pipelineBubbleEl = null;
|
|
2582
|
+
pipelineStepperEl = null;
|
|
2583
|
+
pipelineDetailEl = null;
|
|
2584
|
+
pipelineModulesEl = null;
|
|
2585
|
+
pipelineEstimateEl = null;
|
|
2586
|
+
streamingMsgEl = null;
|
|
2587
|
+
streamStartTime = Date.now();
|
|
2588
|
+
setStatus("Generating...");
|
|
2589
|
+
}
|
|
2590
|
+
ws.send(JSON.stringify({
|
|
2591
|
+
type: "checkpoint_resolve",
|
|
2592
|
+
action,
|
|
2593
|
+
note: note || undefined,
|
|
2594
|
+
// Structure checkpoint (VIB-1879) outline; brand-intake channels (VIB-1878).
|
|
2595
|
+
outline: extra && extra.outline ? extra.outline : undefined,
|
|
2596
|
+
brandIntake: extra && extra.brandIntake ? extra.brandIntake : undefined,
|
|
2597
|
+
}));
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
function renderCheckpointCard(preview, data, estCostNext) {
|
|
2601
|
+
// Brand-intake gate (VIB-1878) — the front-of-flow ask-back; different card.
|
|
2602
|
+
if (preview && preview.kind === "brand_intake") {
|
|
2603
|
+
return renderBrandIntakeCard(preview, data);
|
|
2604
|
+
}
|
|
2605
|
+
// Structure checkpoint (VIB-1879) renders an editable module outline instead
|
|
2606
|
+
// of the design palette/hero.
|
|
2607
|
+
if (preview && preview.kind === "structure") {
|
|
2608
|
+
return renderStructureCheckpointCard(preview, data, estCostNext);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
const div = document.createElement("div");
|
|
2612
|
+
div.className = "chat-msg chat-msg--assistant";
|
|
2613
|
+
|
|
2614
|
+
const palette = Array.isArray(data.palette) ? data.palette : [];
|
|
2615
|
+
const swatches = palette.map((p) =>
|
|
2616
|
+
`<div class="checkpoint__swatch" title="${escapeHtml(p.label)}: ${escapeHtml(p.value)}">
|
|
2617
|
+
<span class="checkpoint__swatch-chip" style="background:${escapeHtml(p.value)}"></span>
|
|
2618
|
+
<span class="checkpoint__swatch-label">${escapeHtml(p.label)}</span>
|
|
2619
|
+
</div>`).join("");
|
|
2620
|
+
|
|
2621
|
+
const typo = data.typography || {};
|
|
2622
|
+
const heroSrc = typeof data.heroHtml === "string" ? data.heroHtml : "";
|
|
2623
|
+
const costLine = (typeof estCostNext === "number" && estCostNext > 0)
|
|
2624
|
+
? `<span class="checkpoint__cost">Building all modules will cost ~$${estCostNext.toFixed(2)}</span>`
|
|
2625
|
+
: "";
|
|
2626
|
+
|
|
2627
|
+
div.innerHTML = `
|
|
2628
|
+
<div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
|
|
2629
|
+
<div class="chat-msg__content">
|
|
2630
|
+
<div class="checkpoint">
|
|
2631
|
+
<div class="checkpoint__head">
|
|
2632
|
+
<span class="checkpoint__badge">Design checkpoint</span>
|
|
2633
|
+
<span class="checkpoint__headline">${escapeHtml(preview.headline || "Review the design before building")}</span>
|
|
2634
|
+
</div>
|
|
2635
|
+
<div class="checkpoint__hero">
|
|
2636
|
+
<iframe class="checkpoint__hero-frame" title="Hero preview" sandbox="" srcdoc="${escapeHtml(heroSrc)}"></iframe>
|
|
2637
|
+
</div>
|
|
2638
|
+
<div class="checkpoint__tokens">
|
|
2639
|
+
<div class="checkpoint__palette">${swatches || '<span class="checkpoint__muted">No palette tokens</span>'}</div>
|
|
2640
|
+
<div class="checkpoint__type">
|
|
2641
|
+
<div class="checkpoint__type-row" style="font-family:${escapeHtml(typo.heading || "")}">Aa — ${escapeHtml(typo.heading || "Heading font")}</div>
|
|
2642
|
+
<div class="checkpoint__type-row checkpoint__type-row--body" style="font-family:${escapeHtml(typo.body || "")}">Body — ${escapeHtml(typo.body || "Body font")}</div>
|
|
2643
|
+
</div>
|
|
2644
|
+
</div>
|
|
2645
|
+
${costLine ? `<div class="checkpoint__meta">${costLine}</div>` : ""}
|
|
2646
|
+
<div class="checkpoint__steer hidden">
|
|
2647
|
+
<textarea class="checkpoint__steer-input" rows="2" placeholder="What should change? e.g. warmer palette, bigger headings..."></textarea>
|
|
2648
|
+
</div>
|
|
2649
|
+
<div class="checkpoint__actions">
|
|
2650
|
+
<button class="btn btn--primary checkpoint__btn" data-action="approve">Looks good — build it</button>
|
|
2651
|
+
<button class="btn checkpoint__btn" data-action="steer">Tweak the design</button>
|
|
2652
|
+
<button class="btn checkpoint__btn" data-action="skip" title="Build now and don't ask again this run">Skip checkpoints</button>
|
|
2653
|
+
<button class="btn btn--ghost checkpoint__btn" data-action="cancel">Cancel</button>
|
|
2654
|
+
</div>
|
|
2655
|
+
</div>
|
|
2656
|
+
</div>`;
|
|
2657
|
+
|
|
2658
|
+
const steerWrap = div.querySelector(".checkpoint__steer");
|
|
2659
|
+
const steerInput = div.querySelector(".checkpoint__steer-input");
|
|
2660
|
+
const steerBtn = div.querySelector('.checkpoint__btn[data-action="steer"]');
|
|
2661
|
+
let steerArmed = false;
|
|
2662
|
+
|
|
2663
|
+
div.querySelectorAll(".checkpoint__btn").forEach((btn) => {
|
|
2664
|
+
btn.addEventListener("click", () => {
|
|
2665
|
+
const action = btn.dataset.action;
|
|
2666
|
+
// Steer is two-step: first click reveals the note box, second submits.
|
|
2667
|
+
if (action === "steer" && !steerArmed) {
|
|
2668
|
+
steerArmed = true;
|
|
2669
|
+
steerWrap.classList.remove("hidden");
|
|
2670
|
+
steerBtn.textContent = "Apply tweak";
|
|
2671
|
+
if (steerInput) steerInput.focus();
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
const note = action === "steer" && steerInput ? steerInput.value.trim() : undefined;
|
|
2675
|
+
div.querySelectorAll(".checkpoint__btn").forEach((b) => (b.disabled = true));
|
|
2676
|
+
resolveCheckpoint(action, note);
|
|
2677
|
+
});
|
|
2678
|
+
});
|
|
2679
|
+
|
|
2680
|
+
return div;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// Structure checkpoint card (VIB-1879) — the planned module skeleton as an
|
|
2684
|
+
// editable outline (reorder / cut / add / rename) before the parallel build.
|
|
2685
|
+
function renderStructureCheckpointCard(preview, data, estCostNext) {
|
|
2686
|
+
const div = document.createElement("div");
|
|
2687
|
+
div.className = "chat-msg chat-msg--assistant";
|
|
2688
|
+
|
|
2689
|
+
const modules = Array.isArray(data.modules) ? data.modules : [];
|
|
2690
|
+
const narrative = typeof data.narrative === "string" ? data.narrative : "";
|
|
2691
|
+
const costLine = (typeof estCostNext === "number" && estCostNext > 0)
|
|
2692
|
+
? `<span class="checkpoint__cost">Building all modules will cost ~$${estCostNext.toFixed(2)}</span>`
|
|
2693
|
+
: "";
|
|
2694
|
+
|
|
2695
|
+
div.innerHTML = `
|
|
2696
|
+
<div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
|
|
2697
|
+
<div class="chat-msg__content">
|
|
2698
|
+
<div class="checkpoint checkpoint--structure">
|
|
2699
|
+
<div class="checkpoint__head">
|
|
2700
|
+
<span class="checkpoint__badge">Structure checkpoint</span>
|
|
2701
|
+
<span class="checkpoint__headline">${escapeHtml(preview.headline || "Review the structure before building")}</span>
|
|
2702
|
+
</div>
|
|
2703
|
+
${narrative ? `<div class="checkpoint__narrative">${escapeHtml(narrative)}</div>` : ""}
|
|
2704
|
+
<div class="checkpoint__outline"></div>
|
|
2705
|
+
<button type="button" class="btn btn--ghost checkpoint__add">+ Add section</button>
|
|
2706
|
+
${costLine ? `<div class="checkpoint__meta">${costLine}</div>` : ""}
|
|
2707
|
+
<div class="checkpoint__steer hidden">
|
|
2708
|
+
<textarea class="checkpoint__steer-input" rows="2" placeholder="What should change? e.g. add a pricing section, drop the FAQ..."></textarea>
|
|
2709
|
+
</div>
|
|
2710
|
+
<div class="checkpoint__actions">
|
|
2711
|
+
<button class="btn btn--primary checkpoint__btn" data-action="approve">Build these sections</button>
|
|
2712
|
+
<button class="btn checkpoint__btn" data-action="steer">Re-plan structure</button>
|
|
2713
|
+
<button class="btn checkpoint__btn" data-action="skip" title="Build now and don't ask again this run">Skip checkpoints</button>
|
|
2714
|
+
<button class="btn btn--ghost checkpoint__btn" data-action="cancel">Cancel</button>
|
|
2715
|
+
</div>
|
|
2716
|
+
</div>
|
|
2717
|
+
</div>`;
|
|
2718
|
+
|
|
2719
|
+
const listEl = div.querySelector(".checkpoint__outline");
|
|
2720
|
+
|
|
2721
|
+
function makeRow(name, description, sourceIndex) {
|
|
2722
|
+
const row = document.createElement("div");
|
|
2723
|
+
row.className = "checkpoint__row";
|
|
2724
|
+
if (sourceIndex != null) row.dataset.sourceIndex = String(sourceIndex);
|
|
2725
|
+
if (description) row.dataset.desc = description;
|
|
2726
|
+
row.innerHTML = `
|
|
2727
|
+
<div class="checkpoint__row-move">
|
|
2728
|
+
<button type="button" class="checkpoint__row-btn" data-move="up" title="Move up" aria-label="Move up">▲</button>
|
|
2729
|
+
<button type="button" class="checkpoint__row-btn" data-move="down" title="Move down" aria-label="Move down">▼</button>
|
|
2730
|
+
</div>
|
|
2731
|
+
<div class="checkpoint__row-main">
|
|
2732
|
+
<input class="checkpoint__row-name" value="${escapeHtml(name || "")}" placeholder="section-name" spellcheck="false" />
|
|
2733
|
+
<span class="checkpoint__row-desc">${escapeHtml(description || "")}</span>
|
|
2734
|
+
</div>
|
|
2735
|
+
<button type="button" class="checkpoint__row-remove" data-remove title="Remove section" aria-label="Remove section">✕</button>`;
|
|
2736
|
+
|
|
2737
|
+
row.querySelector('[data-move="up"]').addEventListener("click", () => {
|
|
2738
|
+
const prev = row.previousElementSibling;
|
|
2739
|
+
if (prev) listEl.insertBefore(row, prev);
|
|
2740
|
+
});
|
|
2741
|
+
row.querySelector('[data-move="down"]').addEventListener("click", () => {
|
|
2742
|
+
const next = row.nextElementSibling;
|
|
2743
|
+
if (next) listEl.insertBefore(next, row);
|
|
2744
|
+
});
|
|
2745
|
+
row.querySelector("[data-remove]").addEventListener("click", () => {
|
|
2746
|
+
row.remove();
|
|
2747
|
+
});
|
|
2748
|
+
return row;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
modules.forEach((m) =>
|
|
2752
|
+
listEl.appendChild(makeRow(m.name, m.description, m.sourceIndex)),
|
|
2753
|
+
);
|
|
2754
|
+
|
|
2755
|
+
div.querySelector(".checkpoint__add").addEventListener("click", () => {
|
|
2756
|
+
const row = makeRow("", "", null);
|
|
2757
|
+
listEl.appendChild(row);
|
|
2758
|
+
const input = row.querySelector(".checkpoint__row-name");
|
|
2759
|
+
if (input) input.focus();
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2762
|
+
function collectOutline() {
|
|
2763
|
+
return [...listEl.querySelectorAll(".checkpoint__row")]
|
|
2764
|
+
.map((row) => {
|
|
2765
|
+
const name = (row.querySelector(".checkpoint__row-name").value || "").trim();
|
|
2766
|
+
const si = row.dataset.sourceIndex;
|
|
2767
|
+
return {
|
|
2768
|
+
name,
|
|
2769
|
+
description: row.dataset.desc || undefined,
|
|
2770
|
+
sourceIndex: si !== undefined && si !== "" ? Number(si) : undefined,
|
|
2771
|
+
};
|
|
2772
|
+
})
|
|
2773
|
+
.filter((it) => it.name);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
const steerWrap = div.querySelector(".checkpoint__steer");
|
|
2777
|
+
const steerInput = div.querySelector(".checkpoint__steer-input");
|
|
2778
|
+
const steerBtn = div.querySelector('.checkpoint__btn[data-action="steer"]');
|
|
2779
|
+
let steerArmed = false;
|
|
2780
|
+
|
|
2781
|
+
div.querySelectorAll(".checkpoint__btn").forEach((btn) => {
|
|
2782
|
+
btn.addEventListener("click", () => {
|
|
2783
|
+
const action = btn.dataset.action;
|
|
2784
|
+
// Steer is two-step: first click reveals the note box, second submits.
|
|
2785
|
+
if (action === "steer" && !steerArmed) {
|
|
2786
|
+
steerArmed = true;
|
|
2787
|
+
steerWrap.classList.remove("hidden");
|
|
2788
|
+
steerBtn.textContent = "Apply re-plan";
|
|
2789
|
+
if (steerInput) steerInput.focus();
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
const note = action === "steer" && steerInput ? steerInput.value.trim() : undefined;
|
|
2793
|
+
// approve/skip carry the edited outline; steer/cancel don't.
|
|
2794
|
+
const outline = action === "approve" || action === "skip" ? collectOutline() : undefined;
|
|
2795
|
+
div.querySelectorAll(".checkpoint__btn").forEach((b) => (b.disabled = true));
|
|
2796
|
+
resolveCheckpoint(action, note, { outline });
|
|
2797
|
+
});
|
|
2798
|
+
});
|
|
2799
|
+
|
|
2800
|
+
return div;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// ---------------------------------------------------------------------------
|
|
2804
|
+
// Brand-intake gate card (VIB-1878) — the front-of-flow ask-back shown when a
|
|
2805
|
+
// new page has no style system yet: "Surprise me" (AI invents) vs "Bring your
|
|
2806
|
+
// brand" (paste colors/code/voice, or point at a site/theme → seed the design).
|
|
2807
|
+
// ---------------------------------------------------------------------------
|
|
2808
|
+
|
|
2809
|
+
function renderBrandIntakeCard(preview, data) {
|
|
2810
|
+
const div = document.createElement("div");
|
|
2811
|
+
div.className = "chat-msg chat-msg--assistant";
|
|
2812
|
+
|
|
2813
|
+
const channels = Array.isArray(data.channels) ? data.channels : [];
|
|
2814
|
+
const fields = channels.map((c) => {
|
|
2815
|
+
const id = `brand-${escapeHtml(c.key)}`;
|
|
2816
|
+
const input = c.multiline
|
|
2817
|
+
? `<textarea class="brand-intake__input" data-key="${escapeHtml(c.key)}" rows="3" placeholder="${escapeHtml(c.placeholder || "")}"></textarea>`
|
|
2818
|
+
: `<input class="brand-intake__input" data-key="${escapeHtml(c.key)}" type="text" placeholder="${escapeHtml(c.placeholder || "")}" />`;
|
|
2819
|
+
return `<label class="brand-intake__field" for="${id}">
|
|
2820
|
+
<span class="brand-intake__label">${escapeHtml(c.label || c.key)}</span>
|
|
2821
|
+
${input}
|
|
2822
|
+
</label>`;
|
|
2823
|
+
}).join("");
|
|
2824
|
+
|
|
2825
|
+
div.innerHTML = `
|
|
2826
|
+
<div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
|
|
2827
|
+
<div class="chat-msg__content">
|
|
2828
|
+
<div class="checkpoint brand-intake">
|
|
2829
|
+
<div class="checkpoint__head">
|
|
2830
|
+
<span class="checkpoint__badge">Brand checkpoint</span>
|
|
2831
|
+
<span class="checkpoint__headline">${escapeHtml(preview.headline || "Match your brand, or surprise you?")}</span>
|
|
2832
|
+
</div>
|
|
2833
|
+
<div class="brand-intake__choices">
|
|
2834
|
+
<button class="btn btn--primary brand-intake__choice" data-choice="surprise">
|
|
2835
|
+
<span class="brand-intake__choice-title">${escapeHtml(data.surpriseLabel || "Surprise me")}</span>
|
|
2836
|
+
<span class="brand-intake__choice-hint">${escapeHtml(data.surpriseHint || "")}</span>
|
|
2837
|
+
</button>
|
|
2838
|
+
<button class="btn brand-intake__choice" data-choice="bring">
|
|
2839
|
+
<span class="brand-intake__choice-title">${escapeHtml(data.bringLabel || "Bring your brand")}</span>
|
|
2840
|
+
<span class="brand-intake__choice-hint">${escapeHtml(data.bringHint || "")}</span>
|
|
2841
|
+
</button>
|
|
2842
|
+
</div>
|
|
2843
|
+
<div class="brand-intake__form hidden">
|
|
2844
|
+
${fields}
|
|
2845
|
+
<div class="brand-intake__form-actions">
|
|
2846
|
+
<button class="btn btn--primary brand-intake__submit">Use my brand</button>
|
|
2847
|
+
<button class="btn btn--ghost brand-intake__back">Back</button>
|
|
2848
|
+
</div>
|
|
2849
|
+
</div>
|
|
2850
|
+
<div class="checkpoint__actions brand-intake__top-actions">
|
|
2851
|
+
<button class="btn btn--ghost checkpoint__btn" data-action="cancel">Cancel</button>
|
|
2852
|
+
</div>
|
|
2853
|
+
</div>
|
|
2854
|
+
</div>`;
|
|
2855
|
+
|
|
2856
|
+
const form = div.querySelector(".brand-intake__form");
|
|
2857
|
+
const choices = div.querySelector(".brand-intake__choices");
|
|
2858
|
+
|
|
2859
|
+
div.querySelector('[data-choice="surprise"]').addEventListener("click", () => {
|
|
2860
|
+
div.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
2861
|
+
// "Surprise me" = skip the brand step; the AI invents the design system.
|
|
2862
|
+
resolveCheckpoint("skip");
|
|
2863
|
+
});
|
|
2864
|
+
|
|
2865
|
+
div.querySelector('[data-choice="bring"]').addEventListener("click", () => {
|
|
2866
|
+
if (choices) choices.classList.add("hidden");
|
|
2867
|
+
if (form) form.classList.remove("hidden");
|
|
2868
|
+
const first = div.querySelector(".brand-intake__input");
|
|
2869
|
+
if (first) first.focus();
|
|
2870
|
+
});
|
|
2871
|
+
|
|
2872
|
+
const backBtn = div.querySelector(".brand-intake__back");
|
|
2873
|
+
if (backBtn) backBtn.addEventListener("click", () => {
|
|
2874
|
+
if (form) form.classList.add("hidden");
|
|
2875
|
+
if (choices) choices.classList.remove("hidden");
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
const submitBtn = div.querySelector(".brand-intake__submit");
|
|
2879
|
+
if (submitBtn) submitBtn.addEventListener("click", () => {
|
|
2880
|
+
const brandIntake = {};
|
|
2881
|
+
div.querySelectorAll(".brand-intake__input").forEach((el) => {
|
|
2882
|
+
const v = (el.value || "").trim();
|
|
2883
|
+
if (v) brandIntake[el.dataset.key] = v;
|
|
2884
|
+
});
|
|
2885
|
+
if (Object.keys(brandIntake).length === 0) {
|
|
2886
|
+
// Nothing filled in → treat as "surprise me" rather than a no-op.
|
|
2887
|
+
div.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
2888
|
+
resolveCheckpoint("skip");
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
div.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
2892
|
+
resolveCheckpoint("approve", undefined, { brandIntake });
|
|
2893
|
+
});
|
|
2894
|
+
|
|
2895
|
+
const cancelBtn = div.querySelector('.checkpoint__btn[data-action="cancel"]');
|
|
2896
|
+
if (cancelBtn) cancelBtn.addEventListener("click", () => {
|
|
2897
|
+
div.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
2898
|
+
resolveCheckpoint("cancel");
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
return div;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2249
2904
|
// ---------------------------------------------------------------------------
|
|
2250
2905
|
// Version history panel
|
|
2251
2906
|
// ---------------------------------------------------------------------------
|
|
@@ -3137,6 +3792,13 @@ sendBtn.addEventListener("click", () => {
|
|
|
3137
3792
|
sendMessage(inputEl.value);
|
|
3138
3793
|
});
|
|
3139
3794
|
|
|
3795
|
+
// One-shot button — build the whole page, skipping the design checkpoint.
|
|
3796
|
+
if (oneShotBtn) {
|
|
3797
|
+
oneShotBtn.addEventListener("click", () => {
|
|
3798
|
+
sendMessage(inputEl.value, { oneShot: true });
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3140
3802
|
// Enter to send (Shift+Enter for newline)
|
|
3141
3803
|
inputEl.addEventListener("keydown", (e) => {
|
|
3142
3804
|
if (e.key === "Enter" && !e.shiftKey) {
|