← back to Ventura Claw
server/public/homepage.html
481 lines
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" type="application/rss+xml" title="VenturaClaw changelog" href="/feed" />
<link rel="prefetch" href="/connectors" />
<meta name="theme-color" content="#0e0e10" />
<script>(function(){try{if(localStorage.getItem('vc-theme')==='light')document.documentElement.setAttribute('data-theme','light');}catch(e){}})();</script>
<title>VenturaClaw — One AI command runs 67 SaaS tools</title>
<meta name="description" content="Type one sentence. The AI picks the right SaaS tool — Stripe, Shopify, Slack, Cloudflare, HubSpot — and runs the action. 67 connectors out of the box. Encrypted at rest, audit-trailed, sensitive actions gated by approval." />
<link rel="canonical" href="https://venturaclaw.com/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="VenturaClaw — One AI command runs 67 SaaS tools" />
<meta property="og:description" content="Type one sentence. The AI picks the tool and runs the action. 67 connectors. Zero workflow-building." />
<meta property="og:url" content="https://venturaclaw.com/" />
<meta property="og:image" content="https://venturaclaw.com/static/og-cover.svg" />
<meta property="og:site_name" content="VenturaClaw" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="VenturaClaw — One AI command runs 67 SaaS tools" />
<meta name="twitter:description" content="Type one sentence. The AI picks the tool and runs the action." />
<meta name="twitter:image" content="https://venturaclaw.com/static/og-cover.svg" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"SoftwareApplication","name":"VenturaClaw","applicationCategory":"BusinessApplication","operatingSystem":"Web","description":"AI agent that routes natural-language commands to 67 business connectors including Stripe, Shopify, Slack, Cloudflare, Notion, HubSpot, and Discord. Encrypted at rest, audit-trailed, with a sensitive-action approval queue.","url":"https://venturaclaw.com","offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},"publisher":{"@type":"Organization","name":"Steve Abrams","email":"info@venturaclaw.com"}}
</script>
<link rel="stylesheet" href="/static/style.css" />
<link rel="stylesheet" href="/static/homepage.css" />
<style>
html[data-theme="light"] {
--bg: #faf8f3; --bg-elevated: #f1ece1;
--rule: #d6cfbd; --rule-strong: #c3b9a2;
--ink: #1a1a1a; --ink-soft: #383530; --ink-mute: #6a655a;
--gold: #8a6520;
}
html, body { transition: background-color 200ms ease, color 200ms ease; }
.theme-toggle {
position: fixed; top: 18px; right: 18px; z-index: 100;
background: var(--bg-elevated); color: var(--ink-mute); border: 1px solid var(--rule);
width: 32px; height: 32px; border-radius: 50%; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center;
font-size: 14px; line-height: 1; padding: 0;
transition: all 200ms;
}
.theme-toggle:hover { color: var(--gold); border-color: var(--gold); transform: rotate(20deg); }
.theme-toggle .moon { display: inline; }
.theme-toggle .sun { display: none; }
html[data-theme="light"] .theme-toggle .moon { display: none; }
html[data-theme="light"] .theme-toggle .sun { display: inline; }
</style>
</head><body>
<button class="theme-toggle" id="themeToggle" type="button" title="Toggle light/dark" aria-label="Toggle theme"><span class="moon">☾</span><span class="sun">☀︎</span></button>
<script>document.getElementById('themeToggle').addEventListener('click',()=>{const c=document.documentElement.getAttribute('data-theme')==='light'?'light':'dark';const n=c==='light'?'dark':'light';if(n==='light')document.documentElement.setAttribute('data-theme','light');else document.documentElement.removeAttribute('data-theme');try{localStorage.setItem('vc-theme',n);}catch{}});</script>
<header class="topbar" style="max-width:var(--max);margin:0 auto;padding:18px 28px;display:flex;justify-content:space-between;align-items:center">
<div class="brand" style="display:flex;gap:12px;align-items:center">
<div class="logo-dot"></div>
<div>
<div class="brand-name" style="font-family:var(--serif);font-size:20px;font-weight:500">Ventura<em style="font-style:italic;color:var(--gold)">Claw</em></div>
<div class="brand-sub" style="font-family:var(--mono);font-size:9px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute)">Connected Operations</div>
</div>
</div>
<nav style="display:flex;gap:18px;align-items:center;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase">
<a href="#demo" style="color:var(--ink-soft);text-decoration:none">Try the demo</a>
<a href="#moats" style="color:var(--ink-soft);text-decoration:none">How it works</a>
<a href="/login" style="color:var(--gold);border:1px solid var(--gold);padding:8px 16px;border-radius:2px;text-decoration:none">Sign in</a>
</nav>
</header>
<main>
<section class="hero">
<div class="eyebrow"><span class="dot"></span><span id="eyebrow-text">67 connectors pre-wired · Zero Zaps to maintain</span></div>
<h1 class="headline">Don't build the workflow.<br>Just <em>ask for the result.</em></h1>
<p class="subhead">Every other tool sells the canvas. We sell the outcome. Type what you want — "refund order 1234 on Shopify," "post in #launch on Slack," "purge the homepage cache" — and the AI picks the right connector and runs it. 67 wired in. Encrypted per-user keys. Approval queue on the dangerous stuff. Audit trail on everything.</p>
<noscript>
<div style="background:rgba(212,160,74,0.08);border:1px solid var(--gold);border-radius:4px;padding:18px 22px;margin-bottom:24px;font-family:var(--mono);font-size:12px;line-height:1.7;color:var(--ink-soft)">
<strong style="color:var(--gold)">JavaScript is off</strong> — the live-demo widget needs JS to talk to the routing endpoint. Everything else on this page (the connector list at <a href="/connectors" style="color:var(--gold)">/connectors</a>, the API at <a href="/docs" style="color:var(--gold)">/docs</a>, sign-in at <a href="/login" style="color:var(--gold)">/login</a>) works without it.
</div>
</noscript>
<!-- ─── Live demo widget ─── -->
<div id="demo" class="demo-card" role="region" aria-label="Live routing demo">
<div class="demo-label">Try it · The AI tells you which tool would fire</div>
<form id="demoForm" class="demo-input-row" autocomplete="off">
<input id="demoInput" type="text" placeholder='e.g. "refund order 1234 on Shopify"' maxlength="200" aria-label="Demo command input" />
<button id="demoBtn" type="submit">Route →</button>
</form>
<div class="demo-presets">
<button type="button" class="demo-preset" data-preset="post in #launch on slack saying we shipped">Slack post</button>
<button type="button" class="demo-preset" data-preset="refund order 1234 on shopify">Shopify refund</button>
<button type="button" class="demo-preset" data-preset="purge cloudflare cache for venturaclaw.com">Cloudflare purge</button>
<button type="button" class="demo-preset" data-preset="add a row to my hubspot deals pipeline">HubSpot CRM</button>
<button type="button" class="demo-preset" data-preset="send a message to the team">Ambiguous (watch it ask)</button>
</div>
<div id="demoResult" class="demo-result" role="status" aria-live="polite"></div>
</div>
</section>
<!-- ─── Live route trail (autonomous fake terminal) ─── -->
<section aria-label="Live route trail" style="max-width:var(--max);margin:36px auto 0;padding:0 28px">
<div style="font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:10px;display:flex;align-items:center;gap:10px">
<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--good);box-shadow:0 0 10px var(--good);animation:pulse 1.8s infinite"></span>
Live route trail · last 6 commands
</div>
<div id="trail" style="background:rgba(0,0,0,0.32);border:1px solid var(--rule);border-left:3px solid var(--good);border-radius:3px;padding:14px 18px;font-family:var(--mono);font-size:12px;line-height:1.85;color:var(--ink-soft);overflow:hidden;min-height:170px"></div>
</section>
<!-- ─── 56-connector grid that lights up ─── -->
<section aria-label="All 67 connectors">
<div style="max-width:var(--max);margin:0 auto;padding:0 28px;font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:14px">All 67 connectors · <span id="connectorCount">loading…</span></div>
<div id="connectorGrid" class="connector-grid" aria-live="polite"></div>
</section>
<!-- ─── Moats ─── -->
<section id="moats" class="moats">
<h2>What makes it <em>different.</em></h2>
<div class="moat-grid">
<div class="moat">
<div class="moat-num">01</div>
<div class="moat-title">Encrypted at rest, by default.</div>
<div class="moat-body">Every API key and OAuth token is wrapped in AES-256-GCM with a versioned key envelope. Rotate the master key and old data still decrypts via dual-read until the next save sweep. Plaintext never touches disk after the first write.</div>
<span class="moat-tag">AES-256-GCM · key_id rotation</span>
</div>
<div class="moat">
<div class="moat-num">02</div>
<div class="moat-title">Sensitive actions wait for approval.</div>
<div class="moat-body">Reads execute. Writes — refunds, posts, DNS edits, payouts — land in an approval queue with the parsed intent shown in plain English. You see what's about to happen before it happens. Fail-closed against prompt injection.</div>
<span class="moat-tag">Approval gate · audit trail</span>
</div>
<div class="moat">
<div class="moat-num">03</div>
<div class="moat-title">Local LLM, your IP doesn't leak.</div>
<div class="moat-body">Routing runs on a Mac Studio at the edge of the network — qwen3:14b — not a third-party API. Your business commands aren't training someone else's model. Zero per-request inference cost.</div>
<span class="moat-tag">Ollama · qwen3:14b · $0/req</span>
</div>
</div>
</section>
<!-- How it works — 3-step pipeline visualized as code-on-card -->
<section style="max-width:var(--max);margin:80px auto 0;padding:0 28px">
<div style="font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:18px">How it works · 3 steps</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px">
<div style="background:var(--bg-elevated);border:1px solid var(--rule);border-radius:4px;padding:24px;position:relative;overflow:hidden">
<div style="font-family:var(--serif);font-style:italic;font-size:34px;color:var(--gold);line-height:1;margin-bottom:14px">i.</div>
<div style="font-family:var(--serif);font-size:18px;margin-bottom:10px">You type one sentence.</div>
<pre style="font-family:var(--mono);font-size:11px;line-height:1.7;color:var(--ink-soft);background:rgba(0,0,0,0.32);padding:10px 12px;border-radius:2px;border-left:2px solid var(--gold);margin:0;white-space:pre-wrap"><span style="color:var(--gold)">></span> refund order 1234 on shopify
<span style="color:var(--gold)">></span> post #launch on slack
<span style="color:var(--gold)">></span> purge cloudflare cache</pre>
</div>
<div style="background:var(--bg-elevated);border:1px solid var(--rule);border-radius:4px;padding:24px;position:relative;overflow:hidden">
<div style="font-family:var(--serif);font-style:italic;font-size:34px;color:var(--gold);line-height:1;margin-bottom:14px">ii.</div>
<div style="font-family:var(--serif);font-size:18px;margin-bottom:10px">Local LLM picks the tool.</div>
<pre style="font-family:var(--mono);font-size:10px;line-height:1.7;color:var(--ink-soft);background:rgba(0,0,0,0.32);padding:10px 12px;border-radius:2px;border-left:2px solid var(--good);margin:0;white-space:pre-wrap">{
<span style="color:var(--gold)">"connector"</span>: <span style="color:var(--good)">"shopify"</span>,
<span style="color:var(--gold)">"action"</span>: <span style="color:var(--good)">"order.refund"</span>,
<span style="color:var(--gold)">"input"</span>: { <span style="color:var(--gold)">"order_id"</span>: <span style="color:var(--good)">"1234"</span> }
}</pre>
</div>
<div style="background:var(--bg-elevated);border:1px solid var(--rule);border-left:3px solid var(--gold);border-radius:4px;padding:24px;position:relative;overflow:hidden">
<div style="font-family:var(--serif);font-style:italic;font-size:34px;color:var(--gold);line-height:1;margin-bottom:14px">iii.</div>
<div style="font-family:var(--serif);font-size:18px;margin-bottom:10px">You approve. It runs.</div>
<pre style="font-family:var(--mono);font-size:10px;line-height:1.7;color:var(--ink-soft);background:rgba(0,0,0,0.32);padding:10px 12px;border-radius:2px;border-left:2px solid var(--bad);margin:0;white-space:pre-wrap"><span style="color:var(--bad)">⚠ SENSITIVE</span> · awaiting approval
shopify.order.refund(1234)
<span style="color:var(--good)">[approve]</span> <span style="color:var(--ink-mute)">[reject]</span>
audit log #4827 written ✓</pre>
</div>
</div>
</section>
<!-- Versus the workflow builders -->
<section style="max-width:var(--max);margin:80px auto 0;padding:0 28px">
<div style="font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:18px">Versus the workflow-builder maze</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px">
<div style="background:var(--bg-elevated);border:1px solid var(--rule);border-radius:4px;padding:22px 24px">
<div style="font-family:var(--serif);font-size:18px;color:var(--ink);margin-bottom:6px">Zapier · Make · n8n</div>
<div style="font-family:var(--mono);font-size:11px;line-height:1.7;color:var(--ink-soft)">7,000+ apps in a directory. You drag, you wire, you maintain. Make's free tier polls every 15 minutes — by design.</div>
</div>
<div style="background:var(--bg-elevated);border:1px solid var(--rule);border-radius:4px;padding:22px 24px">
<div style="font-family:var(--serif);font-size:18px;color:var(--ink);margin-bottom:6px">Bardeen · Relay</div>
<div style="font-family:var(--mono);font-size:11px;line-height:1.7;color:var(--ink-soft)">Credit-metered AI assists. Bardeen's 1,000 Premium credits burn through in a single workday at 3 credits/row. The math is the moat — until it isn't.</div>
</div>
<div style="background:var(--bg-elevated);border:1px solid var(--rule);border-left:3px solid var(--gold);border-radius:4px;padding:22px 24px">
<div style="font-family:var(--serif);font-size:18px;color:var(--gold);margin-bottom:6px">Ventura<em style="font-style:italic">Claw</em></div>
<div style="font-family:var(--mono);font-size:11px;line-height:1.7;color:var(--ink-soft)">56 curated connectors, on-demand routing. The command layer IS the product, not a feature bolted on. Local LLM means your IP doesn't leak.</div>
</div>
</div>
</section>
<!-- FAQ — addresses real objections (security, cost, lock-in, who is this for) -->
<section id="faq" style="max-width:var(--max);margin:80px auto 0;padding:0 28px">
<div style="font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:18px">FAQ · what people actually ask</div>
<h2 style="font-family:var(--serif);font-weight:500;font-size:36px;margin:0 0 28px;letter-spacing:-.01em">Honest <em style="font-style:italic;color:var(--gold)">answers.</em></h2>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:14px">
<details class="faq-item"><summary>Where do my API keys actually live?</summary><p>On disk, AES-256-GCM encrypted with a versioned key envelope (key_id rotation supported). The encryption key is derived from a server-side secret loaded only at boot and never logged. The plaintext leaves memory the moment it's saved.</p></details>
<details class="faq-item"><summary>Does the AI ever see my full data?</summary><p>The local LLM only sees your <em>command</em> (e.g., "refund order 1234 on Shopify") to pick the connector and parse arguments. It does not see vendor responses or your other connectors' data. Routing runs on Steve's hardware — no third-party LLM API.</p></details>
<details class="faq-item"><summary>What if the AI picks the wrong tool?</summary><p>Sensitive actions (refunds, posts, DNS edits, deletions) land in an approval queue with the parsed intent shown in plain English. Read it. Approve or reject. Reads execute immediately because they don't change state.</p></details>
<details class="faq-item"><summary>How does this differ from Zapier or Make?</summary><p>Those are workflow builders — you wire triggers to actions on a canvas, then maintain the wires. VenturaClaw is the opposite: type one sentence, the AI picks the connector. No canvas, no Zaps to maintain, no per-task pricing.</p></details>
<details class="faq-item"><summary>What's the pricing?</summary><p>Currently free while in early access. The eventual model will be a flat workspace fee (no per-task metering, no credit math). If pricing changes, you'll get 30 days notice and can export your audit log + connector registrations on the way out.</p></details>
<details class="faq-item"><summary>Can I self-host?</summary><p>Not yet packaged for distribution. The architecture is a single Node process + JSON store + local Ollama, so it's straightforward to lift — email <a href="mailto:info@venturaclaw.com" style="color:var(--gold)">info@venturaclaw.com</a> if you want the Docker recipe.</p></details>
<details class="faq-item"><summary>What if I want a connector that's not in the 56?</summary><p>Most modern SaaS tools speak OAuth2 + REST and slot in cleanly. Tell us what you need; new connectors typically ship in 1-3 days. Anything that exposes a webhook can be wired the same week.</p></details>
<details class="faq-item"><summary>Is this production-ready?</summary><p>Encryption-at-rest, atomic writes with fsync, audit trail, sensitive-action approval queue, CSP+HSTS, rate limits — yes, by mainstream-SaaS standards. We're early-access because the connector matrix and UX are still hardening, not because the security floor is.</p></details>
</div>
</section>
<style>
.faq-item { background: var(--bg-elevated); border: 1px solid var(--rule); border-radius: 4px; padding: 16px 22px; transition: border-color 200ms; }
.faq-item:hover { border-color: var(--rule-strong); }
.faq-item[open] { border-color: var(--gold); }
.faq-item summary { font-family: var(--serif); font-size: 17px; font-weight: 500; color: var(--ink); cursor: pointer; list-style: none; position: relative; padding-right: 24px; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after { content: '+'; position: absolute; right: 0; top: 50%; transform: translateY(-50%); color: var(--gold); font-family: var(--mono); font-size: 18px; transition: transform 200ms; }
.faq-item[open] summary::after { content: '−'; }
.faq-item p { font-family: var(--mono); font-size: 12px; line-height: 1.75; color: var(--ink-soft); margin: 12px 0 0; }
.faq-item p em { color: var(--gold); font-style: italic; }
</style>
<section class="cta">
<h2>Skip the canvas. <em>Just type.</em></h2>
<div class="cta-row">
<a href="/login" class="btn-cta">Sign in →</a>
<a href="mailto:info@venturaclaw.com" class="btn-cta btn-cta-ghost">Talk to Steve</a>
</div>
</section>
<footer class="footer">
<div class="left">VenturaClaw</div>
<div class="right">
<a href="/connectors">Connectors</a>
<a href="/about">About</a>
<a href="/privacy">Privacy</a>
<a href="/terms">Terms</a>
<a href="/sitemap.xml">Sitemap</a>
<a href="mailto:info@venturaclaw.com">info@venturaclaw.com</a>
<a href="/login">Sign in</a>
</div>
</footer>
</main>
<script>
function esc(s) { return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
let CONNECTORS = [];
const grid = document.getElementById('connectorGrid');
const countEl = document.getElementById('connectorCount');
const result = document.getElementById('demoResult');
const input = document.getElementById('demoInput');
const btn = document.getElementById('demoBtn');
const form = document.getElementById('demoForm');
function tileHTML(c) {
const tint = String(c.tint || '#888').replace(/[^#0-9a-fA-F]/g,'').slice(0,7);
const slug = String(c.logo || c.id).replace(/[^a-z0-9-]/gi,'');
return `<div class="c-tile" data-id="${esc(c.id)}">
<img class="c-logo" src="https://cdn.simpleicons.org/${esc(slug)}/${esc(tint.replace('#',''))}" alt="${esc(c.name)} logo" loading="lazy" onerror="this.style.display='none'">
<div>
<div class="c-name">${esc(c.name)}</div>
<div class="c-cat">${esc(c.category)}</div>
</div>
</div>`;
}
async function loadConnectors() {
try {
// /api/connectors is auth-only, but anyone can hit /connectors-public-meta? Skip — fetch /sitemap.xml is public, but easier:
// Render a static placeholder grid — we hard-code the count from server-rendered sitemap; let's simplify and use a public catalog endpoint instead.
const r = await fetch('/api/public/connectors').catch(() => null);
if (!r || !r.ok) throw new Error('connectors_fetch_failed');
const d = await r.json();
CONNECTORS = d.connectors || [];
countEl.textContent = CONNECTORS.length + ' wired';
grid.innerHTML = CONNECTORS.map((c, i) => {
const html = tileHTML(c);
// Stagger float animation per tile so they don't bob in unison
return html.replace('class="c-tile"', `class="c-tile" style="--float-delay:${(i % 12) * 0.4}s;--float-dur:${6 + (i % 5)}s"`);
}).join('');
} catch (e) {
countEl.textContent = '(loading failed — refresh)';
}
}
function clearMatch() {
document.querySelectorAll('.c-tile').forEach(t => t.classList.remove('matched', 'dimmed'));
}
function highlightMatch(connectorId) {
clearMatch();
if (!connectorId) return;
let matched = false;
document.querySelectorAll('.c-tile').forEach(t => {
if (t.dataset.id === connectorId) {
t.classList.add('matched');
matched = true;
t.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
t.classList.add('dimmed');
}
});
return matched;
}
function renderResult(intent) {
if (!intent) {
result.innerHTML = '<span class="key">No route</span> · the local LLM is offline. Try again in a moment.';
result.classList.add('visible');
return;
}
if (intent.connector === 'clarify') {
const opts = (intent.options || []).map(esc).join(', ');
result.innerHTML = `<span class="key">Clarify</span> · <span class="val-reason">${esc(intent.question || 'Need more info')}</span><br><span class="key">Options</span> · ${opts}`;
clearMatch();
} else if (intent.connector === 'unconfigured') {
result.innerHTML = `<span class="key">Suggested</span> · <span class="val-connector">${esc(intent.suggested)}</span> — connect it after sign-up.`;
highlightMatch(intent.suggested);
} else if (intent.connector === 'none') {
result.innerHTML = `<span class="key">No tool fits</span> · <span class="val-reason">${esc(intent.reason || 'no match')}</span>`;
clearMatch();
} else {
const c = CONNECTORS.find(x => x.id === intent.connector);
const name = c ? c.name : intent.connector;
let html = `<span class="key">Route</span> → <span class="val-connector">${esc(name)}</span>`;
if (intent.action) html += ` · <span class="val-action">${esc(intent.action)}</span>`;
if (intent.reason) html += `<br><span class="key">Why</span> · <span class="val-reason">${esc(intent.reason)}</span>`;
html += `<br><span style="display:inline-block;margin-top:10px;font-size:10px;color:var(--ink-mute)">▸ <a href="/login" style="color:var(--gold)">Sign in to actually run this →</a></span>`;
result.innerHTML = html;
highlightMatch(intent.connector);
}
result.classList.add('visible');
}
const THINKING_FRAMES = ['Asking the local LLM', 'Asking the local LLM.', 'Asking the local LLM..', 'Asking the local LLM...'];
let _thinkInt = null;
function startThinking() {
let i = 0;
result.classList.add('visible');
result.innerHTML = `<span class="key">${THINKING_FRAMES[0]}</span>`;
_thinkInt = setInterval(() => {
i = (i + 1) % THINKING_FRAMES.length;
result.innerHTML = `<span class="key">${THINKING_FRAMES[i]}</span>`;
}, 420);
}
function stopThinking() { if (_thinkInt) { clearInterval(_thinkInt); _thinkInt = null; } }
async function classify(message) {
btn.disabled = true; btn.textContent = 'Routing…';
startThinking();
try {
const r = await fetch('/api/demo-classify', {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message })
});
const d = await r.json();
stopThinking();
if (!r.ok) {
result.innerHTML = `<span class="key">Error</span> · ${esc(d.msg || d.error || 'request failed')}`;
return;
}
renderResult(d.intent);
} catch (e) {
stopThinking();
result.innerHTML = '<span class="key">Network error</span> · check your connection and retry';
} finally {
stopThinking();
btn.disabled = false; btn.textContent = 'Route →';
}
}
form.addEventListener('submit', e => {
e.preventDefault();
const msg = input.value.trim();
if (msg.length < 3) return;
classify(msg);
});
document.querySelectorAll('.demo-preset').forEach(b => {
b.addEventListener('click', () => {
input.value = b.dataset.preset;
classify(b.dataset.preset);
});
});
loadConnectors();
// Deep-link from /connectors/:id pages — if visitor arrives with ?demo=<text>,
// auto-fill the input + fire the classifier so the demo plays itself.
(function autoplayFromQuery() {
const params = new URLSearchParams(location.search);
const dq = params.get('demo');
if (!dq || dq.length < 3 || dq.length > 200) return;
input.value = dq;
// Wait for connector grid to load, then fire — gives the matched tile something to highlight
const fire = () => { setTimeout(() => classify(dq), 300); };
if (CONNECTORS.length) fire(); else setTimeout(fire, 800);
})();
// Eyebrow rotating taglines — cycles 4 differentiation hooks every 5s
const EYEBROW_ROTATE = [
'67 connectors pre-wired · Zero Zaps to maintain',
'AES-256-GCM at rest · Sensitive actions gated · Audit on everything',
'Local LLM · Your business commands stay private',
'No workflow builder · Just ask for the result',
];
let _ebIdx = 0;
const ebEl = document.getElementById('eyebrow-text');
setInterval(() => {
_ebIdx = (_ebIdx + 1) % EYEBROW_ROTATE.length;
ebEl.style.opacity = '0';
setTimeout(() => {
ebEl.textContent = EYEBROW_ROTATE[_ebIdx];
ebEl.style.opacity = '1';
}, 280);
}, 5000);
ebEl.style.transition = 'opacity 280ms ease';
// ─── Live route trail (UX idea: autonomous fake terminal showing routes) ───
// Generates plausible-looking past routes with absurd-but-credible names. Pure cosmetic;
// no network calls. Adds the "this thing is alive" feel without leaking real user data.
const FAKE_USERS = ["margot","priya","jules","kenji","leo","ana","theo","mira","sasha","felix","noor","wren","ezra","luna","cyrus","aja","ines","maya","odetta","quincy"];
const FAKE_LASTS = ["wenger","okafor","santos","huang","kovac","torres","park","abrams","ahmed","kim","singh","patel","rossi","cohen","greer","ito"];
const TRAIL_TEMPLATES = [
{ fmt: 'refund stripe charge ch_{id}', connector: 'stripe', action: 'refund.create' },
{ fmt: 'post in #ship-it on slack: "{phrase}"', connector: 'slack', action: 'chat.postMessage' },
{ fmt: 'purge cloudflare cache for {host}', connector: 'cloudflare', action: 'cache.purge_all' },
{ fmt: 'add row to hubspot deals: {company}', connector: 'hubspot', action: 'deal.create' },
{ fmt: 'list shopify orders since yesterday', connector: 'shopify', action: 'order.list' },
{ fmt: 'send a notion update to "Q4 Plan"', connector: 'notion', action: 'page.append' },
{ fmt: 'mailchimp pause "Welcome v3" campaign', connector: 'mailchimp', action: 'campaign.pause' },
{ fmt: 'github merge PR #{num}', connector: 'github', action: 'pull_request.merge' },
{ fmt: 'create asana task "Fix SSO redirect"', connector: 'asana', action: 'task.create' },
{ fmt: 'twilio sms +1 415-***-{num}: "ETA 5min"', connector: 'twilio', action: 'message.create' },
{ fmt: 'discord post in #lobby: {phrase}', connector: 'discord', action: 'channel.send' },
{ fmt: 'airtable update "Roadmap" row {num}', connector: 'airtable', action: 'record.update' },
];
const PHRASES = ["shipped","approved","just landed","green light","review needed","backfill done","cron paused","invoice paid"];
const COMPANIES = ["Olympia Bakery","Northbrook Roofing","Astra Films","Verdant CSA","Lumen Tutoring","Beachgrass Vinyl","Iron Compass"];
const HOSTS = ["acme.co","studio.example","forza.legal","designerwallcoverings.com","jules.studio"];
function _pick(arr) { return arr[Math.floor(Math.random()*arr.length)]; }
function _id() { return Array.from({length:14},() => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random()*36)]).join(""); }
function _trailLine() {
const t = _pick(TRAIL_TEMPLATES);
let cmd = t.fmt
.replace('{id}', _id())
.replace('{host}', _pick(HOSTS))
.replace('{phrase}', _pick(PHRASES))
.replace('{company}', _pick(COMPANIES))
.replace('{num}', String(100 + Math.floor(Math.random()*900)));
const user = _pick(FAKE_USERS) + '.' + _pick(FAKE_LASTS).slice(0,1);
const t0 = new Date(Date.now() - Math.floor(Math.random()*180000));
const tag = `[${t0.toISOString().slice(11,19)}Z]`;
return { cmd, user, tag, connector: t.connector, action: t.action };
}
const trailEl = document.getElementById('trail');
function _renderTrail() {
const lines = Array.from({length: 6}, _trailLine);
trailEl.innerHTML = lines.map(l => `
<div class="trail-row" style="display:flex;gap:10px;align-items:baseline">
<span style="color:var(--ink-mute);font-size:10px;flex-shrink:0">${esc(l.tag)}</span>
<span style="color:var(--gold);flex-shrink:0">${esc(l.user)}</span>
<span style="opacity:0.85">${esc(l.cmd)}</span>
<span style="color:var(--good);font-size:10px;margin-left:auto;flex-shrink:0">→ ${esc(l.connector)}.${esc(l.action)}</span>
</div>`).join('');
}
_renderTrail();
// Replace the top entry every 4-7s — feels alive without distracting the demo widget.
setInterval(() => {
const fresh = _trailLine();
const row = document.createElement('div');
row.className = 'trail-row';
row.style.cssText = 'display:flex;gap:10px;align-items:baseline;animation:trailIn 600ms ease-out';
row.innerHTML = `<span style="color:var(--ink-mute);font-size:10px;flex-shrink:0">${esc(fresh.tag)}</span>
<span style="color:var(--gold);flex-shrink:0">${esc(fresh.user)}</span>
<span style="opacity:0.85">${esc(fresh.cmd)}</span>
<span style="color:var(--good);font-size:10px;margin-left:auto;flex-shrink:0">→ ${esc(fresh.connector)}.${esc(fresh.action)}</span>`;
trailEl.insertBefore(row, trailEl.firstChild);
while (trailEl.children.length > 6) trailEl.removeChild(trailEl.lastChild);
}, 4500 + Math.floor(Math.random()*2500));
const _trailStyle = document.createElement('style');
_trailStyle.textContent = '@keyframes trailIn { from { opacity:0; transform:translateY(-8px) } to { opacity:1; transform:none } }';
document.head.appendChild(_trailStyle);
</script>
</body></html>