diff --git a/frontend/src/landing/app/landing/page.tsx b/frontend/src/landing/app/landing/page.tsx index a9eb619d..a03d70a9 100644 --- a/frontend/src/landing/app/landing/page.tsx +++ b/frontend/src/landing/app/landing/page.tsx @@ -2,7 +2,6 @@ import { LandingNav } from "../../components/LandingNav"; import { LandingHero } from "../../components/LandingHero"; import { LandingAbout } from "../../components/LandingAbout"; import { LandingAgentsBar } from "../../components/LandingAgentsBar"; -import { LandingStats } from "../../components/LandingStats"; import { LandingVideo } from "../../components/LandingVideo"; import { LandingFeatures } from "../../components/LandingFeatures"; import { LandingWorkflow } from "../../components/LandingWorkflow"; @@ -10,7 +9,6 @@ import { LandingUseCases } from "../../components/LandingUseCases"; import { LandingTestimonials } from "../../components/LandingTestimonials"; import { LandingHowItWorks } from "../../components/LandingHowItWorks"; import { LandingQuickStart } from "../../components/LandingQuickStart"; -import { LandingCTA } from "../../components/LandingCTA"; import { ScrollRevealProvider } from "../../components/ScrollRevealProvider"; import { PageConstellation } from "../../components/PageConstellation"; import { formatCompactNumber, getGitHubRepoStats } from "../../lib/github-repo"; @@ -31,10 +29,8 @@ export default async function LandingPage() {
-
-
MIT Licensed · Open Source
diff --git a/frontend/src/landing/components/LandingAbout.tsx b/frontend/src/landing/components/LandingAbout.tsx index b59c02f4..b2af21af 100644 --- a/frontend/src/landing/components/LandingAbout.tsx +++ b/frontend/src/landing/components/LandingAbout.tsx @@ -1,51 +1,167 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +// The "before": scattered, faintly drifting session cards — the pain in the +// headline made visual. Same dark-card vocabulary as the rest of the site, so +// it reads as restless work, not clip-art browser tabs. +type Tone = "fail" | "warn" | "idle"; + +const chaos: { + tab: string; + line: string; + tone: Tone; + rot: string; + x: string; + y: string; + z: number; + dur: string; + delay: string; +}[] = [ + { tab: "tab · #44 ci", line: "tests/auth ✗", tone: "fail", rot: "-4deg", x: "2%", y: "4%", z: 5, dur: "5.6s", delay: "0s" }, + { tab: "tab · #42 pr", line: "review pending", tone: "warn", rot: "3deg", x: "44%", y: "0%", z: 6, dur: "6.4s", delay: "0.7s" }, + { tab: "tab · #46 merge", line: "merge conflict", tone: "fail", rot: "-2deg", x: "14%", y: "35%", z: 7, dur: "5.9s", delay: "1.2s" }, + { tab: "tab · #43 api", line: "rate-limited", tone: "idle", rot: "5deg", x: "56%", y: "41%", z: 4, dur: "6.7s", delay: "0.3s" }, + { tab: "tab · logs", line: "copy-pasting…", tone: "idle", rot: "-3deg", x: "4%", y: "66%", z: 6, dur: "5.3s", delay: "0.9s" }, + { tab: "tab · #51 ci", line: "queued", tone: "warn", rot: "2deg", x: "46%", y: "68%", z: 5, dur: "6.1s", delay: "1.5s" }, +]; + +const toneDot: Record = { + fail: "rgba(248,113,113,0.85)", + warn: "rgba(251,191,36,0.8)", + idle: "rgba(168,162,158,0.5)", +}; + +const config: [string, string][] = [ + ["agent", "claude-code"], + ["tracker", "github"], + ["workspace", "worktree"], + ["runtime", "tmux"], + ["notifier", "slack"], +]; + export function LandingAbout() { + // Reveal the calm config line-by-line once the section scrolls into view — + // a deliberate assembling, never a typewriter. + const ref = useRef(null); + const [shown, setShown] = useState(false); + useEffect(() => { + const el = ref.current; + if (!el) return; + const obs = new IntersectionObserver( + ([e]) => { + if (e.isIntersecting) { + setShown(true); + obs.disconnect(); + } + }, + { threshold: 0.4 }, + ); + obs.observe(el); + return () => obs.disconnect(); + }, []); + return (
-
+
The problem
-

+

You're running AI agents in 10 browser tabs.{" "} Checking if PRs landed. Re-running failed CI. Copy-pasting error logs.

-
-

- Agent Orchestrator replaces that with one YAML file. Point it at - your GitHub issues, pick your agents, and walk away. Each agent - spawns in its own git worktree, creates PRs, fixes CI failures, - addresses review comments, and moves toward merge. If you are new, start with the docs quickstart and configuration guides. -

- - {/* Config preview — show how simple setup is */} -
-
-
-
-
- - agent-orchestrator.yaml - +
+ {/* before — the drifting chaos pile */} + + + {/* after — one still YAML file + the resolution copy */} +
+
+
+
+
+
+ + agent-orchestrator.yaml + +
+
+ {config.map(([k, v], i) => ( +
+ {k}:{" "} + {v} +
+ ))} +
-
-              agent:{" "}
-              claude-code
-              {"\n"}
-              tracker:{" "}
-              github
-              {"\n"}
-              workspace:{" "}
-              worktree
-              {"\n"}
-              runtime:{" "}
-              tmux
-              {"\n"}
-              notifier:{" "}
-              slack
-            
+ +

+ Agent Orchestrator replaces that with one YAML file. Point it at your + GitHub issues, pick your agents, and walk away. Each agent spawns in + its own git worktree, creates PRs, fixes CI failures, addresses review + comments, and moves toward merge. If you are new, start with the{" "} + + docs quickstart and configuration guides + + . +

diff --git a/frontend/src/landing/components/LandingAgentsBar.tsx b/frontend/src/landing/components/LandingAgentsBar.tsx index 1dcd5242..3a49dc73 100644 --- a/frontend/src/landing/components/LandingAgentsBar.tsx +++ b/frontend/src/landing/components/LandingAgentsBar.tsx @@ -28,21 +28,21 @@ const agents = [ export function LandingAgentsBar() { return ( -
-
+
+
Works with your favorite AI agents
-
+
{agents.map((agent) => ( -
- {agent.alt} -
- {agent.name} +
+
+ {agent.alt}
+
{agent.name}
))}
diff --git a/frontend/src/landing/components/LandingCTA.tsx b/frontend/src/landing/components/LandingCTA.tsx deleted file mode 100644 index d1b2c3df..00000000 --- a/frontend/src/landing/components/LandingCTA.tsx +++ /dev/null @@ -1,33 +0,0 @@ -export function LandingCTA() { - return ( -
-
-

- Stop babysitting. -

-

- Start orchestrating. -

-
- $ npm i -g @aoagents/ao -
- -
-
- ); -} diff --git a/frontend/src/landing/components/LandingFeatures.tsx b/frontend/src/landing/components/LandingFeatures.tsx index a70530f1..3e6a9c59 100644 --- a/frontend/src/landing/components/LandingFeatures.tsx +++ b/frontend/src/landing/components/LandingFeatures.tsx @@ -2,56 +2,61 @@ import { useEffect, useRef, useState } from "react"; -type DemoKind = "parallel" | "recovery" | "plugins" | "dashboard"; +type Asset = + | { type: "image"; src: string; w: number; h: number } + | { type: "video"; src: string; poster?: string; w: number; h: number }; -const features: { n: string; title: string; desc: string; demo: DemoKind }[] = [ +// A feature's product surface is either a real capture (image/video dropped +// into /public/features) or an in-frame illustration for concept features +// whose UI isn't shippable as a screenshot yet. +type Surface = + | { kind: "capture"; asset: Asset } + | { kind: "render"; id: "parallel" | "recovery" | "slots" }; + +type Feature = { + n: string; + title: string; + desc: string; + frameTitle: string; + cta?: string; + surface: Surface; +}; + +const features: Feature[] = [ { n: "01", title: "Multi-agent execution", desc: "Run Claude Code, Codex, Cursor, Aider, and OpenCode in parallel. Each agent in its own git worktree, branch, and context.", - demo: "parallel", + frameTitle: "reverbcode · sessions", + cta: "npx @aoagents/ao start", + surface: { kind: "render", id: "parallel" }, }, { n: "02", title: "Autonomous CI + review handling", desc: "CI fails? The agent reads the logs and pushes a fix. Review comments land? The agent addresses them. You sleep, your agents ship.", - demo: "recovery", + frameTitle: "reverbcode · s-312", + surface: { kind: "render", id: "recovery" }, }, { n: "03", title: "Seven swappable slots", desc: "Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal. Use tmux or process. GitHub or GitLab. Slack or webhooks.", - demo: "plugins", + frameTitle: "agent-orchestrator.yaml", + surface: { kind: "render", id: "slots" }, }, { n: "04", title: "Real-time Kanban + terminal", desc: "Every agent's state in one view. Attach to any terminal via the browser. SSE updates every 5 seconds. WebSocket for live I/O.", - demo: "dashboard", + frameTitle: "reverbcode · live", + surface: { + kind: "capture", + asset: { type: "video", src: "/features/live.webm", poster: "/features/live.png", w: 1280, h: 800 }, + }, }, ]; -// The feature's animated demo — the stacked back panel + a smaller front peek, -// reused as-is from the original switcher so each card stays rich. -function FeatureDemo({ kind }: { kind: DemoKind }) { - return ( -
-
- {kind === "parallel" && } - {kind === "recovery" && } - {kind === "plugins" && } - {kind === "dashboard" && } -
-
- {kind === "parallel" && } - {kind === "recovery" && } - {kind === "plugins" && } - {kind === "dashboard" && } -
-
- ); -} - // Sticky offset from the top of the viewport where each card pins (leaves room // for the fixed nav); each successive card pins STACK_GAP lower so the tops peek. const BASE_TOP = 120; @@ -113,7 +118,7 @@ export function LandingFeatures() { }, [stack]); return ( -
+
Features @@ -133,81 +138,196 @@ export function LandingFeatures() {
- {features.map((f, i) => ( -
{ - cardRefs.current[i] = el; - }} - className="landing-card rounded-2xl grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-center overflow-hidden" - style={{ - padding: "clamp(1.5rem, 3vw, 2.5rem)", - marginBottom: "1.5rem", - transformOrigin: "center top", - transition: "transform 0.4s ease, opacity 0.4s ease, border-color 0.2s ease", - ...(stack - ? { position: "sticky", top: `${BASE_TOP + i * STACK_GAP}px`, zIndex: i + 1 } - : null), - }} - > -
-
- {f.n} + {features.map((f, i) => { + const flip = i % 2 === 1; // alternate product/text sides down the deck + return ( +
{ + cardRefs.current[i] = el; + }} + className="landing-card rounded-2xl overflow-hidden" + style={{ + padding: "clamp(1.5rem, 3vw, 2.5rem)", + marginBottom: "1.5rem", + transformOrigin: "center top", + transition: "transform 0.4s ease, opacity 0.4s ease, border-color 0.2s ease", + ...(stack + ? { position: "sticky", top: `${BASE_TOP + i * STACK_GAP}px`, zIndex: i + 1 } + : null), + }} + > +
+
+
+ {f.n} +
+

+ {f.title} +

+

+ {f.desc} +

+ {f.cta && ( +
+ +
+ )} +
+
+ +
-

- {f.title} -

-

- {f.desc} -

- -
- ))} + ); + })}
); } -/* ──────── 01 · Parallel ──────── */ +// macOS-style window chrome around the product surface — the universal "this is +// real software" cue. Same chrome for captures and illustrations so all four +// cards read as one coherent product. +function ProductFrame({ feature }: { feature: Feature }) { + const s = feature.surface; + const ratio = s.kind === "capture" ? `${s.asset.w} / ${s.asset.h}` : "1280 / 800"; + return ( +
+
+ + + + + + + {feature.frameTitle} + +
+
+ {s.kind === "capture" ? ( + + ) : s.id === "parallel" ? ( + + ) : s.id === "recovery" ? ( + + ) : ( + + )} +
+
+ ); +} + +// Renders the real asset; falls back to a labelled placeholder if it 404s, so +// dropping a file into /public/features just works with no code change. +function ProductMedia({ asset, title }: { asset: Asset; title: string }) { + const [failed, setFailed] = useState(false); -function ParallelBack() { - const agents = [ - { name: "claude-code", task: "#42 auth", color: "rgba(255,159,102,0.7)", dur: 3.4, delay: 0 }, - { name: "codex", task: "#43 pagination", color: "rgba(134,239,172,0.65)", dur: 4.2, delay: 0.5 }, - { name: "aider", task: "#44 rate limit", color: "rgba(167,139,250,0.65)", dur: 3.6, delay: 1.0 }, - { name: "opencode", task: "#46 db refactor", color: "rgba(96,165,250,0.65)", dur: 4.8, delay: 0.3 }, - ]; + if (failed) { + const name = asset.src.split("/").pop(); + return ( +
+
{name}
+
+ {asset.w}×{asset.h} + {asset.type === "video" ? " · loop" : ""} +
+
+ ); + } + + if (asset.type === "video") { + return ( +