Files
vibn-agent-runner/design-templates/VIBN (2)/onboarding-build.jsx

446 lines
18 KiB
JavaScript

// Build + Ready screens. The build screen shows the terminal stream + a live
// preview stencil; ready is a quiet confirmation page with the workspace URL.
// ── Per-path build plans ───────────────────────────────────────────────────
function buildPlanFor(path, data) {
const common = [
{ line: "vibn init — reading brief", ms: 600 },
{ line: "↳ provisioning workspace", ms: 700 },
{ line: "↳ wiring auth (email + Google)", ms: 800 },
{ line: "↳ minting database & seed schema", ms: 700 },
];
if (path === "entrepreneur") {
return [
...common,
{ line: `↳ generating landing page · vibe "${data.vibe || "warm"}"`, ms: 900 },
{ line: `↳ writing copy aimed at "${(data.audience || "your audience").slice(0, 40)}"`, ms: 800 },
{ line: "↳ wiring email capture + Stripe payment link", ms: 700 },
{ line: "↳ scaffolding admin: subscribers, sales, comments", ms: 800 },
{ line: `↳ tuning launch plan for goal: ${data.goal || "first_customer"}`, ms: 700 },
{ line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 },
{ line: "ready.", ms: 400, ok: true },
];
}
if (path === "owner") {
return [
...common,
{ line: `↳ modelling ${data.biz || "small business"} for "${data.bizName || "your business"}"`, ms: 800 },
{ line: `↳ importing your stack (${(data.tools || []).length} tools)`, ms: 800 },
{ line: `↳ building module: ${labelFor(data.firstThing)}`, ms: 1000 },
{ line: "↳ generating customer + job records (10 sample)", ms: 700 },
{ line: "↳ scheduling daily ops view + weekly report", ms: 700 },
{ line: `↳ wiring savings tracker · est. $${(data.spend || 0)}/mo replaced`, ms: 800 },
{ line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 },
{ line: "ready.", ms: 400, ok: true },
];
}
return [
...common,
{ line: `↳ branding workspace for "${data.clientName || "client"}"`, ms: 800 },
{ line: `↳ scaffolding scope (${(data.scope || []).length} modules)`, ms: 1000 },
...(data.scope || []).slice(0, 4).map((s) => ({ line: `${s}`, ms: 350 })),
{ line: "↳ generating handoff document + invoice template", ms: 700 },
{ line: `↳ setting deploy target: ${data.handoff || "subdomain"}`, ms: 700 },
{ line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 },
{ line: "ready.", ms: 400, ok: true },
];
}
function labelFor(id) {
const map = {
booking: "Bookings & scheduling",
invoicing: "Quotes & invoices",
customers: "Customer portal",
inventory: "Inventory & orders",
team: "Team & dispatch",
marketing: "Marketing site",
};
return map[id] || "your first workflow";
}
function workspaceUrlFor(path, data) {
const seed =
(path === "owner" && data.bizName) ||
(path === "consultant" && data.clientName) ||
(path === "entrepreneur" && (data.audience || data.idea)) ||
"your-workspace";
const slug = String(seed).toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.trim()
.split(/\s+/)
.slice(0, 3)
.join("-")
.slice(0, 28) || "your-workspace";
return `${slug}.vibn.app`;
}
const BUILD_BIZ_LABEL = {
service: "Trades / services",
retail: "Retail",
food: "Food & drink",
appointments: "Appointments",
events: "Events / hospitality",
other: "Small business",
};
// ── Build screen ───────────────────────────────────────────────────────────
function BuildScreen({ path, data, onBack, onClose, onOpen }) {
const plan = React.useMemo(() => buildPlanFor(path, data), [path, data]);
const [lineIdx, setLineIdx] = React.useState(0);
const [done, setDone] = React.useState(false);
const logRef = React.useRef(null);
React.useEffect(() => {
if (lineIdx >= plan.length) { setDone(true); return undefined; }
const t = setTimeout(() => setLineIdx(lineIdx + 1), plan[lineIdx].ms);
return () => clearTimeout(t);
}, [lineIdx, plan]);
React.useEffect(() => {
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
}, [lineIdx]);
const url = workspaceUrlFor(path, data);
const lane = LANE_LABELS[path];
const previewTitle =
path === "owner" ? (data.bizName || "Your business") :
path === "consultant" ? (data.clientName || "Your client") :
"Your launch page";
const previewSub =
path === "owner" ? `${BUILD_BIZ_LABEL[data.biz] || "Small business"} · ${labelFor(data.firstThing)}` :
path === "consultant" ? (data.industry || "Project") :
(data.audience || "An idea worth building").slice(0, 64);
const pct = done ? 1 : lineIdx / plan.length;
return (
<>
<WizardTop
onBack={onBack}
onClose={onClose}
lane={lane}
stepText={done ? "Done" : "Building"}
progress={pct}
/>
<main className="wiz-body" style={{ paddingTop: "clamp(28px, 5vh, 56px)" }}>
<div className="wiz-card xwide" style={{ gap: 18 }}>
<style>{`
.b-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
gap: 18px;
}
@media (max-width: 920px) { .b-grid { grid-template-columns: 1fr; } }
.b-pane {
border-radius: 12px;
border: 1px solid var(--hairline);
background: linear-gradient(180deg, oklch(0.16 0.008 60 / 0.92), oklch(0.14 0.008 60 / 0.92));
min-height: 420px;
display: flex; flex-direction: column;
overflow: hidden;
}
.b-pane-bar {
display: flex; align-items: center; gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid var(--hairline);
background: oklch(0.16 0.008 60 / 0.6);
font-family: var(--font-mono);
font-size: 11px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.b-pane-bar .live {
margin-left: auto;
display: inline-flex; align-items: center; gap: 6px;
color: oklch(0.85 0.16 155);
font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase;
}
.b-pane-bar .live::before {
content: ""; width: 5px; height: 5px; border-radius: 50%;
background: oklch(0.78 0.16 155);
box-shadow: 0 0 0 0 oklch(0.78 0.16 155 / 0.6);
animation: pulse 2s ease-out infinite;
}
.b-log {
flex: 1;
padding: 14px 16px;
font-family: var(--font-mono);
font-size: 12.5px;
line-height: 1.7;
color: var(--fg-dim);
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
.b-log .l-ok { color: oklch(0.85 0.16 155); font-weight: 500; }
.b-log .l-current { color: var(--fg); }
.b-log .l-done { color: var(--fg-mute); }
.b-log .l-done::before { content: "✓ "; color: var(--ok); margin-right: 2px; }
.b-log .l-current::before { content: "● "; color: var(--accent); margin-right: 2px;
animation: blink 1s steps(2) infinite; }
.b-cursor {
display: inline-block;
width: 7px; height: 13px; vertical-align: -2px;
background: var(--accent);
margin-left: 2px;
animation: blink 1s steps(2) infinite;
box-shadow: 0 0 12px var(--accent-glow);
}
.b-prev-bar {
padding: 10px 14px;
display: flex; gap: 10px; align-items: center;
border-bottom: 1px solid var(--hairline);
background: oklch(0.16 0.008 60 / 0.5);
}
.b-prev-url {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--fg-mute);
padding: 4px 12px;
background: oklch(0.13 0.008 60);
border: 1px solid var(--hairline);
border-radius: 999px;
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.b-prev-stage {
flex: 1;
display: flex; align-items: center; justify-content: center;
padding: 22px;
}
.b-stencil {
width: 100%;
display: flex; flex-direction: column; gap: 12px;
opacity: 0;
animation: stencil-in 0.7s ease-out forwards;
}
@keyframes stencil-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.b-stencil-block {
border-radius: 9px;
background: linear-gradient(135deg, oklch(0.22 0.011 60), oklch(0.18 0.009 60));
border: 1px solid var(--hairline);
position: relative; overflow: hidden;
}
.b-stencil-block::after {
content: "";
position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, oklch(1 0 0 / 0.06), transparent);
transform: translateX(-100%);
animation: shimmer 2s ease-in-out infinite;
}
@keyframes shimmer { to { transform: translateX(100%); } }
@keyframes blink { 50% { opacity: 0.25; } }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 oklch(0.78 0.16 155 / 0.6); }
70% { box-shadow: 0 0 0 8px oklch(0.78 0.16 155 / 0); }
100% { box-shadow: 0 0 0 0 oklch(0.78 0.16 155 / 0); }
}
.b-foot {
display: flex; align-items: center; justify-content: space-between;
gap: 14px;
padding-top: 6px;
}
.b-foot-status {
font-family: var(--font-mono);
font-size: 11px; letter-spacing: 0.06em;
color: var(--fg-faint);
}
.b-foot-status b { color: var(--accent); font-weight: 500; }
`}</style>
<WizardQ
title={done ? "Your workspace is live." : "Building your workspace…"}
sub={done
? "Open it to see what Vibn made. Every change from here happens in the chat."
: "Nothing for you to do. Vibn is scaffolding everything from your answers."}
/>
<div className="b-grid">
{/* terminal */}
<div className="b-pane">
<div className="b-pane-bar">
<span style={{ width: 9, height: 9, borderRadius: "50%", background: "oklch(0.65 0.18 25 / 0.7)" }} />
<span style={{ width: 9, height: 9, borderRadius: "50%", background: "oklch(0.78 0.13 80 / 0.7)" }} />
<span style={{ width: 9, height: 9, borderRadius: "50%", background: "oklch(0.72 0.16 145 / 0.7)" }} />
<span style={{ marginLeft: 6 }}>vibn build {url}</span>
{!done && <span className="live">live</span>}
</div>
<div className="b-log" ref={logRef}>
{plan.slice(0, lineIdx + 1).map((p, i) => {
const isCurrent = i === lineIdx && !done;
const isOk = p.ok && i <= lineIdx;
const cls = isOk ? "l-ok" : isCurrent ? "l-current" : "l-done";
return <div key={i} className={cls}>{p.line}</div>;
})}
{!done && <span className="b-cursor" aria-hidden="true" />}
</div>
</div>
{/* preview */}
<div className="b-pane">
<div className="b-prev-bar">
<span style={{ display: "flex", gap: 5 }}>
<span style={{ width: 9, height: 9, borderRadius: "50%", background: "oklch(0.30 0.010 60)" }}/>
<span style={{ width: 9, height: 9, borderRadius: "50%", background: "oklch(0.30 0.010 60)" }}/>
<span style={{ width: 9, height: 9, borderRadius: "50%", background: "oklch(0.30 0.010 60)" }}/>
</span>
<span className="b-prev-url">https://{url}</span>
</div>
<div className="b-prev-stage">
<div className="b-stencil">
<div style={{ fontSize: 20, fontWeight: 500, letterSpacing: "-0.02em", color: "var(--fg)", lineHeight: 1.15 }}>
{previewTitle}
</div>
<div style={{ fontSize: 13, color: "var(--fg-mute)", lineHeight: 1.45 }}>
{previewSub}
</div>
<div style={{ height: 4 }} />
<div className="b-stencil-block" style={{ height: 80 }} />
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
<div className="b-stencil-block" style={{ height: 60, animationDelay: "0.4s" }} />
<div className="b-stencil-block" style={{ height: 60, animationDelay: "0.8s" }} />
</div>
</div>
</div>
</div>
</div>
<div className="b-foot">
<span className="b-foot-status">
{done ? "build complete" : <>Building <b>{Math.min(lineIdx, plan.length)}/{plan.length}</b></>}
</span>
{done && (
<button type="button" className="btn btn-primary btn-wiz" onClick={onOpen}>
Open my workspace <Arrow size={13} />
</button>
)}
</div>
</div>
</main>
</>
);
}
// ── Ready screen ───────────────────────────────────────────────────────────
function ReadyScreen({ path, data, onClose, onOpenChat }) {
const url = workspaceUrlFor(path, data);
const summary = summaryFor(path, data);
return (
<>
<WizardTop
onClose={onClose}
lane={LANE_LABELS[path]}
stepText="Ready"
progress={1}
/>
<WizardBody>
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 8 }}>
<span
style={{
width: 32, height: 32, borderRadius: 8,
background: "linear-gradient(135deg, var(--accent), oklch(0.65 0.20 18))",
boxShadow: "0 0 18px var(--accent-glow)",
display: "grid", placeItems: "center",
color: "var(--accent-fg)", flexShrink: 0,
}}
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor"
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m3 8.5 3.2 3.2L13 5"/>
</svg>
</span>
</div>
<WizardQ
title="You're in."
sub="Workspace provisioned, first build is online. Every change from here happens in the chat."
/>
<div
style={{
borderRadius: 10,
border: "1px solid var(--hairline)",
background: "oklch(0.18 0.009 60 / 0.6)",
overflow: "hidden",
}}
>
<div
style={{
padding: "14px 16px",
borderBottom: "1px solid var(--hairline)",
display: "flex", alignItems: "center", gap: 10,
}}
>
<span className="mono" style={{ fontSize: 10.5, color: "var(--fg-faint)", letterSpacing: "0.12em", textTransform: "uppercase" }}>
Workspace URL
</span>
<span className="mono" style={{ fontSize: 14, color: "var(--fg)", marginLeft: "auto" }}>
{url}
</span>
</div>
<div style={{ padding: "12px 16px 14px", display: "flex", flexDirection: "column", gap: 8 }}>
{summary.map((row, i) => (
<div key={i} style={{ display: "flex", gap: 14, alignItems: "baseline", fontSize: 13.5 }}>
<span
className="mono"
style={{
width: 86, flexShrink: 0,
color: "var(--fg-faint)",
fontSize: 10.5, letterSpacing: "0.08em", textTransform: "uppercase",
}}
>
{row.label}
</span>
<span style={{ color: "var(--fg-dim)" }}>{row.value}</span>
</div>
))}
</div>
</div>
<div className="wiz-foot">
<a href="index.html" className="wiz-skip">Back to home</a>
<button type="button" className="btn btn-primary btn-wiz" onClick={onOpenChat}>
Open the build chat <Arrow size={13} />
</button>
</div>
</WizardBody>
</>
);
}
function summaryFor(path, data) {
if (path === "owner") {
return [
{ label: "Business", value: `${data.bizName || "Untitled"} · ${BUILD_BIZ_LABEL[data.biz] || "Small business"}` },
{ label: "Replacing", value: `${(data.tools || []).length} tools · ~$${data.spend || 0}/mo` },
{ label: "First fix", value: labelFor(data.firstThing) },
{ label: "Team", value: `${data.team || 1} · ${(data.customers || 0).toLocaleString()} cust/mo` },
];
}
if (path === "consultant") {
return [
{ label: "Client", value: `${data.clientName || "Untitled"} · ${data.industry || ""}` },
{ label: "Scope", value: `${(data.scope || []).length} modules` },
{ label: "Brief", value: (data.brief || "").slice(0, 60) + ((data.brief || "").length > 60 ? "…" : "") },
{ label: "Handoff", value: data.handoff || "subdomain" },
];
}
return [
{ label: "Building", value: (data.idea || "").slice(0, 64) + ((data.idea || "").length > 64 ? "…" : "") },
{ label: "Audience", value: (data.audience || "").slice(0, 64) },
{ label: "Goal", value: data.goal || "first_customer" },
{ label: "Vibe", value: data.vibe || "warm" },
];
}
Object.assign(window, { BuildScreen, ReadyScreen });