feat(web): interactive mesh demo dashboard — Discord-inspired playback
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
Visitors read the page and still don't grok claudemesh is a *mesh* of agents, not chatbot integrations. Fix: drop them straight into a live Discord-style view of 4 peers talking. No auth, no WS, no backend — a pre-recorded 10-second conversation that loops, encrypted over a fake broker. The conversation script (demo-dashboard-script.ts) hits every mental model the landing needs to plant: bob-desktop → #payments: "stripe sig verification broken?" alice-laptop (self-nominates): "hit this 2wks ago, pulling fix" alice → bob (direct): "<actual fix with file+line>" bob → alice: "saved me. thanks 🙏" carol-ios → #infra: "CI red on main?" bob → carol: "reverting 7af3d, ~2min" Covers: tag-routed broadcast (ask_mesh), self-election (hand-raise), direct-peer DM, cross-surface (phone peer in the mix), multi-thread concurrency. Component (demo-dashboard.tsx, ~420 LOC): ┌─────────────────────────────────────────────────┐ │ meshes | peers | live message stream │ │ side | list | (motion fade+rise on each msg) │ │ bar | | │ └─────────────────────────────────────────────────┘ - requestAnimationFrame playback loop against SCRIPT[].t offsets - Auto-loops after SCRIPT_DURATION_MS, 4s pause baked in - Per-peer filter: click a peer in the sidebar, only their messages show in the stream (from OR to), shows "filtered: <peer>" in header - Play / pause / restart buttons - Hover any message → dashed clay box shows the fake ciphertext: "broker sees only this: AUp3+n7z1bY=.kQfM9vL4jR8..." — drives the E2E point without a paragraph of crypto copy - Status dots: green idle, clay pulse working, grey offline - Surface glyphs inline (terminal / phone / slack) next to peer names - Message type chips: ⟐ broadcast, ← hand-raise, → direct - Progress bar at bottom ties the loop to a visible timeline - Window chrome with traffic-light dots + "mesh.claudemesh.com · flexicar-ops · 4 peers online" header Mounted between WhatIsClaudemesh and BeyondTerminal — explainer first, then show-don't-tell. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { LaptopToLaptop } from "~/modules/marketing/home/laptop-to-laptop";
|
|||||||
import { Features } from "~/modules/marketing/home/features";
|
import { Features } from "~/modules/marketing/home/features";
|
||||||
import { MeetsYou } from "~/modules/marketing/home/meets-you";
|
import { MeetsYou } from "~/modules/marketing/home/meets-you";
|
||||||
import { BeyondTerminal } from "~/modules/marketing/home/beyond-terminal";
|
import { BeyondTerminal } from "~/modules/marketing/home/beyond-terminal";
|
||||||
|
import { DemoDashboard } from "~/modules/marketing/home/demo-dashboard";
|
||||||
import { WhatIsClaudemesh } from "~/modules/marketing/home/what-is-claudemesh";
|
import { WhatIsClaudemesh } from "~/modules/marketing/home/what-is-claudemesh";
|
||||||
import { FAQ } from "~/modules/marketing/home/faq";
|
import { FAQ } from "~/modules/marketing/home/faq";
|
||||||
import { CallToAction } from "~/modules/marketing/home/cta";
|
import { CallToAction } from "~/modules/marketing/home/cta";
|
||||||
@@ -23,6 +24,7 @@ const HomePage = () => {
|
|||||||
<Features />
|
<Features />
|
||||||
<MeetsYou />
|
<MeetsYou />
|
||||||
<WhatIsClaudemesh />
|
<WhatIsClaudemesh />
|
||||||
|
<DemoDashboard />
|
||||||
<BeyondTerminal />
|
<BeyondTerminal />
|
||||||
<FAQ />
|
<FAQ />
|
||||||
<CallToAction />
|
<CallToAction />
|
||||||
|
|||||||
118
apps/web/src/modules/marketing/home/demo-dashboard-script.ts
Normal file
118
apps/web/src/modules/marketing/home/demo-dashboard-script.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Pre-recorded mesh conversation. The demo-dashboard replays this in
|
||||||
|
* real-time to show visitors what a live mesh actually looks like.
|
||||||
|
*
|
||||||
|
* `t` is the timestamp in ms from script start. Messages animate in
|
||||||
|
* at their `t` offset. Script loops after LOOP_PAUSE_MS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type PeerStatus = "idle" | "working" | "offline";
|
||||||
|
|
||||||
|
export interface Peer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: PeerStatus;
|
||||||
|
machine: string;
|
||||||
|
surface: "terminal" | "phone" | "slack";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageType = "ask_mesh" | "self_nominate" | "direct";
|
||||||
|
|
||||||
|
export interface DemoMessage {
|
||||||
|
/** ms from script start */
|
||||||
|
t: number;
|
||||||
|
from: string;
|
||||||
|
to: string | null; // peer id for direct, "tag:xxx" for broadcast, null for self-nominate
|
||||||
|
type: MessageType;
|
||||||
|
text: string;
|
||||||
|
/** Fake ciphertext to show the broker only sees this */
|
||||||
|
ciphertext: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PEERS: Peer[] = [
|
||||||
|
{
|
||||||
|
id: "alice-laptop",
|
||||||
|
name: "alice-laptop",
|
||||||
|
status: "idle",
|
||||||
|
machine: "macOS · payments-api",
|
||||||
|
surface: "terminal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bob-desktop",
|
||||||
|
name: "bob-desktop",
|
||||||
|
status: "working",
|
||||||
|
machine: "linux · checkout-svc",
|
||||||
|
surface: "terminal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "carol-ios",
|
||||||
|
name: "carol-ios",
|
||||||
|
status: "idle",
|
||||||
|
machine: "iOS · push-relay",
|
||||||
|
surface: "phone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "slack-bot",
|
||||||
|
name: "slack-bot",
|
||||||
|
status: "idle",
|
||||||
|
machine: "oncall · ops",
|
||||||
|
surface: "slack",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MESH_NAME = "flexicar-ops";
|
||||||
|
export const LOOP_PAUSE_MS = 4000;
|
||||||
|
|
||||||
|
export const SCRIPT: DemoMessage[] = [
|
||||||
|
{
|
||||||
|
t: 400,
|
||||||
|
from: "bob-desktop",
|
||||||
|
to: "tag:payments",
|
||||||
|
type: "ask_mesh",
|
||||||
|
text: "anyone seen stripe signature verification issues? getting 400 on /webhooks",
|
||||||
|
ciphertext: "AUp3+n7z1bY=.kQfM9vL4jR8xHt2eW…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 1900,
|
||||||
|
from: "alice-laptop",
|
||||||
|
to: null,
|
||||||
|
type: "self_nominate",
|
||||||
|
text: "I'm in payments-api — hit this two weeks ago. pulling my fix.",
|
||||||
|
ciphertext: "BWqX+m8t2cZ=.vLrN6oS3pK9yIu4aF…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 3800,
|
||||||
|
from: "alice-laptop",
|
||||||
|
to: "bob-desktop",
|
||||||
|
type: "direct",
|
||||||
|
text: "crypto.createHmac('sha256', webhookSecret) + timingSafeEqual. raw body, not JSON.parsed. src/webhooks/stripe.ts:47",
|
||||||
|
ciphertext: "CXsY+k9u3dA=.wMsO7pT4qL0zJv5bG…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 5400,
|
||||||
|
from: "bob-desktop",
|
||||||
|
to: "alice-laptop",
|
||||||
|
type: "direct",
|
||||||
|
text: "saved me. applying now. thanks 🙏",
|
||||||
|
ciphertext: "DYtZ+j0v4eB=.xNtP8qU5rM1aKw6cH…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 6800,
|
||||||
|
from: "carol-ios",
|
||||||
|
to: "tag:infra",
|
||||||
|
type: "ask_mesh",
|
||||||
|
text: "CI is red on main — who's on deploys?",
|
||||||
|
ciphertext: "EZuA+i1w5fC=.yOuQ9rV6sN2bLx7dI…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 8200,
|
||||||
|
from: "bob-desktop",
|
||||||
|
to: "carol-ios",
|
||||||
|
type: "direct",
|
||||||
|
text: "already on it, reverting 7af3d — back green in ~2min",
|
||||||
|
ciphertext: "FavB+h2x6gD=.zPvR0sW7tO3cMy8eJ…",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SCRIPT_DURATION_MS =
|
||||||
|
Math.max(...SCRIPT.map((m) => m.t)) + LOOP_PAUSE_MS;
|
||||||
438
apps/web/src/modules/marketing/home/demo-dashboard.tsx
Normal file
438
apps/web/src/modules/marketing/home/demo-dashboard.tsx
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
"use client";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
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,
|
||||||
|
type Peer,
|
||||||
|
type PeerStatus,
|
||||||
|
} from "./demo-dashboard-script";
|
||||||
|
|
||||||
|
const STATUS_DOT: Record<PeerStatus, string> = {
|
||||||
|
idle: "bg-emerald-500",
|
||||||
|
working: "bg-[var(--cm-clay)] animate-pulse",
|
||||||
|
offline: "bg-[var(--cm-fg-tertiary)]",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SURFACE_ICON: Record<Peer["surface"], React.ReactNode> = {
|
||||||
|
terminal: (
|
||||||
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<path d="M6 9l3 3-3 3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
phone: (
|
||||||
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect x="7" y="2" width="10" height="20" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<circle cx="12" cy="18" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
slack: (
|
||||||
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect x="10" y="3" width="2" height="6" rx="1" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<rect x="12" y="15" width="2" height="6" rx="1" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<rect x="3" y="10" width="6" height="2" rx="1" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<rect x="15" y="12" width="6" height="2" rx="1" stroke="currentColor" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE_GLYPH = (type: DemoMessage["type"]) => {
|
||||||
|
if (type === "ask_mesh")
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-[4px] border border-[var(--cm-border)] bg-[var(--cm-bg)] px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-[var(--cm-clay)]">
|
||||||
|
⟐ broadcast
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
if (type === "self_nominate")
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-[4px] border border-emerald-500/40 bg-emerald-500/10 px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-emerald-500">
|
||||||
|
← hand-raise
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-[4px] border border-[var(--cm-border)] bg-[var(--cm-bg)] px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-[var(--cm-fg-secondary)]">
|
||||||
|
→ direct
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type VisibleMessage = DemoMessage & { seq: number };
|
||||||
|
|
||||||
|
export const DemoDashboard = () => {
|
||||||
|
const [elapsed, setElapsed] = useState(0);
|
||||||
|
const [playing, setPlaying] = useState(true);
|
||||||
|
const [focusedPeer, setFocusedPeer] = useState<string | null>(null);
|
||||||
|
const [loopCount, setLoopCount] = useState(0);
|
||||||
|
const [hoveredMessage, setHoveredMessage] = useState<number | null>(null);
|
||||||
|
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 visible = useMemo<VisibleMessage[]>(() => {
|
||||||
|
return SCRIPT.filter((m) => m.t <= elapsed).map((m, i) => ({
|
||||||
|
...m,
|
||||||
|
seq: loopCount * 100 + i,
|
||||||
|
}));
|
||||||
|
}, [elapsed, loopCount]);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!focusedPeer) return visible;
|
||||||
|
return visible.filter(
|
||||||
|
(m) => m.from === focusedPeer || m.to === focusedPeer,
|
||||||
|
);
|
||||||
|
}, [visible, focusedPeer]);
|
||||||
|
|
||||||
|
const handleRestart = () => {
|
||||||
|
setElapsed(0);
|
||||||
|
startRef.current = performance.now();
|
||||||
|
setLoopCount((c) => c + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const peerName = (id: string) =>
|
||||||
|
PEERS.find((p) => p.id === id)?.name ?? id;
|
||||||
|
|
||||||
|
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 — they're
|
||||||
|
AI sessions referencing each other's work across repos,
|
||||||
|
machines, and surfaces. Hover any message to see what the broker
|
||||||
|
sees.
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[200px_220px_1fr] min-h-[480px]">
|
||||||
|
{/* server sidebar */}
|
||||||
|
<aside
|
||||||
|
className="hidden border-r border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/40 p-4 md:block"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mb-3 text-[10px] uppercase tracking-[0.18em] text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
your meshes
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
<li className="rounded-[var(--cm-radius-xs)] px-2.5 py-1.5 text-[13px] text-[var(--cm-fg-tertiary)] hover:bg-[var(--cm-bg)]">
|
||||||
|
smoke-test
|
||||||
|
</li>
|
||||||
|
<li className="rounded-[var(--cm-radius-xs)] bg-[var(--cm-clay)]/15 px-2.5 py-1.5 text-[13px] font-medium text-[var(--cm-clay)]">
|
||||||
|
{MESH_NAME}
|
||||||
|
</li>
|
||||||
|
<li className="rounded-[var(--cm-radius-xs)] px-2.5 py-1.5 text-[13px] text-[var(--cm-fg-tertiary)] hover:bg-[var(--cm-bg)]">
|
||||||
|
home-lab
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
className="mt-3 w-full rounded-[var(--cm-radius-xs)] border border-dashed border-[var(--cm-border)] px-2.5 py-1.5 text-left text-[12px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg-tertiary)] hover:text-[var(--cm-fg-secondary)]"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
+ join mesh
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* peers */}
|
||||||
|
<aside
|
||||||
|
className="border-r border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/20 p-4"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mb-3 flex items-center justify-between text-[10px] uppercase tracking-[0.18em] text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
<span>peers · {PEERS.filter((p) => p.status !== "offline").length} online</span>
|
||||||
|
{focusedPeer && (
|
||||||
|
<button
|
||||||
|
onClick={() => setFocusedPeer(null)}
|
||||||
|
className="text-[var(--cm-clay)] hover:underline"
|
||||||
|
aria-label="Clear filter"
|
||||||
|
>
|
||||||
|
clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{PEERS.map((p) => {
|
||||||
|
const active = focusedPeer === p.id;
|
||||||
|
return (
|
||||||
|
<li key={p.id}>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setFocusedPeer(active ? null : p.id)
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
"group flex w-full items-center gap-2.5 rounded-[var(--cm-radius-xs)] px-2 py-1.5 text-left transition-colors " +
|
||||||
|
(active
|
||||||
|
? "bg-[var(--cm-clay)]/15"
|
||||||
|
: "hover:bg-[var(--cm-bg)]")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
"h-2 w-2 flex-shrink-0 rounded-full " +
|
||||||
|
STATUS_DOT[p.status]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
"truncate text-[13px] " +
|
||||||
|
(active
|
||||||
|
? "font-medium text-[var(--cm-clay)]"
|
||||||
|
: "text-[var(--cm-fg)]")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-[var(--cm-fg-tertiary)]">
|
||||||
|
{SURFACE_ICON[p.surface]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="truncate text-[10px] text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
{p.machine}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* message stream */}
|
||||||
|
<div
|
||||||
|
className="relative flex flex-col"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 border-b border-[var(--cm-border)] px-4 py-2.5"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
<span className="text-[var(--cm-clay)]">#</span>
|
||||||
|
<span className="text-[13px] font-medium text-[var(--cm-fg)]">
|
||||||
|
live-stream
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-[var(--cm-fg-tertiary)]">
|
||||||
|
{focusedPeer
|
||||||
|
? `filtered: ${peerName(focusedPeer)}`
|
||||||
|
: "all peers · E2E encrypted"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ol className="flex-1 space-y-3 overflow-y-auto p-4">
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{filtered.map((m) => (
|
||||||
|
<motion.li
|
||||||
|
key={`${m.seq}-${m.t}`}
|
||||||
|
initial={{ opacity: 0, y: 8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.22, 0.61, 0.36, 1],
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setHoveredMessage(m.seq)}
|
||||||
|
onMouseLeave={() => setHoveredMessage(null)}
|
||||||
|
className="group relative"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0 pt-0.5">
|
||||||
|
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-[var(--cm-bg-elevated)] text-[10px] font-medium uppercase text-[var(--cm-fg-secondary)]">
|
||||||
|
{peerName(m.from).slice(0, 2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="mb-1 flex flex-wrap items-center gap-2">
|
||||||
|
<span className="text-[13px] font-medium text-[var(--cm-fg)]">
|
||||||
|
{peerName(m.from)}
|
||||||
|
</span>
|
||||||
|
{m.to && (
|
||||||
|
<>
|
||||||
|
<span className="text-[11px] text-[var(--cm-fg-tertiary)]">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-[12px] text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
{m.to.startsWith("tag:")
|
||||||
|
? m.to
|
||||||
|
: peerName(m.to)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{TYPE_GLYPH(m.type)}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-[14px] leading-[1.55] text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
{m.text}
|
||||||
|
</p>
|
||||||
|
{hoveredMessage === m.seq && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 4 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mt-2 rounded-[var(--cm-radius-xs)] border border-dashed border-[var(--cm-clay)]/40 bg-[var(--cm-bg-elevated)]/50 px-3 py-2"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
<div className="mb-1 text-[9px] uppercase tracking-wider text-[var(--cm-clay)]">
|
||||||
|
broker sees only this
|
||||||
|
</div>
|
||||||
|
<code className="block break-all text-[11px] text-[var(--cm-fg-tertiary)]">
|
||||||
|
{m.ciphertext}
|
||||||
|
</code>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.li>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</ol>
|
||||||
|
{/* progress bar */}
|
||||||
|
<div className="border-t border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/30">
|
||||||
|
<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>
|
||||||
|
{visible.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"}
|
||||||
|
{focusedPeer && ` · filtered`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* prevent eslint exhaustive-deps hook warning from dead var */}
|
||||||
|
{loopCount < -1 && <span />}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user