Skip to content

Commit f603cdc

Browse files
committed
fix: synchronize agent hero visuals
1 parent 17448d4 commit f603cdc

2 files changed

Lines changed: 397 additions & 27 deletions

File tree

src/App.jsx

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ const CLOUD = "https://onecomputer-openvtc.eastus2.cloudapp.azure.com";
3232
const DOCS = "/docs/";
3333

3434
const agentOptions = [
35-
{ id: "claude", name: "Claude", kind: "Desktop · Code", href: "https://claude.com/download" },
36-
{ id: "nanoclaw", name: "NanoClaw", kind: "Container-native", href: "https://github.com/qwibitai/nanoclaw" },
37-
{ id: "openclaw", name: "OpenClaw", kind: "Open agent platform", href: "https://github.com/openclaw/openclaw" },
38-
{ id: "codex", name: "Codex", kind: "Coding agent", href: "https://openai.com/codex/" },
39-
{ id: "hermes", name: "Hermes Agent", kind: "Open · extensible", href: "https://github.com/NousResearch/hermes-agent" },
35+
{ id: "claude", name: "Claude", mark: "C", kind: "Desktop · Code", command: "claude", badges: ["Claude Desktop", "Claude Code"], accent: "#d9946d", href: "https://claude.com/download" },
36+
{ id: "nanoclaw", name: "NanoClaw", mark: "N", kind: "Container-native", command: "nanoclaw", badges: ["NanoClaw runtime", "Container-native"], accent: "#d3b75e", href: "https://github.com/qwibitai/nanoclaw" },
37+
{ id: "openclaw", name: "OpenClaw", mark: "O", kind: "Open agent platform", command: "openclaw gateway", badges: ["OpenClaw gateway", "Channel bridge"], accent: "#c976e4", href: "https://github.com/openclaw/openclaw" },
38+
{ id: "codex", name: "Codex", mark: "X", kind: "Coding agent", command: "codex", badges: ["Codex workspace", "Terminal agent"], accent: "#8bd8b0", href: "https://openai.com/codex/" },
39+
{ id: "hermes", name: "Hermes Agent", mark: "H", kind: "Open · extensible", command: "hermes", badges: ["Hermes Agent", "Tool runtime"], accent: "#8fb6ed", href: "https://github.com/NousResearch/hermes-agent" },
4040
];
4141

4242
const productSteps = [
@@ -126,30 +126,34 @@ const reveal = {
126126
}),
127127
};
128128

129-
function AgentRotator({ compact = false }) {
129+
function useAgentCycle(interval = 4200, paused = false) {
130130
const reduceMotion = useReducedMotion();
131131
const [active, setActive] = useState(0);
132132

133133
useEffect(() => {
134-
if (reduceMotion) return undefined;
134+
if (reduceMotion || paused) return undefined;
135135
const timer = window.setInterval(
136136
() => setActive((current) => (current + 1) % agentOptions.length),
137-
2700,
137+
interval,
138138
);
139139
return () => window.clearInterval(timer);
140-
}, [reduceMotion]);
140+
}, [active, interval, paused, reduceMotion]);
141+
142+
return { active, setActive, agent: agentOptions[active] };
143+
}
141144

142-
const agent = agentOptions[active];
145+
function AgentName({ agent, compact = false }) {
146+
const reduceMotion = useReducedMotion();
143147

144148
return (
145149
<span className={`agent-rotator ${compact ? "agent-rotator-compact" : ""}`} aria-live="polite">
146-
<AnimatePresence initial={false} mode="wait">
150+
<AnimatePresence initial={false}>
147151
<motion.span
148152
key={agent.id}
149-
initial={{ opacity: 0, y: 14, filter: "blur(8px)" }}
153+
initial={{ opacity: 0, y: 12, filter: "blur(7px)" }}
150154
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
151-
exit={{ opacity: 0, y: -14, filter: "blur(8px)" }}
152-
transition={{ duration: reduceMotion ? 0 : 0.38, ease: [0.22, 1, 0.36, 1] }}
155+
exit={{ opacity: 0, y: -12, filter: "blur(7px)" }}
156+
transition={{ duration: reduceMotion ? 0 : 0.42, ease: [0.22, 1, 0.36, 1] }}
153157
>
154158
{agent.name}
155159
</motion.span>
@@ -158,6 +162,14 @@ function AgentRotator({ compact = false }) {
158162
);
159163
}
160164

165+
function AgentRotator({ compact = false }) {
166+
const { agent } = useAgentCycle(3200);
167+
168+
return (
169+
<AgentName agent={agent} compact={compact} />
170+
);
171+
}
172+
161173
function Logo() {
162174
return (
163175
<span className="logo-lockup">
@@ -184,10 +196,13 @@ function Reveal({ children, className = "", delay = 0 }) {
184196
);
185197
}
186198

187-
function ProductCapture({ step, hero = false }) {
199+
function ProductCapture({ step, hero = false, agent = null }) {
200+
const workspaceBadges = agent?.badges || ["Claude Desktop", "Claude Code"];
201+
188202
return (
189203
<motion.div
190204
className={`capture capture-${step.kind} ${hero ? "capture-hero" : ""}`}
205+
style={agent ? { "--agent-accent": agent.accent } : undefined}
191206
initial={{ opacity: 0, scale: 0.975, y: 16 }}
192207
animate={{ opacity: 1, scale: 1, y: 0 }}
193208
exit={{ opacity: 0, scale: 0.985, y: -12 }}
@@ -207,11 +222,53 @@ function ProductCapture({ step, hero = false }) {
207222
<div className="capture-viewport">
208223
<img src={step.image} alt={step.alt} loading={hero ? "eager" : "lazy"} />
209224
{step.kind === "workspace" && (
210-
<div className="workspace-badges" aria-hidden="true">
211-
<span><Monitor size={13} /> Claude Desktop</span>
212-
<span><Cpu size={13} /> Claude Code</span>
213-
<span className="workspace-badge-live"><i /> Isolated sandbox</span>
214-
</div>
225+
<AnimatePresence initial={false}>
226+
<motion.div
227+
className="workspace-badges"
228+
key={agent?.id || "claude"}
229+
aria-hidden="true"
230+
initial={{ opacity: 0, y: -8 }}
231+
animate={{ opacity: 1, y: 0 }}
232+
exit={{ opacity: 0, y: 8 }}
233+
transition={{ duration: 0.32 }}
234+
>
235+
<span><Monitor size={13} /> {workspaceBadges[0]}</span>
236+
<span><Cpu size={13} /> {workspaceBadges[1]}</span>
237+
<span className="workspace-badge-live"><i /> Isolated sandbox</span>
238+
</motion.div>
239+
</AnimatePresence>
240+
)}
241+
{hero && agent && (
242+
<AnimatePresence initial={false}>
243+
<motion.div
244+
className="agent-runtime-layer"
245+
key={agent.id}
246+
initial={{ opacity: 0 }}
247+
animate={{ opacity: 1 }}
248+
exit={{ opacity: 0 }}
249+
transition={{ duration: 0.38 }}
250+
>
251+
<div className="agent-visual-tint" />
252+
<motion.div
253+
className="agent-runtime-card"
254+
initial={{ opacity: 0, y: 14, scale: 0.97 }}
255+
animate={{ opacity: 1, y: 0, scale: 1 }}
256+
exit={{ opacity: 0, y: -10, scale: 0.98 }}
257+
transition={{ duration: 0.42, ease: [0.22, 1, 0.36, 1] }}
258+
>
259+
<div className="agent-runtime-top">
260+
<span className="agent-runtime-mark">{agent.mark}</span>
261+
<span className="agent-runtime-title"><small>Connected agent</small><strong>{agent.name}</strong></span>
262+
<span className="agent-runtime-ready"><i /> Ready</span>
263+
</div>
264+
<div className="agent-runtime-command"><small>$ onecomputer agent attach</small><code>{agent.command}</code></div>
265+
<div className="agent-runtime-foot">
266+
<span><ShieldCheck size={12} /> Policy-bound</span>
267+
<span><LockKeyhole size={12} /> Isolated</span>
268+
</div>
269+
</motion.div>
270+
</motion.div>
271+
</AnimatePresence>
215272
)}
216273
<div className="capture-redactions" aria-hidden="true">
217274
{step.kind === "ciso" && (
@@ -276,6 +333,8 @@ function Nav({ progress, page = "home" }) {
276333

277334
function Hero() {
278335
const heroRef = useRef(null);
336+
const [rotationPaused, setRotationPaused] = useState(false);
337+
const { active: activeAgentIndex, setActive: setActiveAgentIndex, agent: activeAgent } = useAgentCycle(4400, rotationPaused);
279338
const { scrollYProgress } = useScroll({
280339
target: heroRef,
281340
offset: ["start start", "end start"],
@@ -303,7 +362,11 @@ function Hero() {
303362
animate={{ opacity: 1, y: 0 }}
304363
transition={{ delay: 0.25, duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
305364
>
306-
A safe computer for <span><AgentRotator /></span>
365+
<span className="hero-headline-static">A safe computer</span>
366+
<span className="hero-agent-line">
367+
<span className="hero-headline-for">for</span>
368+
<span className="hero-agent-stage" style={{ "--agent-accent": activeAgent.accent }}><AgentName agent={activeAgent} /></span>
369+
</span>
307370
</motion.h1>
308371
<motion.p
309372
className="hero-lede"
@@ -343,12 +406,16 @@ function Hero() {
343406

344407
<motion.div
345408
className="hero-product"
409+
onMouseEnter={() => setRotationPaused(true)}
410+
onMouseLeave={() => setRotationPaused(false)}
411+
onFocusCapture={() => setRotationPaused(true)}
412+
onBlurCapture={() => setRotationPaused(false)}
346413
style={{ y: visualY, scale: visualScale }}
347414
initial={{ opacity: 0, y: 45, rotateX: 6 }}
348415
animate={{ opacity: 1, y: 0, rotateX: 0 }}
349416
transition={{ delay: 0.35, duration: 1, ease: [0.22, 1, 0.36, 1] }}
350417
>
351-
<ProductCapture step={productSteps[0]} hero />
418+
<ProductCapture step={productSteps[0]} hero agent={activeAgent} />
352419
<motion.div
353420
className="floating-proof proof-policy"
354421
animate={{ y: [0, -7, 0] }}
@@ -369,6 +436,26 @@ function Hero() {
369436
Company policy <b>connected</b>
370437
</span>
371438
</motion.div>
439+
<div className="hero-agent-dock" aria-label="Choose an agent runtime">
440+
<span className="hero-agent-dock-label">Runtime</span>
441+
{agentOptions.map((agent, index) => (
442+
<motion.button
443+
type="button"
444+
key={agent.id}
445+
className={index === activeAgentIndex ? "is-active" : ""}
446+
style={{ "--agent-accent": agent.accent }}
447+
aria-label={`Show ${agent.name} workspace`}
448+
aria-pressed={index === activeAgentIndex}
449+
title={agent.name}
450+
onClick={() => setActiveAgentIndex(index)}
451+
whileHover={{ y: -2 }}
452+
whileTap={{ scale: 0.94 }}
453+
>
454+
<span>{agent.mark}</span>
455+
<small>{agent.name}</small>
456+
</motion.button>
457+
))}
458+
</div>
372459
</motion.div>
373460
</div>
374461

0 commit comments

Comments
 (0)