vibespot 1.0.8 → 1.1.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/ui/settings.js CHANGED
@@ -24,8 +24,13 @@ const ENGINE_LABELS = {
24
24
  // Open / Close
25
25
  // ---------------------------------------------------------------------------
26
26
 
27
- function openSettings() {
27
+ function openSettings(tab) {
28
28
  if (typeof closeMenu === "function") closeMenu();
29
+ if (tab) {
30
+ activeTab = tab;
31
+ const tabs = document.querySelectorAll("#settings-tabs .settings__tab");
32
+ tabs.forEach((t) => t.classList.toggle("active", t.dataset.tab === tab));
33
+ }
29
34
  document.getElementById("settings-overlay").classList.remove("hidden");
30
35
  refreshSettings();
31
36
  }
@@ -78,6 +83,7 @@ function renderSettings(data) {
78
83
  switch (activeTab) {
79
84
  case "ai": renderAITab(body, data); break;
80
85
  case "hubspot": renderHubSpotTab(body, data); break;
86
+ case "figma": renderFigmaTab(body, data); break;
81
87
  case "github": renderGitHubTab(body, data); break;
82
88
  case "vibespot": renderVibeSpotTab(body, data); break;
83
89
  }
@@ -209,6 +215,11 @@ function renderAITab(body, data) {
209
215
  agenticSection.appendChild(toggleRow);
210
216
  body.appendChild(agenticSection);
211
217
 
218
+ // AI Capabilities section — exposes Anthropic SDK features (extended
219
+ // thinking) and shows the status of features that auto-engage based on
220
+ // engine (prompt caching).
221
+ body.appendChild(renderAICapabilitiesSection(activeEngine, config));
222
+
212
223
  // API Keys section
213
224
  const keysSection = el("section", "settings__section");
214
225
  keysSection.appendChild(sectionTitle("API Keys"));
@@ -379,6 +390,222 @@ function renderAITab(body, data) {
379
390
  body.appendChild(cliSection);
380
391
  }
381
392
 
393
+ // ---------------------------------------------------------------------------
394
+ // AI Capabilities — feature toggles + capability status per engine
395
+ // ---------------------------------------------------------------------------
396
+
397
+ function renderAICapabilitiesSection(activeEngine, config) {
398
+ const section = el("section", "settings__section");
399
+ section.appendChild(sectionTitle("AI Capabilities"));
400
+ section.appendChild(desc("Advanced model features. Some are configurable directly; some auto-engage based on the active engine."));
401
+
402
+ // Engine classification — different engines have different feature surfaces.
403
+ const isAnthropicAPI = activeEngine === "anthropic-api" || activeEngine === "claude-oauth";
404
+ const isClaudeCode = activeEngine === "claude-code";
405
+ const isAnthropicAny = isAnthropicAPI || isClaudeCode;
406
+
407
+ // ---- Prompt Caching (auto on Anthropic engines — status indicator only) ----
408
+ section.appendChild(capabilityRow({
409
+ label: "Prompt Caching",
410
+ description: isClaudeCode
411
+ ? "Claude Code manages prompt caching internally — automatic, no configuration needed."
412
+ : "System prompts and tool schemas cached on Anthropic. Automatic — no setup needed.",
413
+ status: isAnthropicAny ? "active" : "n/a",
414
+ statusText: isAnthropicAPI ? "Active" : isClaudeCode ? "Auto (CLI-managed)" : "Anthropic only",
415
+ }));
416
+
417
+ // ---- Extended Thinking (toggle + budget) ----
418
+ const thinkingActive = !!config.extendedThinking && isAnthropicAPI;
419
+ const thinkingRow = capabilityRow({
420
+ label: "Extended Thinking",
421
+ description: isClaudeCode
422
+ ? "Claude Code uses thinking automatically when the model supports it. Budget can't be tuned via the CLI."
423
+ : "The model deliberates internally before responding. Higher quality on the Page Architect stage; slower and slightly more expensive.",
424
+ status: isAnthropicAPI ? (thinkingActive ? "active" : "off") : isClaudeCode ? "active" : "n/a",
425
+ statusText: isAnthropicAPI
426
+ ? thinkingActive ? "On" : "Off"
427
+ : isClaudeCode ? "Auto (CLI-managed)" : "Anthropic only",
428
+ toggle: isAnthropicAPI
429
+ ? {
430
+ active: thinkingActive,
431
+ onChange: async (val) => {
432
+ await fetch("/api/settings", {
433
+ method: "POST",
434
+ headers: { "Content-Type": "application/json" },
435
+ body: JSON.stringify({ extendedThinking: val }),
436
+ });
437
+ refreshSettings();
438
+ },
439
+ }
440
+ : null,
441
+ });
442
+ section.appendChild(thinkingRow);
443
+
444
+ // Budget selector — only meaningful when thinking is enabled
445
+ if (isAnthropicAPI && thinkingActive) {
446
+ const budgetRow = el("div", "settings__capability-sub");
447
+ const budgetLabel = el("span", "settings__capability-sub-label");
448
+ budgetLabel.textContent = "Budget";
449
+ budgetRow.appendChild(budgetLabel);
450
+
451
+ const budgetSelect = el("select", "settings__capability-select");
452
+ const budgets = [
453
+ { id: "low", label: "Low (~4k tokens)" },
454
+ { id: "medium", label: "Medium (~16k tokens)" },
455
+ { id: "high", label: "High (~32k tokens)" },
456
+ ];
457
+ for (const b of budgets) {
458
+ const opt = document.createElement("option");
459
+ opt.value = b.id;
460
+ opt.textContent = b.label;
461
+ if ((config.extendedThinkingBudget || "medium") === b.id) opt.selected = true;
462
+ budgetSelect.appendChild(opt);
463
+ }
464
+ budgetSelect.addEventListener("change", async () => {
465
+ await fetch("/api/settings", {
466
+ method: "POST",
467
+ headers: { "Content-Type": "application/json" },
468
+ body: JSON.stringify({ extendedThinkingBudget: budgetSelect.value }),
469
+ });
470
+ refreshSettings();
471
+ });
472
+ budgetRow.appendChild(budgetSelect);
473
+
474
+ const hint = el("span", "settings__capability-sub-hint");
475
+ hint.textContent = "Higher = more deliberation, more cost";
476
+ budgetRow.appendChild(hint);
477
+
478
+ section.appendChild(budgetRow);
479
+ }
480
+
481
+ // ---- Web Search (toggle on Anthropic API + Claude Code CLI) ----
482
+ const webSearchSupported = isAnthropicAny;
483
+ const webSearchActive = !!config.webSearch && webSearchSupported;
484
+ section.appendChild(capabilityRow({
485
+ label: "Web Search",
486
+ description: isClaudeCode
487
+ ? "Allow Claude Code to search the web (passes --allowed-tools WebSearch). Adds cost and may surface irrelevant results."
488
+ : "Let the AI search the web for context (competitor pages, industry references) during planning. Adds cost and may surface irrelevant results.",
489
+ status: !webSearchSupported ? "n/a" : webSearchActive ? "active" : "off",
490
+ statusText: !webSearchSupported
491
+ ? "Anthropic / Claude Code only"
492
+ : webSearchActive ? "On" : "Off",
493
+ toggle: webSearchSupported
494
+ ? {
495
+ active: webSearchActive,
496
+ onChange: async (val) => {
497
+ await fetch("/api/settings", {
498
+ method: "POST",
499
+ headers: { "Content-Type": "application/json" },
500
+ body: JSON.stringify({ webSearch: val }),
501
+ });
502
+ refreshSettings();
503
+ },
504
+ }
505
+ : null,
506
+ }));
507
+
508
+ // ---- Citations (auto-on for documents — status only) ----
509
+ section.appendChild(capabilityRow({
510
+ label: "Citations",
511
+ description: "When you upload PDFs/docs, the model can cite specific passages it referenced.",
512
+ status: "soon",
513
+ statusText: "Coming soon",
514
+ }));
515
+
516
+ return section;
517
+ }
518
+
519
+ function capabilityRow({ label, description, status, statusText, toggle }) {
520
+ const row = el("div", "settings__capability-row");
521
+
522
+ const labelWrap = el("div", "settings__capability-label-wrap");
523
+ const labelEl = el("div", "settings__capability-label");
524
+ labelEl.textContent = label;
525
+ const badge = el("span", "settings__capability-badge settings__capability-badge--" + status);
526
+ badge.textContent = statusText;
527
+ labelEl.appendChild(badge);
528
+ labelWrap.appendChild(labelEl);
529
+
530
+ const descEl = el("div", "settings__capability-desc");
531
+ descEl.textContent = description;
532
+ labelWrap.appendChild(descEl);
533
+
534
+ row.appendChild(labelWrap);
535
+
536
+ if (toggle) {
537
+ const btn = el("button", "settings__toggle" + (toggle.active ? " active" : ""));
538
+ btn.addEventListener("click", () => toggle.onChange(!toggle.active));
539
+ row.appendChild(btn);
540
+ }
541
+
542
+ return row;
543
+ }
544
+
545
+ // ---------------------------------------------------------------------------
546
+ // Figma Tab
547
+ // ---------------------------------------------------------------------------
548
+
549
+ function renderFigmaTab(body, data) {
550
+ const config = data.config;
551
+
552
+ const section = el("section", "settings__section");
553
+ section.appendChild(sectionTitle("Personal Access Token"));
554
+ section.appendChild(desc("Connect your Figma account to import designs directly into HubSpot CMS modules. Tokens are stored locally and only used to call the Figma API."));
555
+
556
+ const figmaToken = config.figmaToken;
557
+ const figmaKeyInfo = {
558
+ configured: !!figmaToken,
559
+ masked: figmaToken || "",
560
+ };
561
+ section.appendChild(createApiKeyCard("figma", "Figma PAT", "figd_...", figmaKeyInfo));
562
+
563
+ // Help link + Test Connection
564
+ const actionsRow = el("div", "settings__card-row");
565
+ actionsRow.style.paddingTop = "4px";
566
+ const helpLink = el("a", "settings__btn");
567
+ helpLink.textContent = "How to get a token";
568
+ helpLink.href = "https://help.figma.com/hc/en-us/articles/8085703771159";
569
+ helpLink.target = "_blank";
570
+ helpLink.style.textDecoration = "none";
571
+ helpLink.style.fontSize = "12px";
572
+ actionsRow.appendChild(helpLink);
573
+
574
+ const testBtn = el("button", "settings__btn settings__btn--small");
575
+ testBtn.textContent = "Test Connection";
576
+ testBtn.disabled = !figmaKeyInfo.configured;
577
+ testBtn.addEventListener("click", async () => {
578
+ testBtn.disabled = true;
579
+ testBtn.textContent = "Testing...";
580
+ try {
581
+ const res = await fetch("/api/figma/test-token", {
582
+ method: "POST",
583
+ headers: { "Content-Type": "application/json" },
584
+ body: JSON.stringify({}),
585
+ });
586
+ const result = await res.json();
587
+ if (result.ok) {
588
+ testBtn.textContent = "\u2713 Connected as " + result.user;
589
+ testBtn.style.color = "var(--success)";
590
+ } else {
591
+ testBtn.textContent = "\u2717 " + (result.error || "Failed");
592
+ testBtn.style.color = "var(--warning)";
593
+ }
594
+ } catch {
595
+ testBtn.textContent = "\u2717 Connection failed";
596
+ testBtn.style.color = "var(--warning)";
597
+ }
598
+ setTimeout(() => {
599
+ testBtn.textContent = "Test Connection";
600
+ testBtn.style.color = "";
601
+ testBtn.disabled = !figmaKeyInfo.configured;
602
+ }, 3000);
603
+ });
604
+ actionsRow.appendChild(testBtn);
605
+ section.appendChild(actionsRow);
606
+ body.appendChild(section);
607
+ }
608
+
382
609
  // ---------------------------------------------------------------------------
383
610
  // HubSpot Tab
384
611
  // ---------------------------------------------------------------------------
package/ui/setup.js CHANGED
@@ -884,7 +884,7 @@ function togglePanel(action) {
884
884
  panels.forEach((p) => p.classList.add("hidden"));
885
885
  buttons.forEach((b) => b.classList.remove("active"));
886
886
 
887
- const panelMap = { new: "panel-new", continue: "panel-continue", download: "panel-download", convert: "panel-convert" };
887
+ const panelMap = { new: "panel-new", continue: "panel-continue", download: "panel-download", figma: "panel-figma", convert: "panel-convert" };
888
888
  const panel = document.getElementById(panelMap[action]);
889
889
  if (panel) {
890
890
  panel.classList.remove("hidden");
@@ -898,6 +898,7 @@ function togglePanel(action) {
898
898
  // Focus input if applicable
899
899
  if (action === "new") setTimeout(() => document.getElementById("new-theme-name")?.focus(), 50);
900
900
  if (action === "convert") setTimeout(() => document.getElementById("import-url")?.focus(), 50);
901
+ if (action === "figma") initFigmaPanel();
901
902
 
902
903
  // Load remote themes on first open
903
904
  if (action === "download" && !remoteThemesLoaded) loadDownloadPanel();
@@ -1135,6 +1136,293 @@ document.getElementById("import-url").addEventListener("keydown", (e) => {
1135
1136
  // Helpers
1136
1137
  // ---------------------------------------------------------------------------
1137
1138
 
1139
+ // ---------------------------------------------------------------------------
1140
+ // Figma import
1141
+ // ---------------------------------------------------------------------------
1142
+
1143
+ let figmaExtractionId = null;
1144
+
1145
+ async function initFigmaPanel() {
1146
+ const tokenPrompt = document.getElementById("figma-token-prompt");
1147
+ const urlSection = document.getElementById("figma-url-section");
1148
+
1149
+ // Check if token is configured
1150
+ try {
1151
+ const res = await fetch("/api/settings/status");
1152
+ const data = await res.json();
1153
+ const hasToken = !!data.config?.figmaToken;
1154
+ tokenPrompt.classList.toggle("hidden", hasToken);
1155
+ urlSection.style.opacity = hasToken ? "1" : "0.5";
1156
+ urlSection.style.pointerEvents = hasToken ? "auto" : "none";
1157
+ } catch {
1158
+ tokenPrompt.classList.remove("hidden");
1159
+ }
1160
+
1161
+ setTimeout(() => {
1162
+ const urlInput = document.getElementById("figma-url");
1163
+ if (urlInput && !urlInput.closest(".hidden")) urlInput.focus();
1164
+ }, 50);
1165
+ }
1166
+
1167
+ // Inline token save
1168
+ document.getElementById("figma-save-token")?.addEventListener("click", async () => {
1169
+ const input = document.getElementById("figma-inline-token");
1170
+ const token = input.value.trim();
1171
+ if (!token) return;
1172
+ const btn = document.getElementById("figma-save-token");
1173
+ btn.disabled = true;
1174
+ btn.textContent = "Saving...";
1175
+ try {
1176
+ await fetch("/api/settings/apikey", {
1177
+ method: "POST",
1178
+ headers: { "Content-Type": "application/json" },
1179
+ body: JSON.stringify({ provider: "figma", apiKey: token }),
1180
+ });
1181
+ input.value = "";
1182
+ btn.textContent = "Saved!";
1183
+ setTimeout(() => { btn.textContent = "Save"; btn.disabled = false; }, 1500);
1184
+ initFigmaPanel(); // refresh state
1185
+ } catch {
1186
+ btn.textContent = "Failed";
1187
+ setTimeout(() => { btn.textContent = "Save"; btn.disabled = false; }, 2000);
1188
+ }
1189
+ });
1190
+
1191
+ // Settings link in Figma panel
1192
+ document.getElementById("figma-open-settings")?.addEventListener("click", (e) => {
1193
+ e.preventDefault();
1194
+ if (typeof openSettings === "function") openSettings("figma");
1195
+ });
1196
+
1197
+ // Extract button
1198
+ document.getElementById("figma-extract-btn")?.addEventListener("click", async () => {
1199
+ const urlInput = document.getElementById("figma-url");
1200
+ const url = urlInput.value.trim();
1201
+ if (!url) return;
1202
+
1203
+ // Basic client-side validation
1204
+ if (!url.match(/figma\.com\/(design|file)\//)) {
1205
+ showError("Not a valid Figma URL. Expected: figma.com/design/...");
1206
+ return;
1207
+ }
1208
+
1209
+ const btn = document.getElementById("figma-extract-btn");
1210
+ btn.disabled = true;
1211
+ btn.textContent = "Extracting...";
1212
+ urlInput.disabled = true;
1213
+
1214
+ const progressEl = document.getElementById("figma-progress");
1215
+ progressEl.classList.remove("hidden");
1216
+ progressEl.innerHTML = `<span class="figma-progress__line">Connecting to Figma...</span>`;
1217
+
1218
+ try {
1219
+ const res = await fetch("/api/figma/extract", {
1220
+ method: "POST",
1221
+ headers: { "Content-Type": "application/json" },
1222
+ body: JSON.stringify({ url }),
1223
+ });
1224
+
1225
+ const reader = res.body.getReader();
1226
+ const decoder = new TextDecoder();
1227
+ let buffer = "";
1228
+ let result = null;
1229
+
1230
+ while (true) {
1231
+ const { done, value } = await reader.read();
1232
+ if (done) break;
1233
+ buffer += decoder.decode(value, { stream: true });
1234
+
1235
+ // Parse SSE events from buffer
1236
+ const lines = buffer.split("\n");
1237
+ buffer = lines.pop() || "";
1238
+ for (const line of lines) {
1239
+ if (!line.startsWith("data: ")) continue;
1240
+ try {
1241
+ const event = JSON.parse(line.slice(6));
1242
+ if (event.type === "progress") {
1243
+ const span = document.createElement("span");
1244
+ span.className = "figma-progress__line";
1245
+ span.textContent = event.message;
1246
+ progressEl.appendChild(span);
1247
+ progressEl.scrollTop = progressEl.scrollHeight;
1248
+ } else if (event.type === "complete") {
1249
+ result = event;
1250
+ }
1251
+ } catch { /* skip malformed lines */ }
1252
+ }
1253
+ }
1254
+
1255
+ if (!result || !result.ok) {
1256
+ showError(result?.error || "Extraction failed");
1257
+ btn.disabled = false;
1258
+ btn.textContent = "Extract";
1259
+ urlInput.disabled = false;
1260
+ return;
1261
+ }
1262
+
1263
+ figmaExtractionId = result.extractionId;
1264
+ progressEl.classList.add("hidden");
1265
+ renderFigmaSummary(result.summary);
1266
+
1267
+ // Auto-fill theme name from extraction summary
1268
+ const nameInput = document.getElementById("figma-theme-name");
1269
+ if (nameInput) {
1270
+ nameInput.value = result.summary.suggestedThemeName || result.summary.fileName
1271
+ ?.toLowerCase()
1272
+ .replace(/[^a-z0-9]+/g, "-")
1273
+ .replace(/(^-|-$)/g, "")
1274
+ .slice(0, 40) || "";
1275
+ }
1276
+
1277
+ document.getElementById("figma-generate").classList.remove("hidden");
1278
+ btn.textContent = "Extracted";
1279
+ } catch (err) {
1280
+ showError("Extraction failed: " + err.message);
1281
+ btn.disabled = false;
1282
+ btn.textContent = "Extract";
1283
+ urlInput.disabled = false;
1284
+ }
1285
+ });
1286
+
1287
+ function renderFigmaSummary(summary) {
1288
+ const container = document.getElementById("figma-summary");
1289
+ container.classList.remove("hidden");
1290
+
1291
+ let html = `<div class="figma-summary">`;
1292
+ html += `<div class="figma-summary__title">${esc(summary.fileName)}</div>`;
1293
+ html += `<div class="figma-summary__stats">`;
1294
+ html += `<span>${summary.sectionCount} section${summary.sectionCount !== 1 ? "s" : ""}</span>`;
1295
+ html += `<span>${summary.assetCount} asset${summary.assetCount !== 1 ? "s" : ""}</span>`;
1296
+ html += `<span>${summary.fontFamilies?.length || 0} font${(summary.fontFamilies?.length || 0) !== 1 ? "s" : ""}</span>`;
1297
+ html += `</div>`;
1298
+
1299
+ // Color swatches
1300
+ if (summary.colorPalette?.length) {
1301
+ html += `<div class="figma-swatches">`;
1302
+ for (const color of summary.colorPalette.slice(0, 8)) {
1303
+ html += `<span class="figma-swatch" style="background:${esc(color)}" title="${esc(color)}"></span>`;
1304
+ }
1305
+ html += `</div>`;
1306
+ }
1307
+
1308
+ // Section names
1309
+ if (summary.sectionNames?.length) {
1310
+ html += `<div class="figma-summary__sections">`;
1311
+ for (const name of summary.sectionNames) {
1312
+ html += `<span class="figma-summary__section-tag">${esc(name)}</span>`;
1313
+ }
1314
+ html += `</div>`;
1315
+ }
1316
+
1317
+ html += `</div>`;
1318
+ container.innerHTML = html;
1319
+ }
1320
+
1321
+ // Image mode toggle hint
1322
+ document.getElementById("figma-use-assets")?.addEventListener("change", (e) => {
1323
+ const hint = document.getElementById("figma-image-hint");
1324
+ if (hint) {
1325
+ hint.textContent = e.target.checked
1326
+ ? "Images uploaded to HubSpot, no manual replacement needed"
1327
+ : "Image fields with placeholders, swap in HubSpot editor";
1328
+ }
1329
+ });
1330
+
1331
+ // Generate button
1332
+ document.getElementById("figma-generate-btn")?.addEventListener("click", () => {
1333
+ const nameInput = document.getElementById("figma-theme-name");
1334
+ const themeName = nameInput.value.trim();
1335
+ if (!themeName) { nameInput.focus(); return; }
1336
+ if (!figmaExtractionId) { showError("No extraction available — extract first"); return; }
1337
+ const useAssets = document.getElementById("figma-use-assets")?.checked ?? true;
1338
+ startFigmaImport(figmaExtractionId, themeName, useAssets);
1339
+ });
1340
+
1341
+ document.getElementById("figma-theme-name")?.addEventListener("keydown", (e) => {
1342
+ if (e.key === "Enter") { e.preventDefault(); document.getElementById("figma-generate-btn")?.click(); }
1343
+ });
1344
+
1345
+ async function startFigmaImport(extractionId, themeName, useAssets = true) {
1346
+ // Disable generate button
1347
+ const genBtn = document.getElementById("figma-generate-btn");
1348
+ if (genBtn) { genBtn.disabled = true; genBtn.textContent = "Converting..."; }
1349
+
1350
+ // Show progress in the same progress element
1351
+ const progressEl = document.getElementById("figma-progress");
1352
+ progressEl.classList.remove("hidden");
1353
+ progressEl.innerHTML = `<span class="figma-progress__line">Creating theme...</span>`;
1354
+
1355
+ // 1. Create theme on server first
1356
+ try {
1357
+ const res = await fetch("/api/setup/create", {
1358
+ method: "POST",
1359
+ headers: { "Content-Type": "application/json" },
1360
+ body: JSON.stringify({ name: themeName }),
1361
+ });
1362
+ const data = await res.json();
1363
+ if (data.error) {
1364
+ showError(data.error);
1365
+ if (genBtn) { genBtn.disabled = false; genBtn.textContent = "Generate Page"; }
1366
+ return;
1367
+ }
1368
+ } catch (err) {
1369
+ showError("Failed to create theme: " + err.message);
1370
+ if (genBtn) { genBtn.disabled = false; genBtn.textContent = "Generate Page"; }
1371
+ return;
1372
+ }
1373
+
1374
+ // 2. Run pipeline via SSE — stay on setup screen
1375
+ try {
1376
+ const res = await fetch("/api/figma/generate", {
1377
+ method: "POST",
1378
+ headers: { "Content-Type": "application/json" },
1379
+ body: JSON.stringify({ extractionId, themeName, useAssets }),
1380
+ });
1381
+
1382
+ const reader = res.body.getReader();
1383
+ const decoder = new TextDecoder();
1384
+ let buffer = "";
1385
+ let result = null;
1386
+
1387
+ while (true) {
1388
+ const { done, value } = await reader.read();
1389
+ if (done) break;
1390
+ buffer += decoder.decode(value, { stream: true });
1391
+
1392
+ const lines = buffer.split("\n");
1393
+ buffer = lines.pop() || "";
1394
+ for (const line of lines) {
1395
+ if (!line.startsWith("data: ")) continue;
1396
+ try {
1397
+ const event = JSON.parse(line.slice(6));
1398
+ if (event.type === "progress") {
1399
+ const span = document.createElement("span");
1400
+ span.className = "figma-progress__line";
1401
+ span.textContent = event.message;
1402
+ progressEl.appendChild(span);
1403
+ progressEl.scrollTop = progressEl.scrollHeight;
1404
+ } else if (event.type === "complete") {
1405
+ result = event;
1406
+ }
1407
+ } catch { /* skip malformed */ }
1408
+ }
1409
+ }
1410
+
1411
+ if (!result || !result.ok) {
1412
+ showError(result?.error || "Conversion failed");
1413
+ if (genBtn) { genBtn.disabled = false; genBtn.textContent = "Generate Page"; }
1414
+ return;
1415
+ }
1416
+
1417
+ // 3. Done — navigate directly to chat (skip dashboard)
1418
+ if (genBtn) genBtn.textContent = "Done!";
1419
+ setTimeout(() => showAppDirect(themeName), 500);
1420
+ } catch (err) {
1421
+ showError("Conversion failed: " + err.message);
1422
+ if (genBtn) { genBtn.disabled = false; genBtn.textContent = "Generate Page"; }
1423
+ }
1424
+ }
1425
+
1138
1426
  function esc(str) {
1139
1427
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1140
1428
  }