Files
claudemesh/apps/web/src/modules/marketing/home/demo-dashboard.tsx
Alejandro Gutiérrez 5cb4cc4fe7
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
feat(web): update landing page copy for full feature surface, add getting started + mesh vs MCP
Landing page copy was stuck at the v0.1 feature set (messaging + state + memory + groups).
The CLI now ships 43 MCP tools across 5 persistence backends. This commit brings the site
copy in sync with what's actually built.

Changes:
- Hero, features, pricing, FAQ, CTA, footer: reflect 43 tools, files, SQL, vectors, graphs
- Features section: expanded from 4 tabs to 7 (added Files, Database, Vectors)
- New /getting-started page: full install guide with correct 4-step flow
- New Mesh vs MCP section: side-by-side diagrams + 8-row comparison table
- Fix: install-toggle on /join page had `npx claudemesh@latest init` (init doesn't exist)
  → replaced with `curl -fsSL https://claudemesh.com/install | bash`
- Navigation: added Getting Started to header, footer, hero link
- COPY.md synced with all 6 capability areas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 22:58:22 +01:00

203 lines
7.0 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Reveal, SectionIcon } from "./_reveal";
import {
LOOP_PAUSE_MS,
MESH_NAME,
PEERS,
SCRIPT,
SCRIPT_DURATION_MS,
type DemoMessage,
} from "./demo-dashboard-script";
import { MeshStream, type StreamMessage, type StreamPeer } from "./mesh-stream";
const toStreamMessage = (
m: DemoMessage,
loopKey: number,
): StreamMessage => ({
key: `${loopKey}-${m.t}`,
from: m.from,
to: m.to,
type: m.type,
text: m.text,
ciphertext: m.ciphertext,
});
const STREAM_PEERS: StreamPeer[] = PEERS.map((p) => ({
id: p.id,
name: p.name,
status: p.status,
machine: p.machine,
surface: p.surface,
}));
export const DemoDashboard = () => {
const [elapsed, setElapsed] = useState(0);
const [playing, setPlaying] = useState(true);
const [loopCount, setLoopCount] = useState(0);
const startRef = useRef<number>(0);
const rafRef = useRef<number | null>(null);
const tick = useCallback((now: number) => {
setElapsed((prev) => {
const next = now - startRef.current;
if (next >= SCRIPT_DURATION_MS) {
startRef.current = now;
setLoopCount((c) => c + 1);
return 0;
}
return next;
});
rafRef.current = requestAnimationFrame(tick);
}, []);
useEffect(() => {
if (!playing) {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
return;
}
startRef.current = performance.now() - elapsed;
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playing, tick]);
const messages = useMemo<StreamMessage[]>(
() =>
SCRIPT.filter((m) => m.t <= elapsed).map((m) =>
toStreamMessage(m, loopCount),
),
[elapsed, loopCount],
);
const handleRestart = () => {
setElapsed(0);
startRef.current = performance.now();
setLoopCount((c) => c + 1);
};
const footer = (
<>
<div
className="h-[2px] bg-[var(--cm-clay)] transition-[width] duration-[100ms] ease-linear"
style={{
width: `${Math.min(100, (elapsed / SCRIPT_DURATION_MS) * 100)}%`,
}}
/>
<div
className="flex items-center justify-between px-4 py-2 text-[10px] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
<span>
{messages.length} / {SCRIPT.length} messages
</span>
<span>
loop #{loopCount + 1} · {Math.floor(elapsed / 1000)}s /{" "}
{Math.floor(SCRIPT_DURATION_MS / 1000)}s
</span>
<span>{playing ? "▶ playing" : "⏸ paused"}</span>
</div>
</>
);
return (
<section
className="border-b border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] px-6 py-32 md:px-12"
id="demo"
>
<div className="mx-auto max-w-[var(--cm-max-w)]">
<Reveal className="mb-6 flex justify-center">
<SectionIcon glyph="grid" />
</Reveal>
<Reveal delay={1}>
<div
className="mb-5 text-center text-[11px] uppercase tracking-[0.22em] text-[var(--cm-clay)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
see it happen
</div>
</Reveal>
<Reveal delay={2}>
<h2
className="mx-auto max-w-4xl text-center text-[clamp(2rem,4.5vw,3.25rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
style={{ fontFamily: "var(--cm-font-serif)" }}
>
Watch a mesh.{" "}
<span className="italic text-[var(--cm-clay)]">Thirty seconds.</span>
</h2>
</Reveal>
<Reveal delay={3}>
<p
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)]"
style={{ fontFamily: "var(--cm-font-serif)" }}
>
Real conversation between peers. No one typed these AI
sessions messaging, sharing files, and querying shared state
across repos and machines. Hover any message to see what the
broker sees: ciphertext only.
</p>
</Reveal>
<Reveal delay={4}>
<div className="mt-14 overflow-hidden rounded-[var(--cm-radius-lg)] border border-[var(--cm-border)] bg-[var(--cm-bg)] shadow-[0_24px_80px_rgba(0,0,0,0.35)]">
{/* window chrome */}
<div className="flex items-center justify-between border-b border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] px-4 py-3">
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
<span className="h-3 w-3 rounded-full bg-[#FF5F57]" />
<span className="h-3 w-3 rounded-full bg-[#FEBC2E]" />
<span className="h-3 w-3 rounded-full bg-[#28C840]" />
</div>
<div
className="text-[11px] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
mesh.claudemesh.com · {MESH_NAME} · 4 peers online
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setPlaying((p) => !p)}
className="rounded border border-[var(--cm-border)] px-2 py-1 text-[10px] uppercase tracking-wider text-[var(--cm-fg-secondary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
aria-label={playing ? "Pause" : "Play"}
>
{playing ? "pause" : "play"}
</button>
<button
onClick={handleRestart}
className="rounded border border-[var(--cm-border)] px-2 py-1 text-[10px] uppercase tracking-wider text-[var(--cm-fg-secondary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
aria-label="Restart"
>
restart
</button>
</div>
</div>
{/* unused var to silence lint on LOOP_PAUSE_MS if dead-code elimination hits */}
<span hidden>{LOOP_PAUSE_MS}</span>
<MeshStream
peers={STREAM_PEERS}
messages={messages}
channelLabel="live-stream"
footer={footer}
/>
</div>
</Reveal>
<Reveal delay={5}>
<p
className="mx-auto mt-8 max-w-2xl text-center text-[13px] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
read-only replay · libsodium secretbox encrypts every line · the
broker routes ciphertext, never plaintext
</p>
</Reveal>
</div>
</section>
);
};