vibespot 1.5.0 → 1.6.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 +83 -271
- package/assets/prompts.bundle.json +53 -0
- package/assets/readme/00-hero-banner.png +0 -0
- package/assets/readme/00-hero-banner.svg +59 -0
- 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/dist/index.js +420 -420
- package/dist/index.js.map +1 -1
- package/package.json +10 -3
- package/ui/chat.js +106 -0
- package/ui/docs/index.html +55 -16
- package/ui/docs/screenshots.zip +0 -0
- package/ui/index.html +1 -0
- package/ui/settings.js +339 -17
- package/ui/styles.css +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibespot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,11 +19,18 @@
|
|
|
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",
|
|
22
26
|
"prepublishOnly": "npm run build",
|
|
23
|
-
"contact-monitor": "tsx scripts/contact-monitor.ts"
|
|
27
|
+
"contact-monitor": "tsx scripts/contact-monitor.ts",
|
|
28
|
+
"docker:publish": "scripts/docker-publish.sh",
|
|
29
|
+
"docker:publish:multi": "scripts/docker-publish.sh --multi-arch",
|
|
30
|
+
"docker:smoke": "scripts/docker-publish.sh --smoke --dry-run"
|
|
24
31
|
},
|
|
25
32
|
"dependencies": {
|
|
26
|
-
"@anthropic-ai/sdk": "^0.
|
|
33
|
+
"@anthropic-ai/sdk": "^0.96.0",
|
|
27
34
|
"@clack/prompts": "^1.3.0",
|
|
28
35
|
"@codemirror/lang-css": "^6.3.1",
|
|
29
36
|
"@codemirror/lang-html": "^6.4.11",
|
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 {
|
package/ui/docs/index.html
CHANGED
|
@@ -1300,20 +1300,60 @@ testimonial carousel, and a minimal footer with social links.</code></pre>
|
|
|
1300
1300
|
============================================================ -->
|
|
1301
1301
|
<div class="doc-section" id="docker-deployment">
|
|
1302
1302
|
<h2 id="docker-deployment-heading">Docker Deployment <a href="#docker-deployment" class="doc-anchor">#</a></h2>
|
|
1303
|
-
<p>Run vibeSpot as a containerised service for your team. The Docker image bundles the server, UI, and all dependencies — just add an AI API key.</p>
|
|
1303
|
+
<p>Run vibeSpot as a containerised service for your team. The Docker image bundles the server, UI, and all dependencies — just add an AI API key. The image is published to the GitHub Container Registry and is <strong>public</strong>, so no login or repository checkout is required to pull it.</p>
|
|
1304
|
+
|
|
1305
|
+
<pre><code>docker pull ghcr.io/borismichel/vibespot:latest</code></pre>
|
|
1306
|
+
<p>Tags: <code>latest</code> (most recent release), <code>1.5.0</code> / <code>1.5</code> (specific version), <code>main</code> (latest commit, may be unstable). Pin a version for production.</p>
|
|
1304
1307
|
|
|
1305
1308
|
<h3 id="docker-quick-start">Quick Start (LAN / VPN) <a href="#docker-quick-start" class="doc-anchor">#</a></h3>
|
|
1306
|
-
<
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1309
|
+
<p>Run the public image directly — no repository or compose file needed:</p>
|
|
1310
|
+
<pre><code>docker run -d --name vibespot \
|
|
1311
|
+
-p 4200:4200 \
|
|
1312
|
+
-e VIBESPOT_AI_ENGINE=anthropic-api \
|
|
1313
|
+
-e VIBESPOT_AGENTIC_MODE=true \
|
|
1314
|
+
-e ANTHROPIC_API_KEY=sk-ant-... \
|
|
1315
|
+
-v vibespot-config:/home/vibespot/.vibespot \
|
|
1316
|
+
-v vibespot-themes:/home/vibespot/vibespot-themes \
|
|
1317
|
+
ghcr.io/borismichel/vibespot:latest</code></pre>
|
|
1318
|
+
<p>Open <code>http://<host-ip>:4200</code> in a browser. Swap the engine and key for whichever provider you use. The two volumes keep config and generated themes across restarts.</p>
|
|
1310
1319
|
|
|
1311
1320
|
<h3 id="docker-https">HTTPS with Caddy <a href="#docker-https" class="doc-anchor">#</a></h3>
|
|
1312
|
-
<p>For public or semi-public deployments,
|
|
1313
|
-
<pre><code
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1321
|
+
<p>For public or semi-public deployments, put a <a href="https://caddyserver.com/" target="_blank">Caddy</a> reverse proxy in front to auto-provision a Let’s Encrypt TLS certificate. Save a <code>docker-compose.yml</code> and <code>Caddyfile</code> in a directory, then run <code>docker compose up -d</code>.</p>
|
|
1322
|
+
<pre><code># docker-compose.yml
|
|
1323
|
+
services:
|
|
1324
|
+
vibespot:
|
|
1325
|
+
image: ghcr.io/borismichel/vibespot:latest
|
|
1326
|
+
restart: unless-stopped
|
|
1327
|
+
environment:
|
|
1328
|
+
VIBESPOT_AI_ENGINE: anthropic-api
|
|
1329
|
+
VIBESPOT_AGENTIC_MODE: "true"
|
|
1330
|
+
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
|
|
1331
|
+
volumes:
|
|
1332
|
+
- vibespot-config:/home/vibespot/.vibespot
|
|
1333
|
+
- vibespot-themes:/home/vibespot/vibespot-themes
|
|
1334
|
+
expose: ["4200"]
|
|
1335
|
+
caddy:
|
|
1336
|
+
image: caddy:2-alpine
|
|
1337
|
+
restart: unless-stopped
|
|
1338
|
+
ports: ["80:80", "443:443"]
|
|
1339
|
+
volumes:
|
|
1340
|
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
1341
|
+
- caddy-data:/data
|
|
1342
|
+
depends_on: [vibespot]
|
|
1343
|
+
volumes:
|
|
1344
|
+
vibespot-config:
|
|
1345
|
+
vibespot-themes:
|
|
1346
|
+
caddy-data:</code></pre>
|
|
1347
|
+
<pre><code># Caddyfile (set your real domain)
|
|
1348
|
+
vibespot.example.com {
|
|
1349
|
+
@websockets {
|
|
1350
|
+
header Connection *Upgrade*
|
|
1351
|
+
header Upgrade websocket
|
|
1352
|
+
}
|
|
1353
|
+
reverse_proxy @websockets vibespot:4200
|
|
1354
|
+
reverse_proxy vibespot:4200
|
|
1355
|
+
}</code></pre>
|
|
1356
|
+
<p>Caddy binds ports 80 and 443. Point your DNS A record at the host, and HTTPS works automatically. The <code>@websockets</code> rule is required — the chat and generation pipeline run over a persistent WebSocket.</p>
|
|
1317
1357
|
|
|
1318
1358
|
<h3 id="docker-env-vars">Environment Variables <a href="#docker-env-vars" class="doc-anchor">#</a></h3>
|
|
1319
1359
|
<p>The Docker image reads all configuration from environment variables. The <code>.env.example</code> file documents every option.</p>
|
|
@@ -1324,7 +1364,6 @@ docker compose --profile https up -d</code></pre>
|
|
|
1324
1364
|
</thead>
|
|
1325
1365
|
<tbody>
|
|
1326
1366
|
<tr><td><code>VIBESPOT_PORT</code></td><td><code>4200</code></td><td>Port the server listens on inside the container</td></tr>
|
|
1327
|
-
<tr><td><code>VIBESPOT_DOMAIN</code></td><td>—</td><td>Public domain for the Caddy HTTPS profile</td></tr>
|
|
1328
1367
|
<tr><td><code>VIBESPOT_NO_OPEN</code></td><td><code>1</code></td><td>Suppress auto-open browser (set automatically in Docker)</td></tr>
|
|
1329
1368
|
<tr><td><code>VIBESPOT_AI_ENGINE</code></td><td>—</td><td>Default engine: <code>anthropic-api</code>, <code>openai-api</code>, <code>gemini-api</code>, <code>langdock-api</code></td></tr>
|
|
1330
1369
|
<tr><td><code>VIBESPOT_AGENTIC_MODE</code></td><td>—</td><td>Set <code>true</code> to enable the multi-stage agentic pipeline</td></tr>
|
|
@@ -1348,9 +1387,9 @@ docker compose --profile https up -d</code></pre>
|
|
|
1348
1387
|
<tr><td><code>vibespot-themes</code></td><td><code>/home/vibespot/vibespot-themes</code></td><td>Generated HubSpot themes</td></tr>
|
|
1349
1388
|
</tbody>
|
|
1350
1389
|
</table>
|
|
1351
|
-
<p>To back up:</p>
|
|
1352
|
-
<pre><code>docker
|
|
1353
|
-
docker
|
|
1390
|
+
<p>Mount the themes volume at exactly <code>/home/vibespot/vibespot-themes</code> — that is where the app writes generated themes. Without it, themes are lost when the container is recreated. To back up:</p>
|
|
1391
|
+
<pre><code>docker cp vibespot:/home/vibespot/.vibespot ./backup-config
|
|
1392
|
+
docker cp vibespot:/home/vibespot/vibespot-themes ./backup-themes</code></pre>
|
|
1354
1393
|
|
|
1355
1394
|
<h3 id="docker-reverse-proxy">Reverse Proxy & Kubernetes <a href="#docker-reverse-proxy" class="doc-anchor">#</a></h3>
|
|
1356
1395
|
<p>If you already have a reverse proxy (nginx, Traefik, k8s Ingress), skip the Caddy profile and point your proxy at port 4200. Key requirements:</p>
|
|
@@ -1360,8 +1399,8 @@ docker compose cp vibespot:/home/vibespot/vibespot-themes ./backup-themes</code>
|
|
|
1360
1399
|
<li><strong>No buffering</strong> — for streaming AI responses, disable proxy buffering.</li>
|
|
1361
1400
|
</ul>
|
|
1362
1401
|
<div class="doc-callout doc-callout--tip">
|
|
1363
|
-
<div class="doc-callout__label">&#
|
|
1364
|
-
<p>For
|
|
1402
|
+
<div class="doc-callout__label">⚠️ Single instance & auth</div>
|
|
1403
|
+
<p>vibeSpot keeps active sessions in memory — run a single instance (no horizontal scaling without sticky sessions). It runs as a non-root user and has <strong>no built-in authentication</strong>. For internet-facing deployments, put an authenticating proxy (OAuth2 Proxy, Authelia, Cloudflare Access) in front. Persistence is filesystem-only on the named volumes — there is no database to run.</p>
|
|
1365
1404
|
</div>
|
|
1366
1405
|
</div>
|
|
1367
1406
|
|
package/ui/docs/screenshots.zip
CHANGED
|
Binary file
|
package/ui/index.html
CHANGED
|
@@ -484,6 +484,7 @@
|
|
|
484
484
|
<div class="chat__header" id="chat-header">
|
|
485
485
|
<span class="chat__header-title" id="chat-header-title">Chat</span>
|
|
486
486
|
<span class="chat__header-context" id="chat-header-context"></span>
|
|
487
|
+
<span class="chat__cost-total hidden" id="chat-cost-total" title="Estimated total generation cost for this project"></span>
|
|
487
488
|
</div>
|
|
488
489
|
<div class="chat__messages" id="chat-messages" role="log" aria-live="polite">
|
|
489
490
|
<div class="chat__welcome" id="chat-welcome">
|