diff --git a/frontend/src/landing/components/LandingAbout.tsx b/frontend/src/landing/components/LandingAbout.tsx new file mode 100644 index 000000000..b59c02f44 --- /dev/null +++ b/frontend/src/landing/components/LandingAbout.tsx @@ -0,0 +1,54 @@ +export function LandingAbout() { + 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 + +
+
+              agent:{" "}
+              claude-code
+              {"\n"}
+              tracker:{" "}
+              github
+              {"\n"}
+              workspace:{" "}
+              worktree
+              {"\n"}
+              runtime:{" "}
+              tmux
+              {"\n"}
+              notifier:{" "}
+              slack
+            
+
+
+
+
+ ); +} diff --git a/frontend/src/landing/components/LandingAgentsBar.tsx b/frontend/src/landing/components/LandingAgentsBar.tsx new file mode 100644 index 000000000..1dcd52428 --- /dev/null +++ b/frontend/src/landing/components/LandingAgentsBar.tsx @@ -0,0 +1,51 @@ +const agents = [ + { + name: "Claude Code", + src: "/docs/logos/claude-code.svg", + alt: "Anthropic", + }, + { + name: "Codex", + src: "/docs/logos/codex.svg", + alt: "OpenAI", + }, + { + name: "Cursor", + src: "/docs/logos/cursor.svg", + alt: "Cursor", + }, + { + name: "Aider", + src: "https://aider.chat/assets/logo.svg", + alt: "Aider", + }, + { + name: "OpenCode", + src: "/docs/logos/opencode.svg", + alt: "OpenCode", + }, +]; + +export function LandingAgentsBar() { + return ( +
+
+ Works with your favorite AI agents +
+
+ {agents.map((agent) => ( +
+ {agent.alt} +
+ {agent.name} +
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/landing/components/LandingCTA.tsx b/frontend/src/landing/components/LandingCTA.tsx new file mode 100644 index 000000000..d1b2c3dfe --- /dev/null +++ b/frontend/src/landing/components/LandingCTA.tsx @@ -0,0 +1,33 @@ +export function LandingCTA() { + return ( +
+
+

+ Stop babysitting. +

+

+ Start orchestrating. +

+
+ $ npm i -g @aoagents/ao +
+
+ + Read Docs + + + View on GitHub + +
+
+
+ ); +} diff --git a/frontend/src/landing/components/LandingDifferentiators.tsx b/frontend/src/landing/components/LandingDifferentiators.tsx new file mode 100644 index 000000000..d104b0b8d --- /dev/null +++ b/frontend/src/landing/components/LandingDifferentiators.tsx @@ -0,0 +1,64 @@ +const rows = [ + { feature: "Web-based dashboard", others: "Native Mac apps only" }, + { feature: "Open source (MIT)", others: "Closed source" }, + { feature: "Multi-agent (Claude, Codex, Aider, OpenCode)", others: "Single agent" }, + { feature: "Auto CI failure recovery", others: "Manual" }, + { feature: "Plugin architecture (7 slots)", others: "Fixed integrations" }, + { feature: "Git worktree isolation", others: "Shared workspace" }, +]; + +export function LandingDifferentiators() { + return ( +
+
+
+ Why Agent Orchestrator +
+

+ The only{" "} + open-source, web-based{" "} + agent orchestrator +

+

+ Conductor, T3 Code, and Codex App are native Mac apps. AO runs in + your browser, works on any OS, and you can self-host or extend it. +

+
+
+ + + + + + + + + + {rows.map((row, i) => ( + + + + + + ))} + +
+ Feature + + AO + + Others +
+ {row.feature} + + ✓ + + {row.others} +
+
+
+ ); +} diff --git a/frontend/src/landing/components/LandingFeatures.tsx b/frontend/src/landing/components/LandingFeatures.tsx new file mode 100644 index 000000000..a70530f15 --- /dev/null +++ b/frontend/src/landing/components/LandingFeatures.tsx @@ -0,0 +1,560 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +type DemoKind = "parallel" | "recovery" | "plugins" | "dashboard"; + +const features: { n: string; title: string; desc: string; demo: DemoKind }[] = [ + { + 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", + }, + { + 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", + }, + { + 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", + }, + { + 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", + }, +]; + +// 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; +const STACK_GAP = 26; + +export function LandingFeatures() { + const cardRefs = useRef<(HTMLDivElement | null)[]>([]); + const [stack, setStack] = useState(false); + + // Scroll-stack only on desktop; on narrow screens cards read as a plain list. + useEffect(() => { + const mq = window.matchMedia("(min-width: 768px)"); + const apply = () => setStack(mq.matches); + apply(); + mq.addEventListener("change", apply); + return () => mq.removeEventListener("change", apply); + }, []); + + // As later cards pin on top, shrink + dim the cards beneath them so the deck + // reads as a stack. CSS transition smooths the steps; rAF throttles scroll. + useEffect(() => { + const els = cardRefs.current; + if (!stack) { + els.forEach((el) => { + if (el) { + el.style.transform = ""; + el.style.opacity = ""; + } + }); + return; + } + let raf = 0; + const update = () => { + raf = 0; + els.forEach((el, i) => { + if (!el) return; + let depth = 0; + for (let j = i + 1; j < els.length; j++) { + const ej = els[j]; + if (ej && ej.getBoundingClientRect().top <= BASE_TOP + j * STACK_GAP + 0.5) { + depth += 1; + } + } + el.style.transform = `scale(${1 - depth * 0.05})`; + el.style.opacity = `${Math.max(1 - depth * 0.16, 0.55)}`; + }); + }; + const onScroll = () => { + if (!raf) raf = requestAnimationFrame(update); + }; + update(); + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("resize", onScroll); + return () => { + window.removeEventListener("scroll", onScroll); + window.removeEventListener("resize", onScroll); + if (raf) cancelAnimationFrame(raf); + }; + }, [stack]); + + return ( +
+
+ + Features + +
+ +

+ A unified orchestrator that scales. +

+ +
+ {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} +
+

+ {f.title} +

+

+ {f.desc} +

+
+ +
+ ))} +
+
+ ); +} + +/* ──────── 01 · Parallel ──────── */ + +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 }, + ]; + return ( +
+
+ + 4 sessions · parallel + + + + live + +
+
+ {agents.map((a) => ( +
+
+ + + {a.name} + +
+
+ {a.task} +
+
+
+
+
+ ))} +
+
+ ); +} + +function ParallelFront() { + const fleet = [ + { name: "claude-code", color: "rgba(255,159,102,0.85)" }, + { name: "codex", color: "rgba(134,239,172,0.75)" }, + { name: "aider", color: "rgba(167,139,250,0.75)" }, + { name: "opencode", color: "rgba(96,165,250,0.75)" }, + { name: "cursor", color: "rgba(244,114,182,0.65)" }, + ]; + return ( +
+
+ Fleet · 5 agents +
+
+ {fleet.map((a) => ( +
+ + + {a.name} + +
+ ))} +
+
+ ); +} + +/* ──────── 02 · Recovery ──────── */ + +const recoveryStages: { time: string; text: string; kind: "info" | "fail" | "fix" | "ok" }[] = [ + { time: "10:42", text: "agent.spawn → s-312", kind: "info" }, + { time: "10:43", text: "✗ tests/auth failed", kind: "fail" }, + { time: "10:44", text: "agent.investigate()", kind: "info" }, + { time: "10:44", text: "patch · re-running ci", kind: "fix" }, + { time: "10:45", text: "✓ tests/auth (48/48)", kind: "ok" }, + { time: "10:45", text: "✗ lint failed", kind: "fail" }, + { time: "10:46", text: "patch · eslint --fix", kind: "fix" }, + { time: "10:47", text: "✓ lint passed", kind: "ok" }, + { time: "10:47", text: "● ready to merge", kind: "ok" }, +]; + +function RecoveryBack() { + const [count, setCount] = useState(3); + useEffect(() => { + const id = setInterval(() => { + setCount((c) => (c >= recoveryStages.length ? 3 : c + 1)); + }, 1000); + return () => clearInterval(id); + }, []); + const visible = recoveryStages.slice(0, count); + return ( +
+
+ PR #312 · feat/user-auth + + healing + +
+
+ {visible.map((s, i) => { + const isLast = i === visible.length - 1; + const color = + s.kind === "fail" + ? "text-[rgba(248,113,113,0.85)]" + : s.kind === "ok" + ? "text-[rgba(134,239,172,0.85)]" + : s.kind === "fix" + ? "text-[rgba(251,191,36,0.85)]" + : "text-[var(--landing-muted)]"; + return ( +
+ + {s.time} + + {s.text} +
+ ); + })} +
+
+ ); +} + +function RecoveryFront() { + return ( +
+
+ + before + + + 12/48 +
+
+ + after + + + 48/48 +
+
+ ); +} + +/* ──────── 03 · Plugins ──────── */ + +function PluginsBack() { + const slots = [ + { slot: "agent", values: ["claude-code", "codex", "aider", "opencode"] }, + { slot: "tracker", values: ["github", "linear", "gitlab"] }, + { slot: "runtime", values: ["tmux", "process"] }, + { slot: "workspace", values: ["worktree", "clone"] }, + { slot: "scm", values: ["github", "gitlab"] }, + { slot: "notifier", values: ["slack", "webhook", "desktop"] }, + { slot: "terminal", values: ["iterm2", "web"] }, + ]; + const [tick, setTick] = useState(0); + useEffect(() => { + const id = setInterval(() => setTick((t) => t + 1), 1600); + return () => clearInterval(id); + }, []); + return ( +
+
+ + agent-orchestrator.yaml + + + 7 slots + +
+
+ {slots.map((s, i) => { + const val = s.values[(tick + i) % s.values.length]; + return ( +
+ + {s.slot}: + + + {val} + +
+ ); + })} +
+
+ ); +} + +function PluginsFront() { + const pairs = [ + { from: "tmux", to: "process" }, + { from: "github", to: "linear" }, + { from: "slack", to: "webhook" }, + { from: "worktree", to: "clone" }, + ]; + const [idx, setIdx] = useState(0); + useEffect(() => { + const id = setInterval(() => setIdx((i) => (i + 1) % pairs.length), 1800); + return () => clearInterval(id); + }, []); + const p = pairs[idx]; + return ( +
+ + swap + +
+ + {p.from} + + + + {p.to} + +
+
+ ); +} + +/* ──────── 04 · Dashboard ──────── */ + +type KanbanCard = { + id: number; + col: 0 | 1 | 2; + title: string; + agent: string; + color: string; +}; + +function DashboardBack() { + const [cards, setCards] = useState([ + { id: 1, col: 0, title: "Add user auth", agent: "claude-code", color: "rgba(255,159,102,0.7)" }, + { id: 2, col: 0, title: "Fix pagination", agent: "codex", color: "rgba(134,239,172,0.65)" }, + { id: 3, col: 1, title: "Add rate limit", agent: "aider", color: "rgba(167,139,250,0.65)" }, + { id: 4, col: 2, title: "Refactor DB", agent: "opencode", color: "rgba(96,165,250,0.65)" }, + ]); + useEffect(() => { + const id = setInterval(() => { + setCards((prev) => { + const advanceable = prev.filter((c) => c.col < 2); + if (advanceable.length === 0) { + return prev.map((c) => ({ ...c, col: 0 as 0 | 1 | 2 })); + } + const oldest = advanceable[0]; + return prev.map((c) => + c.id === oldest.id ? { ...c, col: (c.col + 1) as 0 | 1 | 2 } : c, + ); + }); + }, 2400); + return () => clearInterval(id); + }, []); + const cols = ["Working", "Review", "Merged"]; + return ( +
+
+ + my-saas-app · 4 sessions + + + + sse + +
+
+ {cols.map((name, col) => ( +
+
+ {name} +
+ {cards + .filter((c) => c.col === col) + .map((c) => ( +
+
{c.title}
+
+ + + {c.agent} + +
+
+ ))} +
+ ))} +
+
+ ); +} + +const streamPool = [ + "tests/auth.py::test_login", + "tests/api.py::test_pagination", + "tests/db.py::test_migration", + "tests/queue.py::test_dequeue", + "tests/auth.py::test_logout", + "tests/api.py::test_cursor", + "tests/db.py::test_index", + "tests/queue.py::test_retry", +]; + +function DashboardFront() { + const [stream, setStream] = useState(() => + streamPool.slice(0, 4).map((text, i) => ({ id: i, text, exiting: false })), + ); + const nextRef = useRef(4); + useEffect(() => { + const id = setInterval(() => { + setStream((prev) => { + const marked = prev.map((l, i) => (i === 0 ? { ...l, exiting: true } : l)); + const next = [ + ...marked, + { + id: nextRef.current, + text: streamPool[nextRef.current % streamPool.length], + exiting: false, + }, + ]; + nextRef.current += 1; + return next; + }); + setTimeout(() => { + setStream((prev) => prev.filter((l) => !l.exiting)); + }, 240); + }, 1300); + return () => clearInterval(id); + }, []); + return ( +
+
+ s-003 · attached + tail -f +
+
+ {stream.map((l) => ( +
+ {" "} + {l.text} +
+ ))} +
+
+ ); +} diff --git a/frontend/src/landing/components/LandingHero.tsx b/frontend/src/landing/components/LandingHero.tsx new file mode 100644 index 000000000..ab820a045 --- /dev/null +++ b/frontend/src/landing/components/LandingHero.tsx @@ -0,0 +1,102 @@ +interface LandingHeroProps { + starsLabel: string; +} + +export function LandingHero({ starsLabel }: LandingHeroProps) { + return ( +
+
+
+ + Open Source · MIT Licensed · {starsLabel} GitHub Stars +
+

+ Run 30 AI agents in parallel. +
+ One dashboard. +

+

+ Agent Orchestrator spawns Claude Code, Codex, Cursor, Aider, and OpenCode + in isolated git worktrees. Each agent gets its own branch, creates PRs, + fixes CI, and addresses reviews autonomously. +

+
+
+ $ npx @aoagents/ao start +
+ + Read Docs + + + View on GitHub + +
+ +
+
+ {/* Laptop screen / lid */} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Agent Orchestrator dashboard — live agent sessions flowing from work to review to merge +
+
+ {/* Laptop base / hinge */} +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/landing/components/LandingHowItWorks.tsx b/frontend/src/landing/components/LandingHowItWorks.tsx new file mode 100644 index 000000000..77f785a08 --- /dev/null +++ b/frontend/src/landing/components/LandingHowItWorks.tsx @@ -0,0 +1,316 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +const DURATION_MS = 3000; + +const steps = [ + { + n: "01", + title: "Configure & assign", + titleEm: "assign", + desc: "Point Agent Orchestrator at your repo with a YAML config. Choose your agent, set up trackers and notifiers. One file, full control.", + tags: ["YAML", "Plugins", "Trackers"], + kind: "cli" as const, + }, + { + n: "02", + title: "Agents work", + titleEm: "work", + desc: "Each agent spawns in an isolated worktree. They write code, create PRs, run tests, and fix failures. Monitor everything from the live dashboard, or let them run.", + tags: ["Worktrees", "Live dashboard", "Parallel"], + kind: "dashboard" as const, + }, + { + n: "03", + title: "PRs land", + titleEm: "land", + desc: "Agents create pull requests, address review comments, fix CI failures, and get them to mergeable state. Your morning starts with merged PRs, not a backlog.", + tags: ["Pull requests", "CI fixes", "Review"], + kind: "prs" as const, + }, +]; + +export function LandingHowItWorks() { + const [active, setActive] = useState(0); + const [progress, setProgress] = useState(0); + const [isDesktop, setIsDesktop] = useState(true); + const pausedRef = useRef(false); + const startRef = useRef(null); + + useEffect(() => { + const mq = window.matchMedia("(min-width: 768px)"); + const apply = () => setIsDesktop(mq.matches); + apply(); + mq.addEventListener("change", apply); + return () => mq.removeEventListener("change", apply); + }, []); + + useEffect(() => { + let raf = 0; + const tick = (now: number) => { + if (startRef.current === null) startRef.current = now; + if (!pausedRef.current) { + const p = Math.min((now - startRef.current) / DURATION_MS, 1); + setProgress(p); + if (p >= 1) { + startRef.current = now; + setActive((a) => (a + 1) % steps.length); + setProgress(0); + } + } else { + startRef.current = now - progress * DURATION_MS; + } + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [active]); + + const select = (i: number) => { + if (i === active) return; + startRef.current = null; + setProgress(0); + setActive(i); + }; + + return ( +
+
+
+ Process +
+

+ Three steps to{" "} + orchestration +

+
+ +
(pausedRef.current = true)} + onMouseLeave={() => (pausedRef.current = false)} + > + {steps.map((step, i) => { + const isActive = i === active; + return ( +
select(i)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + select(i); + } + }} + className="relative min-w-0 cursor-pointer overflow-hidden border-l border-[var(--landing-border-subtle)] pl-7 pr-5 py-2 first:border-l-0 first:pl-0 md:first:pl-7" + style={{ + flex: isDesktop + ? isActive + ? "1 1 0%" + : "0 1 15rem" + : "0 0 auto", + transition: "flex 0.6s cubic-bezier(0.22,1,0.36,1)", + }} + > + {/* Header — always visible */} +
+ {step.n} +
+

+ {step.title.replace(` ${step.titleEm}`, "")}{" "} + + {step.titleEm} + +

+ + {/* Expanding body */} +
+
+ {/* Vertical progress bar */} +
+
+
+ +
+

+ {step.desc} +

+
+ {step.kind === "cli" && } + {step.kind === "dashboard" && } + {step.kind === "prs" && } +
+
+ {step.tags.map((t, ti) => ( + + {ti > 0 && ·} + {t} + + ))} +
+
+
+
+
+ ); + })} +
+
+ ); +} + +function CliDemo() { + return ( +
+
+
+
+
+
+
+
+ ${" "} + ao batch-spawn 42 43 44 45 46 +
+
 
+
⟡ Loading config from agent-orchestrator.yaml
+
⟡ Resolving 5 issues from GitHub
+
⟡ Spawning sessions in worktrees...
+
✓ Session s-001 spawned → issue #42
+
✓ Session s-002 spawned → issue #43
+
✓ Session s-003 spawned → issue #44
+
✓ Session s-004 spawned → issue #45
+
✓ Session s-005 spawned → issue #46
+
 
+
+ + 5 agents working · Dashboard → http://localhost:3000 +
+
+
+ ); +} + +function DashboardDemo() { + return ( +
+
+
+
+
+ my-saas-app · 5 sessions +
+
+ + + + +
+
+ ); +} + +function PrsDemo() { + return ( +
+ {[ + { branch: "feat/user-auth", title: "Add user authentication flow" }, + { branch: "fix/pagination-offset", title: "Fix off-by-one in cursor pagination" }, + { branch: "feat/rate-limiting", title: "Add Redis-backed rate limiter" }, + { branch: "refactor/db-layer", title: "Extract repository pattern from services" }, + ].map((pr) => ( +
+
+
{pr.branch}
+
{pr.title}
+
+
+ ✓ Merged +
+
+ ))} +
+ ); +} + +interface DashCardData { + title: string; + meta: string; + agent: string; + amber?: boolean; + done?: boolean; +} + +function DashColumn({ title, cards }: { title: string; cards: DashCardData[] }) { + return ( +
+
+ {title} +
+ {cards.map((card) => ( +
+
{card.title}
+
{card.meta}
+
+ {card.done ? ( + + ) : ( + + )} + {card.agent} +
+
+ ))} +
+ ); +} diff --git a/frontend/src/landing/components/LandingNav.tsx b/frontend/src/landing/components/LandingNav.tsx new file mode 100644 index 000000000..b9ed3b2ee --- /dev/null +++ b/frontend/src/landing/components/LandingNav.tsx @@ -0,0 +1,85 @@ +"use client"; + +function XIcon() { + return ( + + ); +} + +function DiscordIcon() { + return ( + + ); +} + +function GithubIcon() { + return ( + + ); +} + +export function LandingNav() { + return ( + + ); +} diff --git a/frontend/src/landing/components/LandingQuickStart.tsx b/frontend/src/landing/components/LandingQuickStart.tsx new file mode 100644 index 000000000..cd8b60b4a --- /dev/null +++ b/frontend/src/landing/components/LandingQuickStart.tsx @@ -0,0 +1,44 @@ +const steps = [ + { num: "STEP 01", title: "Install", desc: "One command. No dependencies beyond Node.js.", cmd: "npm i -g @aoagents/ao" }, + { num: "STEP 02", title: "Configure", desc: "Create an agent-orchestrator.yaml. Pick your agents, tracker, and notifiers.", cmd: "ao start" }, + { num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" }, +]; + +export function LandingQuickStart() { + return ( +
+
+
+ Get started in 60 seconds +
+

+ Three commands to{" "} + launch +

+
+
+ {steps.map((s) => ( +
+
+ {s.num} +
+

+ {s.title} +

+

+ {s.desc} +

+
+ $ {s.cmd} +
+
+ ))} +
+ +
+ ); +} diff --git a/frontend/src/landing/components/LandingStats.tsx b/frontend/src/landing/components/LandingStats.tsx new file mode 100644 index 000000000..823ba517a --- /dev/null +++ b/frontend/src/landing/components/LandingStats.tsx @@ -0,0 +1,51 @@ +import type { GitHubRepoStats } from "@/lib/github-repo"; + +interface LandingStatsProps { + stats: GitHubRepoStats; +} + +export function LandingStats({ stats }: LandingStatsProps) { + const cards = [ + { number: stats.stars.toLocaleString(), label: "GitHub Stars" }, + { number: stats.forks.toLocaleString(), label: "Forks" }, + { number: stats.openIssues.toLocaleString(), label: "Open Issues" }, + { number: stats.watchers.toLocaleString(), label: "Watchers" }, + ]; + + return ( +
+
+ {cards.map((stat) => ( +
+
+ {stat.number} +
+
+ {stat.label} +
+
+ ))} +
+
+ + + {stats.stars.toLocaleString()} + stars on GitHub + +
+
+ + Built with itself — this repo is managed by Agent Orchestrator +
+
+
+ ); +} diff --git a/frontend/src/landing/components/LandingTestimonials.tsx b/frontend/src/landing/components/LandingTestimonials.tsx new file mode 100644 index 000000000..a555bbaeb --- /dev/null +++ b/frontend/src/landing/components/LandingTestimonials.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { useEffect, useState } from "react"; + +const testimonials = [ + { + quote: + "Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.", + img: "https://i.pravatar.cc/120?img=13", + name: "Staff Engineer", + role: "Series B Startup", + }, + { + quote: + "The auto CI recovery alone saves me hours a week. Agents fix their own broken tests. I just review and merge.", + img: "https://i.pravatar.cc/120?img=32", + name: "Solo Founder", + role: "Indie SaaS", + }, + { + quote: + "We went from 3 PRs/day to 15 PRs/day. The plugin system means we swapped in GitLab and Linear without changing our workflow.", + img: "https://i.pravatar.cc/120?img=8", + name: "Eng Lead", + role: "20-person team", + }, +]; + +const ROTATE_MS = 5500; + +export function LandingTestimonials() { + const [active, setActive] = useState(0); + const [show, setShow] = useState(true); + const [paused, setPaused] = useState(false); + + const change = (next: number) => { + if (next === active) return; + setShow(false); + window.setTimeout(() => { + setActive(next); + setShow(true); + }, 240); + }; + + useEffect(() => { + if (paused) return; + const t = window.setTimeout( + () => change((active + 1) % testimonials.length), + ROTATE_MS, + ); + return () => window.clearTimeout(t); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [active, paused]); + + const t = testimonials[active]; + + return ( +
+
+
+ What engineers say +
+

+ Trusted by builders +

+
+ +
setPaused(true)} + onMouseLeave={() => setPaused(false)} + > + {/* Quote — fades on change */} +
+
+ “{t.quote}” +
+
+ + {/* Bottom row — author cluster on the left, step counter on the right */} +
+
+
+ {testimonials.map((item, i) => { + const isActive = i === active; + const size = isActive ? 56 : 44; + return ( + + ); + })} +
+ + {/* Vertical divider */} +
+ + {/* Author — fades on change */} +
+
+ {t.name} +
+
+ {t.role} +
+
+
+ + {/* Step counter — fills the right side */} +
+ + {String(active + 1).padStart(2, "0")} + + + / {String(testimonials.length).padStart(2, "0")} + +
+
+
+
+ ); +} diff --git a/frontend/src/landing/components/LandingUseCases.tsx b/frontend/src/landing/components/LandingUseCases.tsx new file mode 100644 index 000000000..9107aa212 --- /dev/null +++ b/frontend/src/landing/components/LandingUseCases.tsx @@ -0,0 +1,235 @@ +"use client"; + +import { useEffect, useRef, type PointerEvent as ReactPointerEvent } from "react"; + +const dim = "text-[var(--landing-muted-dim)]"; +const fg = "text-[var(--landing-fg)]/80"; +const ok = "text-[rgba(134,239,172,0.8)]"; + +type UseCase = { + eyebrow: string; + title: string; + desc: string; + prefix: "$" | "⟡"; + cmd: string; + outcome: string; +}; + +// Real, grounded use cases — real ao commands, reaction keys, and lifecycle states. +const cases: UseCase[] = [ + { + eyebrow: "Backlog", + title: "Clear it overnight", + desc: "One agent per issue, each in its own git worktree, all running at once.", + prefix: "$", + cmd: "ao batch-spawn 142 143 144 145", + outcome: "4 worktrees · 4 PRs", + }, + { + eyebrow: "CI recovery", + title: "Self-healing builds", + desc: "A check goes red; the agent reads the logs, pushes a fix, and waits for green.", + prefix: "⟡", + cmd: "reaction · ci-failed", + outcome: "ci_failed → mergeable", + }, + { + eyebrow: "Review loop", + title: "Answers its own reviews", + desc: "Comments land; the agent addresses each one and re-requests review.", + prefix: "⟡", + cmd: "reaction · changes-requested", + outcome: "changes_requested → approved", + }, + { + eyebrow: "Migration", + title: "Grinds through the long ones", + desc: "Hand one agent a sweeping change and let it work file by file until tests pass.", + prefix: "$", + cmd: "ao spawn 305 --agent claude-code", + outcome: "23 files · tests green", + }, + { + eyebrow: "Per-role", + title: "Right model per job", + desc: "Claude Code orchestrates, Codex does the work. Pick the tool per task.", + prefix: "$", + cmd: "ao spawn 88 --agent codex", + outcome: "codex #88 · claude-code #91", + }, + { + eyebrow: "Multi-project", + title: "Every repo, one screen", + desc: "Register all your repos and supervise their agents from a single dashboard.", + prefix: "$", + cmd: "ao start", + outcome: "3 projects · one dashboard", + }, +]; + +const N = cases.length; +const THETA = 360 / N; +const RADIUS = 440; +const CARD_W = 360; +const CARD_H = 440; + +export function LandingUseCases() { + const viewportRef = useRef(null); + const ringRef = useRef(null); + const cardRefs = useRef<(HTMLDivElement | null)[]>([]); + + const angle = useRef(0); + const dragging = useRef(false); + const paused = useRef(false); + const reduced = useRef(false); + const start = useRef({ x: 0, a: 0 }); + + // rAF loop — rotate the ring and fade/scale each card by how far it faces the + // camera. Imperative (no setState) so 60fps stays smooth and re-render-free. + useEffect(() => { + reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + let raf = 0; + const loop = () => { + if (!dragging.current && !paused.current && !reduced.current) { + angle.current += 0.12; + } + const a = angle.current; + if (ringRef.current) { + ringRef.current.style.transform = `translateZ(-${RADIUS}px) rotateY(${a}deg)`; + } + cardRefs.current.forEach((el, i) => { + if (!el) return; + const facing = Math.cos(((i * THETA + a) * Math.PI) / 180); + const vis = Math.max(facing, 0); + el.style.opacity = `${0.2 + 0.8 * vis}`; + el.style.transform = `rotateY(${i * THETA}deg) translateZ(${RADIUS}px) scale(${0.9 + 0.1 * vis})`; + }); + raf = requestAnimationFrame(loop); + }; + raf = requestAnimationFrame(loop); + return () => cancelAnimationFrame(raf); + }, []); + + const onPointerDown = (e: ReactPointerEvent) => { + dragging.current = true; + start.current = { x: e.clientX, a: angle.current }; + e.currentTarget.setPointerCapture(e.pointerId); + if (viewportRef.current) viewportRef.current.style.cursor = "grabbing"; + }; + const onPointerMove = (e: ReactPointerEvent) => { + if (!dragging.current) return; + angle.current = start.current.a + (e.clientX - start.current.x) * 0.4; + }; + const onPointerUp = () => { + dragging.current = false; + if (viewportRef.current) viewportRef.current.style.cursor = "grab"; + }; + + return ( +
+
+
+ Use cases +
+

+ One orchestrator, many jobs +

+

+ Point AO at the work and walk away — drag to explore what a single + run can do. +

+
+ +
(paused.current = true)} + onMouseLeave={() => { + paused.current = false; + onPointerUp(); + }} + onPointerDown={onPointerDown} + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + className="landing-reveal relative mx-auto select-none" + style={{ + perspective: "1900px", + height: `${CARD_H + 80}px`, + maxWidth: "1120px", + cursor: "grab", + touchAction: "pan-y", + WebkitMaskImage: + "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)", + maskImage: + "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)", + }} + > +
+ {cases.map((c, i) => ( +
{ + cardRefs.current[i] = el; + }} + style={{ + position: "absolute", + left: "50%", + top: "50%", + width: `${CARD_W}px`, + height: `${CARD_H}px`, + marginLeft: `-${CARD_W / 2}px`, + marginTop: `-${CARD_H / 2}px`, + backfaceVisibility: "hidden", + }} + > +
+
+ {c.eyebrow} +
+

+ {c.title} +

+

+ {c.desc} +

+
+
+ {c.prefix}{" "} + {c.cmd} +
+
+ → {c.outcome} +
+
+
+
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/landing/components/LandingVideo.tsx b/frontend/src/landing/components/LandingVideo.tsx new file mode 100644 index 000000000..07058074f --- /dev/null +++ b/frontend/src/landing/components/LandingVideo.tsx @@ -0,0 +1,20 @@ +export function LandingVideo() { + return ( +
+
+ + See it in action + +
+
+