vibespot 1.5.1 → 1.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibespot",
3
- "version": "1.5.1",
3
+ "version": "1.6.2",
4
4
  "description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,11 @@
19
19
  "build": "tsup",
20
20
  "test": "vitest run",
21
21
  "test:watch": "vitest",
22
+ "eval": "tsx test/eval/run-eval.ts",
23
+ "benchmark": "tsx test/eval/benchmark.ts",
24
+ "prompts:pull": "tsx scripts/sync-prompts.ts",
25
+ "prompts:seed": "tsx scripts/sync-prompts.ts --from-local",
26
+ "prompts:push": "tsx scripts/sync-prompts.ts --push",
22
27
  "prepublishOnly": "npm run build",
23
28
  "contact-monitor": "tsx scripts/contact-monitor.ts",
24
29
  "docker:publish": "scripts/docker-publish.sh",
@@ -26,7 +31,7 @@
26
31
  "docker:smoke": "scripts/docker-publish.sh --smoke --dry-run"
27
32
  },
28
33
  "dependencies": {
29
- "@anthropic-ai/sdk": "^0.96.0",
34
+ "@anthropic-ai/sdk": "^0.99.0",
30
35
  "@clack/prompts": "^1.3.0",
31
36
  "@codemirror/lang-css": "^6.3.1",
32
37
  "@codemirror/lang-html": "^6.4.11",
@@ -38,6 +43,7 @@
38
43
  "codemirror": "^6.0.2",
39
44
  "commander": "^14.0.3",
40
45
  "execa": "^9.6.1",
46
+ "jszip": "^3.10.1",
41
47
  "mammoth": "^1.11.0",
42
48
  "marked": "^18.0.2",
43
49
  "pdf-parse": "^2.4.5",
package/ui/chat.js CHANGED
@@ -29,6 +29,10 @@ let changedModulesInRun = new Set();
29
29
  let highlightOnNextModulesUpdated = false;
30
30
  let changedListClearTimer = null;
31
31
 
32
+ // VIB-1770 — the assistant bubble of the just-finished generation, so the
33
+ // trailing `generation_cost` event can append its estimated cost line to it.
34
+ let lastGenerationBubbleEl = null;
35
+
32
36
  const messagesEl = document.getElementById("chat-messages");
33
37
  const inputEl = document.getElementById("chat-input");
34
38
  const sendBtn = document.getElementById("chat-send");
@@ -605,6 +609,9 @@ function handleWsMessage(msg) {
605
609
  if (chatHeaderTitle) chatHeaderTitle.textContent = msg.themeName || "Chat";
606
610
  if (chatHeaderContext) chatHeaderContext.textContent = msg.engine || "";
607
611
 
612
+ // Hydrate the per-project generation cost chip (VIB-1770)
613
+ updateProjectCostChip(msg.costTotal);
614
+
608
615
  // Restore chat history from server
609
616
  if (msg.messages && msg.messages.length > 0) {
610
617
  for (const m of msg.messages) {
@@ -744,6 +751,9 @@ function handleWsMessage(msg) {
744
751
  case "pipeline_partial":
745
752
  handlePipelinePartial(msg);
746
753
  break;
754
+ case "generation_cost":
755
+ handleGenerationCost(msg);
756
+ break;
747
757
  case "agentic_prompt":
748
758
  handleAgenticPrompt();
749
759
  break;
@@ -1270,6 +1280,10 @@ function handlePipelineComplete(msg) {
1270
1280
  }
1271
1281
  bubble.appendChild(stats);
1272
1282
 
1283
+ // Remember this bubble so the trailing `generation_cost` event (sent right
1284
+ // after completion) can append its cost line to the same message (VIB-1770).
1285
+ lastGenerationBubbleEl = bubble;
1286
+
1273
1287
  resetPipelineState();
1274
1288
  }
1275
1289
 
@@ -1298,6 +1312,8 @@ function handlePipelinePartial(msg) {
1298
1312
  stats.textContent = `${msg.succeeded.length} modules succeeded, ${msg.failed.length} failed in ${duration}`;
1299
1313
  bubble.appendChild(stats);
1300
1314
 
1315
+ lastGenerationBubbleEl = bubble;
1316
+
1301
1317
  resetPipelineState();
1302
1318
  }
1303
1319
 
@@ -1312,6 +1328,89 @@ function resetPipelineState() {
1312
1328
  renderChatSuggestions();
1313
1329
  }
1314
1330
 
1331
+ // ---------------------------------------------------------------------------
1332
+ // Generation cost (VIB-1770) — per-page estimate + per-project running total
1333
+ // ---------------------------------------------------------------------------
1334
+
1335
+ // Format an estimated USD amount. Sub-dollar costs show 4 decimals so a
1336
+ // fraction of a cent is still legible; larger amounts round to cents.
1337
+ function formatUsd(usd) {
1338
+ const n = Number(usd) || 0;
1339
+ if (n > 0 && n < 0.01) return "$" + n.toFixed(4);
1340
+ if (n < 1) return "$" + n.toFixed(3);
1341
+ return "$" + n.toFixed(2);
1342
+ }
1343
+
1344
+ function formatTokenCount(tokens) {
1345
+ const n = Number(tokens) || 0;
1346
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
1347
+ if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
1348
+ return String(n);
1349
+ }
1350
+
1351
+ // Build the per-page cost summary text from a PageCost object.
1352
+ // `costComplete === false` means some model wasn't in the price table, so the
1353
+ // dollar figure is a lower bound — prefix "≥" and explain it via the title.
1354
+ function buildCostText(cost) {
1355
+ if (!cost || !cost.calls) return null;
1356
+ const tokens = formatTokenCount(cost.totalTokens) + " tokens";
1357
+ if (cost.costUsd > 0 || cost.costComplete) {
1358
+ const prefix = cost.costComplete ? "" : "≥ ";
1359
+ return "Est. " + prefix + formatUsd(cost.costUsd) + " · " + tokens;
1360
+ }
1361
+ // No priced calls at all — show tokens only.
1362
+ return tokens;
1363
+ }
1364
+
1365
+ // Append (or replace) the cost line on a finished generation's bubble.
1366
+ function renderCostLineOnBubble(bubble, cost) {
1367
+ if (!bubble) return;
1368
+ const text = buildCostText(cost);
1369
+ if (!text) return;
1370
+ let line = bubble.querySelector(".pipeline-cost");
1371
+ if (!line) {
1372
+ line = document.createElement("div");
1373
+ line.className = "pipeline-cost";
1374
+ bubble.appendChild(line);
1375
+ }
1376
+ line.textContent = text;
1377
+ if (cost.costComplete === false) {
1378
+ line.title = "Some model calls had no price data — this is a lower-bound estimate.";
1379
+ } else {
1380
+ line.title = "Estimated cost of this generation. Local estimate from public model prices; not a bill.";
1381
+ }
1382
+ }
1383
+
1384
+ // Live `generation_cost` event — arrives right after pipeline completion.
1385
+ function handleGenerationCost(msg) {
1386
+ if (msg.cost) renderCostLineOnBubble(lastGenerationBubbleEl, msg.cost);
1387
+ lastGenerationBubbleEl = null;
1388
+ updateProjectCostChip(msg.projectTotal);
1389
+ }
1390
+
1391
+ // Update the persistent per-project cost chip in the chat header. Hidden when
1392
+ // there's no cost yet (new project / CLI engine).
1393
+ function updateProjectCostChip(total) {
1394
+ const chip = document.getElementById("chat-cost-total");
1395
+ if (!chip) return;
1396
+ if (!total || !total.generations) {
1397
+ chip.classList.add("hidden");
1398
+ chip.textContent = "";
1399
+ return;
1400
+ }
1401
+ const prefix = total.costComplete === false ? "≥ " : "";
1402
+ chip.textContent = "Σ " + prefix + formatUsd(total.costUsd);
1403
+ chip.title =
1404
+ "Estimated total for this project: " +
1405
+ formatUsd(total.costUsd) +
1406
+ " over " +
1407
+ total.generations +
1408
+ " generation" + (total.generations === 1 ? "" : "s") +
1409
+ " · " + formatTokenCount(total.totalTokens) + " tokens" +
1410
+ (total.costComplete === false ? " (lower bound)" : "");
1411
+ chip.classList.remove("hidden");
1412
+ }
1413
+
1315
1414
  // ---------------------------------------------------------------------------
1316
1415
  // Smart chat suggestions — contextual next-step chips after generation
1317
1416
  // ---------------------------------------------------------------------------
@@ -2110,6 +2209,12 @@ function appendRestoredAssistantMessage(text, timestamp, pipeline) {
2110
2209
  }
2111
2210
  const statsClass = pipeline.stats.modulesFailed > 0 ? "pipeline-stats pipeline-stats--partial" : "pipeline-stats";
2112
2211
 
2212
+ // Per-page cost line, if this generation persisted one (VIB-1770)
2213
+ const costText = buildCostText(pipeline.cost);
2214
+ const costHtml = costText
2215
+ ? `<div class="pipeline-cost">${escapeHtml(costText)}</div>`
2216
+ : "";
2217
+
2113
2218
  div.innerHTML = `
2114
2219
  <div class="chat-msg__avatar chat-msg__avatar--ai">AI</div>
2115
2220
  <div class="chat-msg__content">
@@ -2119,6 +2224,7 @@ function appendRestoredAssistantMessage(text, timestamp, pipeline) {
2119
2224
  ${decisionHtml}
2120
2225
  ${modulesHtml}
2121
2226
  <div class="${statsClass}">${statsText}</div>
2227
+ ${costHtml}
2122
2228
  </div>
2123
2229
  </div>`;
2124
2230
  } else {
@@ -2597,6 +2703,10 @@ function updateModuleList(moduleNames) {
2597
2703
  const pageTreeCountEl = document.getElementById("page-tree-module-count");
2598
2704
  if (pageTreeCountEl) pageTreeCountEl.textContent = moduleNames.length;
2599
2705
 
2706
+ // Download is only meaningful once the theme has at least one module.
2707
+ const downloadBtn = document.getElementById("btn-download");
2708
+ if (downloadBtn) downloadBtn.disabled = moduleNames.length === 0;
2709
+
2600
2710
  // Preserve which items were marked as recently changed across re-renders so
2601
2711
  // the dots survive the per-module `modules_updated` events that happen
2602
2712
  // mid-run.
@@ -3138,6 +3248,19 @@ document.getElementById("btn-upload").addEventListener("click", () => {
3138
3248
  }
3139
3249
  });
3140
3250
 
3251
+ // Download button — fetches the HubSpot-ready .zip of the active theme.
3252
+ // The server streams it as an attachment; an anchor lets the browser save it.
3253
+ document.getElementById("btn-download")?.addEventListener("click", () => {
3254
+ const btn = document.getElementById("btn-download");
3255
+ if (!btn || btn.disabled) return;
3256
+ const a = document.createElement("a");
3257
+ a.href = "/api/download-zip";
3258
+ a.download = "";
3259
+ document.body.appendChild(a);
3260
+ a.click();
3261
+ a.remove();
3262
+ });
3263
+
3141
3264
  // Resize handle
3142
3265
  const resizeHandle = document.getElementById("resize-handle");
3143
3266
  const panelLeft = document.getElementById("panel-left");
@@ -104,6 +104,9 @@
104
104
  <a class="doc-nav__link doc-nav__link--sub" href="#field-editor">Field Editor</a>
105
105
  <a class="doc-nav__link doc-nav__link--sub" href="#brand-kit">Brand Kit</a>
106
106
  <a class="doc-nav__link doc-nav__link--sub" href="#design-system">Design System</a>
107
+ <a class="doc-nav__link" href="#observability">Observability &amp; Cost</a>
108
+ <a class="doc-nav__link doc-nav__link--sub" href="#cost-tracking">Per-Page Cost</a>
109
+ <a class="doc-nav__link doc-nav__link--sub" href="#langfuse">Langfuse Tracing</a>
107
110
  </div>
108
111
 
109
112
  <div class="doc-nav__section">
@@ -361,6 +364,10 @@ vibespot</code></pre>
361
364
  <tr><td><code>enabledCLITools</code></td><td>string[]</td><td>Array of enabled CLI tool IDs: <code>["claude-code", "gemini-cli", "codex-cli"]</code>. Only enabled tools are checked on startup.</td></tr>
362
365
  <tr><td><code>agenticMode</code></td><td>boolean</td><td>Enable the 4-stage agentic pipeline. <code>undefined</code> on first run (vibeSpot prompts you to choose), then <code>true</code> or <code>false</code>.</td></tr>
363
366
  <tr><td><code>agenticConcurrency</code></td><td>number</td><td>Max parallel module generation calls (default: <code>20</code>)</td></tr>
367
+ <tr><td><code>langfuseEnabled</code></td><td>boolean</td><td>Opt in to <a href="#langfuse">Langfuse tracing</a>. Off by default; required even when keys are set.</td></tr>
368
+ <tr><td><code>langfusePublicKey</code></td><td>string</td><td>Langfuse public key (<code>pk-lf-</code>)</td></tr>
369
+ <tr><td><code>langfuseSecretKey</code></td><td>string</td><td>Langfuse secret key (<code>sk-lf-</code>)</td></tr>
370
+ <tr><td><code>langfuseBaseUrl</code></td><td>string</td><td>Langfuse ingestion endpoint (default: <code>https://cloud.langfuse.com</code>)</td></tr>
364
371
  </tbody>
365
372
  </table>
366
373
 
@@ -383,6 +390,10 @@ vibespot</code></pre>
383
390
  <tr><td><code>VIBESPOT_AGENTIC_MODE</code></td><td>Set <code>true</code> to enable the multi-stage agentic pipeline</td></tr>
384
391
  <tr><td><code>VIBESPOT_PORT</code></td><td>Override the HTTP server port (default: <code>4200</code>)</td></tr>
385
392
  <tr><td><code>VIBESPOT_NO_OPEN</code></td><td>Set <code>1</code> to suppress auto-opening the browser on startup</td></tr>
393
+ <tr><td><code>LANGFUSE_ENABLED</code></td><td>Set <code>true</code> to send <a href="#langfuse">Langfuse traces</a>. Required &mdash; keys alone don't enable it.</td></tr>
394
+ <tr><td><code>LANGFUSE_PUBLIC_KEY</code></td><td>Langfuse public key (<code>pk-lf-</code>)</td></tr>
395
+ <tr><td><code>LANGFUSE_SECRET_KEY</code></td><td>Langfuse secret key (<code>sk-lf-</code>)</td></tr>
396
+ <tr><td><code>LANGFUSE_BASE_URL</code></td><td>Langfuse ingestion endpoint (default: <code>https://cloud.langfuse.com</code>)</td></tr>
386
397
  </tbody>
387
398
  </table>
388
399
  </div>
@@ -1171,6 +1182,95 @@ testimonial carousel, and a minimal footer with social links.</code></pre>
1171
1182
  </ul>
1172
1183
  </div>
1173
1184
 
1185
+ <!-- ============================================================
1186
+ Section 6b: Observability & Cost
1187
+ ============================================================ -->
1188
+ <div class="doc-section" id="observability">
1189
+ <h2 id="observability-heading">Observability &amp; Cost <a href="#observability" class="doc-anchor">#</a></h2>
1190
+ <p>Every AI generation spends tokens, which cost money and take time. vibeSpot surfaces both. The per-page cost line is always on and needs no setup. For deeper inspection &mdash; full prompts, per-stage breakdowns, latency, and history across runs &mdash; you can opt in to <a href="https://langfuse.com" target="_blank">Langfuse</a> tracing.</p>
1191
+
1192
+ <h3 id="cost-tracking">Per-Page Cost</h3>
1193
+ <p>After each generation, the chat shows what that page cost. There is nothing to configure &mdash; it works with no keys and no Langfuse.</p>
1194
+ <ul>
1195
+ <li><strong>Per-message cost</strong> &mdash; a small line under each assistant reply showing the estimated USD for that generation, summed across every model call the pipeline made (intent analysis, design system, module planning, and each module).</li>
1196
+ <li><strong>Per-project total</strong> &mdash; a running total chip in the chat header that accumulates across every generation in the theme.</li>
1197
+ </ul>
1198
+ <p>Costs are estimates from an approximate per-model price table, not your provider invoice. When a call uses a model vibeSpot can't price, the total is shown as a lower bound (<code>&ge;</code>). CLI engines (Claude Code, Gemini CLI, Codex CLI) don't report token usage, so no cost is shown for them. This is display-only &mdash; no billing, credits, or quotas.</p>
1199
+
1200
+ <h3 id="langfuse">Langfuse Tracing</h3>
1201
+ <p>Langfuse is an LLM observability platform. With it connected, vibeSpot sends a trace for every generation so you can inspect the exact input and output of each model call, token usage, estimated cost, and latency &mdash; and compare runs over time. It is <strong>opt-in and off by default</strong>.</p>
1202
+ <p>vibeSpot runs locally, not on a backend we operate, so traces go straight from your machine to your Langfuse instance &mdash; Langfuse Cloud or your own self-hosted install. The integration is a small, dependency-free client; it never pulls in a heavy SDK or adds startup overhead when it's off.</p>
1203
+
1204
+ <h4 id="langfuse-model">How a trace is structured</h4>
1205
+ <p>vibeSpot maps the pipeline onto Langfuse's nesting so cost and latency roll up at every level:</p>
1206
+ <ul>
1207
+ <li><strong>Trace</strong> &mdash; one user action (a page generation, a Figma conversion, a brand extraction). The Langfuse <em>session</em> is set to the theme name, so every trace for a project groups together.</li>
1208
+ <li><strong>Span</strong> &mdash; one pipeline stage: intent analysis, design system, module planning, and module development.</li>
1209
+ <li><strong>Generation</strong> &mdash; one model call. The N parallel module calls (plus any retries) nest under the single module-development span.</li>
1210
+ </ul>
1211
+ <p>A full page reads as <code>trace &rarr; stage spans &rarr; generations</code>, with token cost summing up the tree.</p>
1212
+
1213
+ <h4 id="langfuse-instance">Get a Langfuse instance</h4>
1214
+ <p>You need somewhere to send the traces. Pick one: Langfuse's hosted cloud (fastest to start) or your own self-hosted install (your data stays on your infrastructure). Either way you end up with a <strong>public key</strong>, a <strong>secret key</strong>, and a <strong>host URL</strong> &mdash; the three things vibeSpot needs in the next step.</p>
1215
+
1216
+ <p><strong>Option A &mdash; Langfuse Cloud</strong></p>
1217
+ <ol class="doc-steps">
1218
+ <li><strong>Sign up.</strong> Create a free account at <a href="https://cloud.langfuse.com" target="_blank">cloud.langfuse.com</a> (EU region) or <a href="https://us.cloud.langfuse.com" target="_blank">us.cloud.langfuse.com</a> (US region).</li>
1219
+ <li><strong>Create an organization and a project.</strong> Langfuse walks you through this on first login. Traces are scoped to a project.</li>
1220
+ <li><strong>Create API keys.</strong> In the project, open Settings &rarr; API Keys &rarr; <strong>Create new API keys</strong>. Copy the public key (<code>pk-lf-...</code>) and secret key (<code>sk-lf-...</code>) &mdash; the secret is shown only once.</li>
1221
+ <li><strong>Note your host.</strong> It's <code>https://cloud.langfuse.com</code> (EU) or <code>https://us.cloud.langfuse.com</code> (US). You'll use this as the base URL.</li>
1222
+ </ol>
1223
+
1224
+ <p><strong>Option B &mdash; Self-hosted (local)</strong></p>
1225
+ <p>Langfuse ships a Docker Compose stack that runs the whole thing on your machine. With <a href="https://docs.docker.com/get-docker/" target="_blank">Docker</a> installed:</p>
1226
+ <pre><code>git clone https://github.com/langfuse/langfuse.git
1227
+ cd langfuse
1228
+ docker compose up</code></pre>
1229
+ <ol class="doc-steps">
1230
+ <li><strong>Open the UI.</strong> Once the containers are healthy, go to <code>http://localhost:3000</code>. Create an account &mdash; the first user becomes the owner.</li>
1231
+ <li><strong>Create an organization and a project</strong>, same as cloud.</li>
1232
+ <li><strong>Create API keys</strong> under Settings &rarr; API Keys, and copy both.</li>
1233
+ <li><strong>Your host is</strong> <code>http://localhost:3000</code> &mdash; use that as the base URL.</li>
1234
+ </ol>
1235
+ <div class="doc-callout doc-callout--info">
1236
+ <div class="doc-callout__label">&#8505; Local vs. production self-host</div>
1237
+ <p>The Docker Compose stack is meant for local use and evaluation. For a persistent, production self-host (managed Postgres, ClickHouse, object storage, scaling), follow the <a href="https://langfuse.com/self-hosting" target="_blank">Langfuse self-hosting guide</a>. vibeSpot only needs the host URL and a key pair &mdash; it doesn't care how Langfuse is deployed.</p>
1238
+ </div>
1239
+
1240
+ <h4 id="langfuse-setup">Connect vibeSpot</h4>
1241
+ <p>With your keys and host URL from the step above:</p>
1242
+ <ol class="doc-steps">
1243
+ <li><strong>Add the keys in vibeSpot.</strong> Open Settings &rarr; AI tab &rarr; <strong>Observability</strong>. Paste the public and secret keys. They are stored locally in <code>~/.vibespot/config.json</code> and sent only to your Langfuse instance.</li>
1244
+ <li><strong>Set the base URL.</strong> Leave it blank for EU cloud (<code>https://cloud.langfuse.com</code>). Use <code>https://us.cloud.langfuse.com</code> for US cloud, or <code>http://localhost:3000</code> (or your own URL) for a self-hosted install.</li>
1245
+ <li><strong>Turn it on.</strong> Flip the <strong>Send traces to Langfuse</strong> toggle. Generate a page, then check your Langfuse project &mdash; the trace appears under a session named after your theme.</li>
1246
+ </ol>
1247
+ <div class="doc-callout doc-callout--info">
1248
+ <div class="doc-callout__label">&#8505; Keys alone don't enable it</div>
1249
+ <p>Tracing requires <strong>both</strong> an explicit opt-in (the toggle, <code>langfuseEnabled: true</code>, or <code>LANGFUSE_ENABLED=true</code>) <strong>and</strong> both keys set. A stray <code>LANGFUSE_*</code> variable in your environment never sends traces on its own.</p>
1250
+ </div>
1251
+
1252
+ <h4 id="langfuse-config">Config &amp; environment</h4>
1253
+ <p>Instead of the UI, you can configure tracing in <code>~/.vibespot/config.json</code> or via environment variables:</p>
1254
+ <table>
1255
+ <thead>
1256
+ <tr><th>Config field</th><th>Environment variable</th><th>Description</th></tr>
1257
+ </thead>
1258
+ <tbody>
1259
+ <tr><td><code>langfuseEnabled</code></td><td><code>LANGFUSE_ENABLED</code></td><td>Set to <code>true</code> to send traces. Required &mdash; keys alone don't enable it.</td></tr>
1260
+ <tr><td><code>langfusePublicKey</code></td><td><code>LANGFUSE_PUBLIC_KEY</code></td><td>Langfuse public key (<code>pk-lf-...</code>)</td></tr>
1261
+ <tr><td><code>langfuseSecretKey</code></td><td><code>LANGFUSE_SECRET_KEY</code></td><td>Langfuse secret key (<code>sk-lf-...</code>)</td></tr>
1262
+ <tr><td><code>langfuseBaseUrl</code></td><td><code>LANGFUSE_BASE_URL</code></td><td>Ingestion endpoint (default <code>https://cloud.langfuse.com</code>)</td></tr>
1263
+ </tbody>
1264
+ </table>
1265
+
1266
+ <h4 id="langfuse-privacy">What's sent, and what isn't</h4>
1267
+ <ul>
1268
+ <li><strong>Only API engines produce traces.</strong> Anthropic, OpenAI, Gemini, and Langdock report token usage. CLI engines (Claude Code, Gemini CLI, Codex CLI) don't, so they generate no traces or cost.</li>
1269
+ <li><strong>Prompts and outputs are truncated</strong> before sending, so a single trace never ships hundreds of kilobytes of content.</li>
1270
+ <li><strong>It never blocks generation.</strong> All network and serialization errors are swallowed &mdash; a Langfuse outage slows nothing and fails nothing. When tracing is off, there are no network calls at all.</li>
1271
+ </ul>
1272
+ </div>
1273
+
1174
1274
  <!-- ============================================================
1175
1275
  Section 7: Deploying to HubSpot
1176
1276
  ============================================================ -->
@@ -1288,7 +1388,7 @@ testimonial carousel, and a minimal footer with social links.</code></pre>
1288
1388
  <div class="doc-section" id="zip-download">
1289
1389
  <h2 id="zip-download-heading">ZIP Download <a href="#zip-download" class="doc-anchor">#</a></h2>
1290
1390
  <p>Export your theme as a ZIP archive for offline use, sharing, or manual upload to HubSpot.</p>
1291
- <p>Click the <strong>Download ZIP</strong> button on the dashboard toolbar (next to the theme name) to generate and download a <code>.zip</code> file containing all theme files: modules, templates, shared CSS/JS, and assets. The ZIP file is named after the theme (e.g., <code>my-saas-landing.zip</code>).</p>
1391
+ <p>Click the <strong>Download</strong> button in the editor topbar (next to <strong>Deploy</strong>) to generate and download a <code>.zip</code> file containing all theme files: modules, templates, shared CSS/JS, and assets. The archive is HubSpot-ready — import it directly into Design Manager. The ZIP file is named after the theme (e.g., <code>my-saas-landing.zip</code>).</p>
1292
1392
  <div class="doc-callout doc-callout--tip">
1293
1393
  <div class="doc-callout__label">&#10024; When to use ZIP download</div>
1294
1394
  <p>ZIP download is useful when you want to share the theme with a colleague who does not have vibeSpot, back up the theme independently of git, or upload manually through HubSpot's Design Manager.</p>
@@ -1584,6 +1684,7 @@ docker cp vibespot:/home/vibespot/vibespot-themes ./backup-themes</code></pre>
1584
1684
  <li><strong>API keys section</strong> &mdash; Input fields for Anthropic, OpenAI, and Google AI API keys. Keys are displayed masked (only last 4 characters visible) after saving.</li>
1585
1685
  <li><strong>Claude OAuth</strong> &mdash; A section to paste the OAuth token generated by <code>claude setup-token</code>. Shows the token status (valid/expired).</li>
1586
1686
  <li><strong>CLI tools</strong> &mdash; Toggle switches for Claude Code, Gemini CLI, and Codex CLI. CLI tools must be toggled <strong>on</strong> before they appear as selectable engines in the Engine section above. This lazy-detection approach means disabled tools add zero startup overhead. Once toggled on, vibeSpot checks if the CLI is installed and authenticated. If not installed, an install button appears. If installed but not authenticated, an auth button appears.</li>
1687
+ <li><strong>Observability section</strong> &mdash; Opt-in <a href="#langfuse">Langfuse tracing</a>. Paste your public and secret keys, set a base URL if you self-host, and flip the toggle to send a trace for every model call. Off by default. See <a href="#observability">Observability &amp; Cost</a>.</li>
1587
1688
  </ul>
1588
1689
 
1589
1690
  <div class="doc-callout doc-callout--tip">
package/ui/index.html CHANGED
@@ -161,6 +161,10 @@
161
161
  <button class="topbar__icon-btn topbar__mode topbar__mode--editor hidden" id="btn-email-preview" type="button" title="Email client preview (Gmail, Outlook, Apple Mail)" aria-label="Email client preview">
162
162
  <svg class="icon icon--md" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="22 4 12 13 2 4"/></svg>
163
163
  </button>
164
+ <button class="btn btn--secondary topbar__mode topbar__mode--editor" id="btn-download" type="button" title="Download theme as a HubSpot-ready .zip" disabled>
165
+ <svg class="icon icon--sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
166
+ Download
167
+ </button>
164
168
  <button class="btn btn--primary topbar__mode topbar__mode--editor" id="btn-upload" type="button" title="Deploy theme to HubSpot">
165
169
  <svg class="icon icon--sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>
166
170
  Deploy
@@ -484,6 +488,7 @@
484
488
  <div class="chat__header" id="chat-header">
485
489
  <span class="chat__header-title" id="chat-header-title">Chat</span>
486
490
  <span class="chat__header-context" id="chat-header-context"></span>
491
+ <span class="chat__cost-total hidden" id="chat-cost-total" title="Estimated total generation cost for this project"></span>
487
492
  </div>
488
493
  <div class="chat__messages" id="chat-messages" role="log" aria-live="polite">
489
494
  <div class="chat__welcome" id="chat-welcome">