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/README.md +62 -5
- package/dist/index.js +459 -197
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/ui/chat.js +70 -7
- package/ui/docs/index.html +157 -4
- package/ui/index.html +79 -3
- package/ui/plan.js +0 -0
- package/ui/settings.js +228 -1
- package/ui/setup.js +289 -1
- package/ui/styles.css +499 -2
- package/ui/vendor/marked.umd.js +46 -41
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1140
1428
|
}
|