diff --git a/apps/docs/app/[lang]/(home)/components/architecture.tsx b/apps/docs/app/[lang]/(home)/components/architecture.tsx index ecd638e94..56e54e7ef 100644 --- a/apps/docs/app/[lang]/(home)/components/architecture.tsx +++ b/apps/docs/app/[lang]/(home)/components/architecture.tsx @@ -1,136 +1,198 @@ -"use client"; +import Link from "next/link"; +import type { JSX, ReactNode } from "react"; +import { + IconArrowUpRight, + IconLinked, + IconMessage, + IconSandbox, + IconSparkles, + IconWorkflow, + IconWrench, +} from "@/components/geistcn-icons"; +import { GradientBorder } from "./gradient-border"; +import { SetupSwitcher } from "./setup-switcher"; -import { motion, useInView } from "motion/react"; -import { useRef } from "react"; +// Runtime primitives shown in a row (desktop) / stacked (mobile) below the +// full-width Durable Workflow card. +const RUNTIME_ITEMS: { + icon: ReactNode; + title: string; + description: string; + href?: string; +}[] = [ + { + icon: , + title: "AI SDK", + description: "Model calls, streaming", + href: "https://ai-sdk.dev/", + }, + { + icon: , + title: "Sandbox SDK", + description: "Isolated execution", + href: "https://vercel.com/docs/sandbox/sdk-reference", + }, + { + icon: , + title: "Connection SDK", + description: "MCP/HTTP endpoints", + href: "/docs/connections/overview", + }, + { + icon: , + title: "Tools & Subagents", + description: "Functions, child agents", + }, +]; -export function ArchitectureDiagram() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.2 }); +const CHANNELS = [ + "Slack", + "Discord", + "Web Chat", + "Google Chat", + "Microsoft Teams", + "WhatsApp", + "API", + "Cron", + "Twilio", + "Linear", +]; +function SectionLabel({ children }: { children: string }): JSX.Element { return ( -
+ + {children} + + ); +} + +function PrimitiveCard({ + icon, + title, + description, + href, +}: { + icon: ReactNode; + title: string; + description: string; + href?: string; +}): JSX.Element { + const body = ( + <> + {icon} +
+ {title} + {description} +
+ + ); + + if (!href) { + return
{body}
; + } + + return ( + + {body} + + + ); +} + +export function ArchitectureDiagram() { + return ( +
-

- Three layers, cleanly separated +

+ Built on open-source SDKs, yours to self-host

- The runtime owns durability and - state. The harness executes AI work. - The channel is where your agent gets - surfaced. + Each capability is its own open-source SDK — workflows, AI, sandbox, connections, and + channels. Swap any backend and self-host the whole runtime, with zero + managed-infrastructure dependencies.

- - - {/* Left column: Harness above Runtime */} -
- {/* Harness */} - -
-
-
- Harness -
-
-
- Executes one unit of AI work per workflow step -
- - - {/* Runtime (contains workflow + primitives) */} - -
-
-
- Runtime -
-
-
+
+ {/* Runtime */} +
+ +
+ Runtime + Durable execution, state persistence, event streaming -
+ +
- {/* Durable Workflow */} -
- -
-
Durable Workflow
-
- Checkpointed steps, park between messages, resume on delivery -
-
-
+ } + title="Durable Workflow" + description="Checkpointed steps, park between messages, resume on delivery" + href="https://workflow-sdk.dev/worlds" + /> - {/* Primitives (inside runtime) */} -
-
-
- AI SDK -
-
Model calls, streaming
-
-
-
- Sandbox SDK -
-
Isolated execution
-
-
-
- Connection SDK -
-
MCP/HTTP endpoints
-
-
-
Tools & Subagents
-
Functions, child agents
-
-
- +
+ {RUNTIME_ITEMS.map((item) => ( + + ))} +
- {/* Channel (right side, stretches full height) */} - -
-
-
- Channel -
+ {/* Channel */} +
+ +
+ Channel + Where your agent gets surfaced
-
Where your agent gets surfaced
-
- {["Slack", "Web Chat", "API", "Cron"].map((ch) => ( -
- {ch} + + {/* lg:h-0 + lg:grow lets the card fill the runtime-driven column + height and clip overflowing channels instead of dictating it. */} + + +
+ Chat SDK + {/* Two columns while the cards are stacked (one-column layout); + a single column once they sit side by side at lg. */} +
+ {CHANNELS.map((channel) => ( + + {channel} + + ))}
- ))} -
- other channels
-
- - + + {/* Fade the clipped channels out toward the bottom of the card. */} +
+ +
+
+ +
); diff --git a/apps/docs/app/[lang]/(home)/components/feature-card.tsx b/apps/docs/app/[lang]/(home)/components/feature-card.tsx deleted file mode 100644 index e6be1a029..000000000 --- a/apps/docs/app/[lang]/(home)/components/feature-card.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { ArrowRight } from "lucide-react"; -import Link from "next/link"; -import type { ReactNode } from "react"; - -export function FeatureCard({ - title, - description, - icon, - visual, - href, -}: { - title: string; - description: string; - icon: ReactNode; - visual: ReactNode; - href: string; -}) { - return ( -
-
-
- {icon} -
-
-

{title}

-

{description}

-
-
-
{visual}
-
- - Learn more - - -
-
- ); -} diff --git a/apps/docs/app/[lang]/(home)/components/feature-grid.tsx b/apps/docs/app/[lang]/(home)/components/feature-grid.tsx index 4a88e4dad..d42a878cf 100644 --- a/apps/docs/app/[lang]/(home)/components/feature-grid.tsx +++ b/apps/docs/app/[lang]/(home)/components/feature-grid.tsx @@ -1,78 +1,73 @@ -import { Bot, Database, FlaskConical, MessageSquare, Shield, Terminal } from "lucide-react"; -import { FeatureCard } from "./feature-card"; -import { ChannelsVisual } from "./visuals/channels"; -import { DurabilityVisual } from "./visuals/durability"; -import { EvalsVisual } from "./visuals/evals"; -import { HITLVisual } from "./visuals/hitl"; -import { SandboxVisual } from "./visuals/sandbox"; -import { SubagentsVisual } from "./visuals/subagents"; +import type { JSX, ReactNode } from "react"; +import { + IconLogs, + IconMessage, + IconRobot, + IconSandbox, + IconUser, + IconWorkflow, +} from "@/components/geistcn-icons"; -const features = [ +const FEATURES: { icon: ReactNode; label: string; description: string }[] = [ { - title: "Durable Execution", + icon: , + label: "Durable Execution", description: "Workflows survive crashes and restarts. Every step is checkpointed. Agents park when waiting, resume on the next message.", - icon: , - visual: , - href: "/docs/concepts/sessions-runs-and-streaming", }, { - title: "Sandboxed Compute", + icon: , + label: "Sandboxed Compute", description: - "Agents spin up isolated VMs on demand. File system access, bash execution, and code runs, all completely isolated.", - icon: , - visual: , - href: "/docs/sandbox", + "Agents run code in isolated sandboxes. File system access, bash execution, and code, all fully isolated.", }, { - title: "Multi-Channel Delivery", + icon: , + label: "Multi-Channel Delivery", description: "One agent codebase deploys to web chat, Slack, API, cron, CLI, and custom apps.", - icon: , - visual: , - href: "/docs/channels/overview", }, { - title: "Human-in-the-Loop", + icon: , + label: "Human-in-the-Loop", description: "Tools that need confirmation trigger approval gates. Sessions park until resolved, then resume seamlessly.", - icon: , - visual: , - href: "/docs/tools", }, { - title: "Subagents", + icon: , + label: "Subagents", description: "Delegate specialized work to child agents with their own prompts, tools, and sandbox.", - icon: , - visual: , - href: "/docs/subagents", }, { - title: "Evaluations", + icon: , + label: "Evaluations", description: "Define test suites with scoring rubrics. Run evals on every deployment and on a schedule.", - icon: , - visual: , - href: "/docs/evals/overview", }, ]; -export function FeatureGrid() { +export function FeatureGrid(): JSX.Element { return (
-

+

Everything you need for production agents

- Enterprise governance, observability, and sandboxed compute come standard. Focus on - building, not infrastructure. + Durability, sandboxing, human-in-the-loop, and evals are built into the framework. Focus + on building your agent.

-
- {features.map((feature) => ( - +
    + {FEATURES.map((feature) => ( +
  • +
    + {feature.icon} + {feature.label} +
    +

    {feature.description}

    +
  • ))} -
+
); diff --git a/apps/docs/app/[lang]/(home)/components/file-tree-view.tsx b/apps/docs/app/[lang]/(home)/components/file-tree-view.tsx new file mode 100644 index 000000000..9ef69ddb0 --- /dev/null +++ b/apps/docs/app/[lang]/(home)/components/file-tree-view.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { type ReactNode, useState } from "react"; +import { IconCheck, IconPlusCircle, IconTrash } from "@/components/geistcn-icons"; +import { cn } from "@/lib/utils"; + +export interface FileTreeItem { + name: string; + fileName: string; + /** Short, what-this-file-does line shown under the code panel header. */ + description: string; + /** File-type icon shown beside the file name in the code panel header. */ + icon: ReactNode; + /** Pre-highlighted code, rendered on the server through the geistdocs CodeBlock. */ + code: ReactNode; +} + +export function FileTreeView({ items }: { items: FileTreeItem[] }) { + const [selectedIndex, setSelectedIndex] = useState(0); + // The first file is "added" by default; every other file is optional and + // only counts as added once the user clicks it. + const [visited, setVisited] = useState>(() => new Set([0])); + const selected = items[selectedIndex]; + + function select(index: number) { + // Clicking the active, already-added file deselects it again. The default + // file (instructions.md) is always present and can't be removed. + if (index !== 0 && index === selectedIndex && visited.has(index)) { + const next = new Set(visited); + next.delete(index); + setVisited(next); + const remaining = [...next]; + setSelectedIndex(remaining.length > 0 ? Math.max(...remaining) : 0); + return; + } + setSelectedIndex(index); + setVisited((prev) => new Set(prev).add(index)); + } + + function reset() { + setSelectedIndex(0); + setVisited(new Set([0])); + } + + return ( + // Full-width container so the header divider can bleed to the page frame's + // vertical borders, forming a cross with the layout grid. +
+ {/* Grid line aligned exactly with the card header's border-b. */} +
+
+
+
+ {/* Sidebar */} +
+
+ agent/ + {/* Counter slides left as the reset button reveals once more + than the default file is selected. */} +
+ + {visited.size} {visited.size === 1 ? "file" : "files"} selected + + +
+
+
+ {items.map((item, i) => { + const added = visited.has(i); + return ( + + ); + })} +
+
+ + {/* Code panel */} +
+
+ {/* {selected.icon} */} + {selected.fileName} + {selectedIndex > 0 ? ( + + Optional + + ) : null} +
+

+ {selected.description} +

+ {/* Re-keyed per file so the code subtly flies in on selection. */} +
+ {selected.code} +
+
+
+
+
+
+
+ ); +} diff --git a/apps/docs/app/[lang]/(home)/components/file-tree.tsx b/apps/docs/app/[lang]/(home)/components/file-tree.tsx index 53b5838d9..fcfec91c5 100644 --- a/apps/docs/app/[lang]/(home)/components/file-tree.tsx +++ b/apps/docs/app/[lang]/(home)/components/file-tree.tsx @@ -1,443 +1,221 @@ -"use client"; - -import type { LucideIcon } from "lucide-react"; +import { CodeBlock } from "@vercel/geistdocs/components/code-block"; +import { geistShikiTheme } from "@vercel/geistdocs/shiki-theme"; +import { highlight } from "fumadocs-core/highlight"; +import type { ComponentProps, JSX } from "react"; import { - Bot, - Clock, - FileText, - MessageSquare, - Plug, - Settings, - Terminal, - Wrench, -} from "lucide-react"; -import { motion, useInView } from "motion/react"; -import type { ReactNode } from "react"; -import { useRef, useState } from "react"; + IconAcronymTs, + IconAgents, + IconClock, + IconFileText, + IconLinked, + IconMessage, + type IconProps, + IconSandbox, + IconWrench, +} from "@/components/geistcn-icons"; +import { cn } from "@/lib/utils"; +import { FileTreeView } from "./file-tree-view"; -function Kw({ children }: { children: ReactNode }) { - return {children}; -} -function Str({ children }: { children: ReactNode }) { - return {children}; -} -function Fn({ children }: { children: ReactNode }) { - return {children}; -} -function Ty({ children }: { children: ReactNode }) { - return {children}; -} -function Pl({ children, className }: { children: ReactNode; className?: string }) { - return {children}; -} - -interface TreeItem { +interface Snippet { name: string; fileName: string; - icon: LucideIcon; - color: string; + lang: string; + Icon: (props: IconProps) => JSX.Element; description: string; - codeHighlighted: ReactNode; - indent: number; + code: string; } -const treeItems: TreeItem[] = [ - { - name: "agent.ts", - fileName: "agent.ts", - icon: Settings, - color: "text-purple-600", - description: "Model and runtime configuration.", - codeHighlighted: ( - <> - import {"{ "} - defineAgent - {" }"} from {'"eve"'} - ; - {"\n"} - {"\n"} - export default defineAgent - {"({"} - {"\n"} - {" "} - model - : {'"openai/gpt-5.4-mini"'} - , - {"\n"} - {"});"} - - ), - indent: 1, - }, +const snippets: Snippet[] = [ { name: "instructions.md", fileName: "instructions.md", - icon: FileText, - color: "text-green-600", - description: "Always-on instructions. The agent's core identity.", - codeHighlighted: ( - <> - {"# Identity"} - {"\n"} - {"\n"} - You are an expert weather assistant. - {"\n"} - You can fetch the weather for any - {"\n"} - city in the world. - - ), - indent: 1, + lang: "markdown", + Icon: IconFileText, + description: + "An instructions.md file is a complete agent — describe its role in Markdown, then run eve.", + code: `# Identity + +You are an expert weather assistant. +You can fetch the weather for any +city in the world.`, + }, + { + name: "agent.ts", + fileName: "agent.ts", + lang: "typescript", + Icon: IconAcronymTs, + description: + "eve uses a default model. Add agent.ts when you want to choose a model or configure the runtime.", + code: `import { defineAgent } from "eve"; + +export default defineAgent({ + model: "openai/gpt-5.4-mini", +});`, }, { name: "skills/", fileName: "skills/research.md", - icon: FileText, - color: "text-amber-600", - description: "On-demand procedures loaded only when relevant.", - codeHighlighted: ( - <> - {"---"} - {"\n"} - name - : research - {"\n"} - description - : Research unfamiliar topics - {"\n"} - {"---"} - {"\n"} - {"\n"} - When the task is novel or ambiguous, - {"\n"} - gather evidence first, then answer. - - ), - indent: 1, + lang: "markdown", + Icon: IconFileText, + description: + "Skills are Markdown playbooks loaded only when relevant, so the agent gets focused guidance without carrying it in every prompt.", + code: `--- +name: research +description: Research unfamiliar topics +--- + +When the task is novel or ambiguous, +gather evidence first, then answer.`, }, { name: "tools/", fileName: "tools/get_weather.ts", - icon: Wrench, - color: "text-orange-600", - description: "Typed integrations exposed to the model. File name becomes the tool name.", - codeHighlighted: ( - <> - import {"{ "} - defineTool - {" }"} from {'"eve/tools"'} - ; - {"\n"} - import z from {'"zod"'} - ; - {"\n"} - {"\n"} - export default defineTool - {"({"} - {"\n"} - {" "} - description - : {'"Get the weather for a city"'} - , - {"\n"} - {" "} - inputSchema - : z. - object - {"({"} - {"\n"} - {" "} - cityName - : z. - string - (), - {"\n"} - {" "} - {"}),"} - {"\n"} - {" "} - async execute - {"(input) {"} - {"\n"} - {" "} - const res = await fetch - ( - {"\n"} - {" "} - {"`${"} - process.env.WEATHER_API_URL - {"}/current?city=${"} - input.cityName - {"}`"} - {"\n"} - {" "} - ); - {"\n"} - {" "} - const data = await res. - json - (); - {"\n"} - {" "} - return data.current_condition[ - 0 - ]; - {"\n"} - {" "} - {"},"} - {"\n"} - {"});"} - - ), - indent: 1, + lang: "typescript", + Icon: IconWrench, + description: + "Drop a TypeScript file in tools/ and the model can call it — the filename becomes the tool name, no registration required.", + code: `import { defineTool } from "eve/tools"; +import z from "zod"; + +export default defineTool({ + description: "Get the weather for a city", + inputSchema: z.object({ + cityName: z.string(), + }), + async execute(input) { + const res = await fetch( + \`\${process.env.WEATHER_API_URL}/current?city=\${input.cityName}\` + ); + const data = await res.json(); + return data.current_condition[0]; + }, +});`, }, { name: "sandbox/", fileName: "sandbox/sandbox.ts", - icon: Terminal, - color: "text-red-600", - description: "Isolated compute environments with lifecycle hooks.", - codeHighlighted: ( - <> - import {"{ "} - defineSandbox - {" }"} from - {"\n"} - {" "} - {'"eve/sandbox"'} - ; - {"\n"} - {"\n"} - export default defineSandbox - {"({"} - {"\n"} - {" "} - async bootstrap - {"({ sandbox }) {"} - {"\n"} - {" "} - await sandbox. - run - ( - {"\n"} - {" "} - {'"git clone repo /workspace"'} - {"\n"} - {" "} - ); - {"\n"} - {" "} - {"},"} - {"\n"} - {"});"} - - ), - indent: 1, + lang: "typescript", + Icon: IconSandbox, + description: + "Every agent includes an isolated sandbox. Add sandbox/sandbox.ts to swap in any backend or customize its setup.", + code: `import { defineSandbox } from + "eve/sandbox"; + +export default defineSandbox({ + async bootstrap({ sandbox }) { + await sandbox.run( + "git clone repo /workspace" + ); + }, +});`, }, { name: "channels/", fileName: "channels/slack.ts", - icon: MessageSquare, - color: "text-cyan-600", - description: "HTTP, Slack, and custom delivery surfaces.", - codeHighlighted: ( - <> - import {"{ "} - slackChannel - {" }"} from - {"\n"} - {" "} - {'"eve/channels/slack"'} - ; - {"\n"} - {"\n"} - export default slackChannel - {"({"} - {"\n"} - {" "} - botName - : {'"my-agent"'} - , - {"\n"} - {"});"} - - ), - indent: 1, + lang: "typescript", + Icon: IconMessage, + description: "Add channel files to use the same agent in Slack, Discord, Teams, or the web.", + code: `import { slackChannel } from + "eve/channels/slack"; + +export default slackChannel({ + botName: "my-agent", +});`, }, { name: "connections/", fileName: "connections/linear.ts", - icon: Plug, - color: "text-pink-600", - description: "External MCP services.", - codeHighlighted: ( - <> - import {"{ "} - defineMcpClientConnection - {" }"} - {"\n"} - {" "} - from {'"eve/connections"'} - ; - {"\n"} - {"\n"} - export default defineMcpClientConnection - {"({"} - {"\n"} - {" "} - url - : {'"https://mcp.linear.app/mcp"'} - , - {"\n"} - {"});"} - - ), - indent: 1, + lang: "typescript", + Icon: IconLinked, + description: + "Connections handle auth for services like GitHub, Stripe, and Linear, so tools can call them without managing tokens.", + code: `import { defineMcpClientConnection } + from "eve/connections"; + +export default defineMcpClientConnection({ + url: "https://mcp.linear.app/mcp", +});`, }, { name: "subagents/", fileName: "subagents/researcher/agent.ts", - icon: Bot, - color: "text-indigo-600", - description: "Specialist child agents.", - codeHighlighted: ( - <> - import {"{ "} - defineAgent - {" }"} from - {"\n"} - {" "} - {'"eve"'} - ; - {"\n"} - {"\n"} - export default defineAgent - {"({"} - {"\n"} - {" "} - description - : {'"Investigate questions"'} - , - {"\n"} - {" "} - model - : {'"openai/gpt-5.4"'} - , - {"\n"} - {"});"} - - ), - indent: 1, + lang: "typescript", + Icon: IconAgents, + description: + "Add subagents for specialized work — the main agent delegates tasks and combines the results.", + code: `import { defineAgent } from + "eve"; + +export default defineAgent({ + description: "Investigate questions", + model: "openai/gpt-5.4", +});`, }, { name: "schedules/", fileName: "schedules/daily-report.md", - icon: Clock, - color: "text-emerald-600", - description: "Recurring cron jobs.", - codeHighlighted: ( - <> - {"---"} - {"\n"} - cron - : {'"0 8 * * *"'} - {"\n"} - {"---"} - {"\n"} - {"\n"} - Send the user a daily weather - {"\n"} - digest for their saved cities. - - ), - indent: 1, + lang: "markdown", + Icon: IconClock, + description: + "Schedules run agents automatically for jobs like daily reports and weekly digests, continuing durably without an active session.", + code: `--- +cron: "0 8 * * *" +--- + +Send the user a daily weather +digest for their saved cities.`, }, ]; -const CODE_BOX_HEIGHT = 280; - -export function FileTree() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.3 }); - const [selectedIndex, setSelectedIndex] = useState(0); +export async function FileTree() { + const rendered = await Promise.all( + snippets.map((snippet) => + highlight(snippet.code, { + lang: snippet.lang, + theme: geistShikiTheme, + components: { + // Render the highlighted output through the geistdocs CodeBlock so the + // snippets match the docs (line numbers, copy button, geist theme), + // with its default border/background stripped to sit flush in the panel. + pre: ({ children, ...props }: ComponentProps<"pre">) => ( + + {children} + + ), + }, + }), + ), + ); - const selected = treeItems[selectedIndex]; + const items = snippets.map((snippet, i) => ({ + name: snippet.name, + fileName: snippet.fileName, + description: snippet.description, + icon: , + code: rendered[i], + })); return ( -
+
-

+

An agent is a directory

- Define instructions and skills in markdown, tools in TypeScript, and deploy. The framework - compiles the directory, wires up durable workflows, and connects channels. + Define instructions and skills in markdown, tools in TypeScript, and deploy anywhere. The + framework compiles the directory, wires up durable workflows, and connects channels.

-
-
-
agent/
- {treeItems.map((item, i) => ( - setSelectedIndex(i)} - className={`flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors ${ - selectedIndex === i ? "bg-gray-100" : "hover:bg-gray-100/40" - }`} - style={{ paddingLeft: `${item.indent * 16 + 4}px` }} - > - - - {item.name} - - - ))} -
- -
-
- -
- - - {selected.fileName} - -
-

{selected.description}

-
-
- - -
-                {selected.codeHighlighted}
-              
-
-
-
+ +
); } diff --git a/apps/docs/app/[lang]/(home)/components/gradient-border.tsx b/apps/docs/app/[lang]/(home)/components/gradient-border.tsx new file mode 100644 index 000000000..199fe2582 --- /dev/null +++ b/apps/docs/app/[lang]/(home)/components/gradient-border.tsx @@ -0,0 +1,23 @@ +import type { JSX } from "react"; + +/** + * Subtle gradient hairline border — strongest at the top, fading to transparent + * toward the bottom. Render inside a `relative rounded-*` element; it inherits + * the parent's corner radius and paints a 1px masked ring at the edge. + */ +export function GradientBorder(): JSX.Element { + return ( +
+ ); +} diff --git a/apps/docs/app/[lang]/(home)/components/home-content.tsx b/apps/docs/app/[lang]/(home)/components/home-content.tsx new file mode 100644 index 000000000..f62a19138 --- /dev/null +++ b/apps/docs/app/[lang]/(home)/components/home-content.tsx @@ -0,0 +1,54 @@ +import type { Metadata } from "next"; +import { staticOgImage } from "@/lib/geistdocs/og"; +import { ArchitectureDiagram } from "./architecture"; +import { CTA } from "./cta"; +import { FeatureGrid } from "./feature-grid"; +import { FileTree } from "./file-tree"; +import { InstallSwitcher } from "./install-switcher"; +import { NextjsInterop } from "./nextjs-interop"; + +const title = "eve"; +const tagline = "Like Next.js for web apps, but for agents."; +const description = + "Markdown for instructions and skills, TypeScript for tools. Durable by default."; + +export const homeMetadata: Metadata = { + title, + description: `${tagline} ${description}`, + openGraph: { + title, + description: `${tagline} ${description}`, + images: [staticOgImage], + }, + twitter: { + card: "summary_large_image", + title, + description: `${tagline} ${description}`, + images: [staticOgImage], + }, +}; + +export const HomeContent = () => ( +
+
+

+ The Framework +
+ for Building Agents +

+

+ {tagline} {description} +

+
+ +
+
+
+ + + + + +
+
+); diff --git a/apps/docs/app/[lang]/(home)/components/install-switcher.tsx b/apps/docs/app/[lang]/(home)/components/install-switcher.tsx new file mode 100644 index 000000000..d3a238019 --- /dev/null +++ b/apps/docs/app/[lang]/(home)/components/install-switcher.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { track } from "@vercel/analytics"; +import { + CommandPromptContent, + CommandPromptCopy, + CommandPromptList, + CommandPromptPrefix, + CommandPromptRoot, + CommandPromptSurface, + CommandPromptTrigger, + CommandPromptTriggerDivider, + CommandPromptViewport, +} from "@vercel/geistdocs/components/command-prompt"; +import type { JSX } from "react"; +import { cn } from "@/lib/utils"; + +const HUMAN_COMMAND = "npx eve@latest init my-agent"; +const AGENT_COMMAND = "npx skills add vercel/eve"; + +/** + * Hero install prompt that toggles between the human-facing `eve init` command + * and the agent-facing skills command, with a copy-to-clipboard pill. + */ +export const InstallSwitcher = ({ className }: { className?: string }): JSX.Element => ( + { + track("Selected installer command", { target: value }); + }} + > + + For humans + + For agents + + + + $ + + + {HUMAN_COMMAND} + + + {AGENT_COMMAND} + + + + + +); diff --git a/apps/docs/app/[lang]/(home)/components/nextjs-interop.tsx b/apps/docs/app/[lang]/(home)/components/nextjs-interop.tsx new file mode 100644 index 000000000..b27608738 --- /dev/null +++ b/apps/docs/app/[lang]/(home)/components/nextjs-interop.tsx @@ -0,0 +1,99 @@ +import { Badge } from "@vercel/geistdocs/components/badge"; +import { CodeBlock } from "@vercel/geistdocs/components/code-block"; +import { geistShikiTheme } from "@vercel/geistdocs/shiki-theme"; +import { highlight } from "fumadocs-core/highlight"; +import type { ComponentProps } from "react"; +import { cn } from "@/lib/utils"; +import { GradientBorder } from "./gradient-border"; + +interface InteropFile { + fileName: string; + lang: string; + code: string; +} + +const FILES: InteropFile[] = [ + { + fileName: "next.config.ts", + lang: "typescript", + code: `import { withEve } from "eve/next"; + +const nextConfig = {}; + +// Agent + app: one dev server, one deploy. +export default withEve(nextConfig);`, + }, + { + fileName: "app/chat.tsx", + lang: "tsx", + code: `"use client"; +import { useEveAgent } from "eve/react"; + +export function Chat() { + // Same-origin routes, found automatically. + const agent = useEveAgent(); + // agent.messages, agent.sendMessage, ... +}`, + }, +]; + +const BENEFITS = ["One Dev Server", "Same-Origin, No CORS", "One Deploy"]; + +async function renderCode(file: InteropFile) { + return highlight(file.code, { + lang: file.lang, + theme: geistShikiTheme, + components: { + pre: ({ children, ...props }: ComponentProps<"pre">) => ( + + {children} + + ), + }, + }); +} + +export async function NextjsInterop() { + const rendered = await Promise.all(FILES.map(renderCode)); + + return ( +
+
+

+ Runs inside your Next.js app +

+

+ Wrap your config with withEve() and the agent runs + alongside your app. useEveAgent() finds the + mounted routes on its own — no CORS to configure and no URL env vars to keep in sync. +

+ + {/* A single gradient-bordered frame around both code blocks. */} +
+ +
+ {FILES.map((file, i) => ( +
+
+ {file.fileName} +
+
{rendered[i]}
+
+ ))} +
+
+ +
+ {BENEFITS.map((benefit) => ( + + {benefit} + + ))} +
+
+
+ ); +} diff --git a/apps/docs/app/[lang]/(home)/components/setup-switcher.tsx b/apps/docs/app/[lang]/(home)/components/setup-switcher.tsx new file mode 100644 index 000000000..381ea56be --- /dev/null +++ b/apps/docs/app/[lang]/(home)/components/setup-switcher.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { + SiAnsible, + SiDigitalocean, + SiDocker, + SiOllama, + SiVercel, +} from "@icons-pack/react-simple-icons"; +import Link from "next/link"; +import { type ComponentType, useState } from "react"; +import { IconArrowUpRight } from "@/components/geistcn-icons"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; + +type Mode = "managed" | "self-hosted"; + +interface StackEntry { + category: string; + name: string; + /** Brand mark, rendered in its original color for recognizability. */ + Logo: ComponentType<{ + size?: number; + color?: string; + className?: string; + title?: string; + }>; + /** When set, the card links to the backend's docs/site. */ + href?: string; +} + +// Same agent, two real-world deployment targets: managed (Vercel-native) and a +// fully self-hosted box. The self-hosted column mirrors a real setup — Docker +// sandbox, a single DigitalOcean server, Ansible deploys, zero managed services. +const STACKS: Record = { + managed: [ + { + category: "Models", + name: "AI Gateway", + Logo: SiVercel, + href: "https://vercel.com/ai-gateway", + }, + { + category: "Sandbox", + name: "Vercel Sandbox", + Logo: SiVercel, + href: "https://vercel.com/sandbox", + }, + { + category: "Runtime", + name: "Vercel Workflows", + Logo: SiVercel, + href: "https://vercel.com/workflow", + }, + { + category: "Deploy", + name: "Vercel", + Logo: SiVercel, + href: "https://vercel.com/docs/deployments", + }, + ], + "self-hosted": [ + { category: "Models", name: "Ollama", Logo: SiOllama }, + { category: "Sandbox", name: "Docker", Logo: SiDocker }, + { category: "Runtime", name: "DigitalOcean", Logo: SiDigitalocean }, + { category: "Deploy", name: "Ansible", Logo: SiAnsible }, + ], +}; + +const CAPTIONS: Record = { + managed: "Deploy to Vercel — sandboxes, durable workflows, and model routing handled for you.", + "self-hosted": + "Runs on a single 4 GB server with Docker and Ansible — no managed-service dependencies.", +}; + +/** + * Toggle that shows the concrete backends behind each capability for a managed + * (Vercel) vs. fully self-hosted deployment, illustrating a real-world setup. + */ +export function SetupSwitcher() { + const [mode, setMode] = useState("self-hosted"); + const selfHosted = mode === "self-hosted"; + + return ( +
+
+ + setMode(checked ? "self-hosted" : "managed")} + aria-label="Toggle deployment target" + className="cursor-pointer" + /> + +
+ + {/* Re-keyed per mode so the stack cross-fades on toggle. px-5 insets the + grid by the gradient cards' padding so these cards line up with the + inner cards (AI SDK, Connection SDK, …) in the section above. */} +
+ {STACKS[mode].map((entry) => { + const body = ( + <> + +
+ + {entry.category} + + + {entry.name} + +
+ + ); + + if (!entry.href) { + return ( +
+ {body} +
+ ); + } + + return ( + + {body} + + + ); + })} +
+ +

+ {CAPTIONS[mode]} +

+
+ ); +} diff --git a/apps/docs/app/[lang]/(home)/components/visuals/channels.tsx b/apps/docs/app/[lang]/(home)/components/visuals/channels.tsx deleted file mode 100644 index bbc37e28e..000000000 --- a/apps/docs/app/[lang]/(home)/components/visuals/channels.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import { Bot, Clock, Globe, MessageSquare, Radio, Terminal } from "lucide-react"; -import { motion, useInView } from "motion/react"; -import { useRef } from "react"; - -const targets = [ - { icon: Globe, label: "Web", color: "text-cyan-600" }, - { icon: MessageSquare, label: "Slack", color: "text-purple-600" }, - { icon: Terminal, label: "CLI", color: "text-green-600" }, - { icon: Radio, label: "API", color: "text-blue-600" }, - { icon: Clock, label: "Cron", color: "text-amber-600" }, -]; - -export function ChannelsVisual() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.5 }); - - return ( -
- - - - -
- {targets.map((t, i) => ( - -
- - {t.label} - - ))} -
-
- ); -} diff --git a/apps/docs/app/[lang]/(home)/components/visuals/durability.tsx b/apps/docs/app/[lang]/(home)/components/visuals/durability.tsx deleted file mode 100644 index 968b1dfd4..000000000 --- a/apps/docs/app/[lang]/(home)/components/visuals/durability.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { motion, useInView } from "motion/react"; -import { useRef } from "react"; - -const steps = [ - { id: "fetch", label: "Step 1: Fetch data", width: "60%", color: "bg-green-600" }, - { id: "process-1", label: "Step 2: Process", width: "45%", color: "bg-green-600" }, - { id: "crash", label: "Crash", width: "15%", color: "bg-red-600" }, - { id: "resume", label: "Resume", width: "10%", color: "bg-amber-600" }, - { id: "process-2", label: "Step 2: Process", width: "45%", color: "bg-green-600" }, - { id: "respond", label: "Step 3: Respond", width: "30%", color: "bg-green-600" }, -]; - -export function DurabilityVisual() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.5 }); - - return ( -
- {steps.map((step, i) => ( -
- - {step.label} - -
- -
-
- ))} -
- ); -} diff --git a/apps/docs/app/[lang]/(home)/components/visuals/evals.tsx b/apps/docs/app/[lang]/(home)/components/visuals/evals.tsx deleted file mode 100644 index 5310a46d4..000000000 --- a/apps/docs/app/[lang]/(home)/components/visuals/evals.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import { motion, useInView } from "motion/react"; -import { useRef } from "react"; - -const scores = [ - { label: "run.didNotFail", value: 100, color: "bg-green-600" }, - { label: "text.includes", value: 85, color: "bg-green-600" }, - { label: "run.usedTool", value: 92, color: "bg-green-600" }, - { label: "run.maxToolCalls", value: 100, color: "bg-green-600" }, -]; - -export function EvalsVisual() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.5 }); - - return ( -
- {scores.map((score, i) => ( -
- - {score.label} - -
- -
- - {score.value}% - -
- ))} -
- ); -} diff --git a/apps/docs/app/[lang]/(home)/components/visuals/hitl.tsx b/apps/docs/app/[lang]/(home)/components/visuals/hitl.tsx deleted file mode 100644 index eaa5162c0..000000000 --- a/apps/docs/app/[lang]/(home)/components/visuals/hitl.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { motion, useInView } from "motion/react"; -import { useRef } from "react"; - -const steps = [ - { label: "Tool called", status: "done" as const }, - { label: "Approval required", status: "waiting" as const }, - { label: "User approved", status: "done" as const }, - { label: "Execution resumed", status: "done" as const }, -]; - -export function HITLVisual() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.5 }); - - return ( -
- {steps.map((step, i) => ( - -
- {step.label} - {step.status === "waiting" && ( - - parked - - )} - - ))} -
- ); -} diff --git a/apps/docs/app/[lang]/(home)/components/visuals/sandbox.tsx b/apps/docs/app/[lang]/(home)/components/visuals/sandbox.tsx deleted file mode 100644 index 441392b77..000000000 --- a/apps/docs/app/[lang]/(home)/components/visuals/sandbox.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { motion, useInView } from "motion/react"; -import { useRef } from "react"; - -const lines = [ - { id: "cmd", prompt: "$ ", text: "sandbox.run('npm test')", color: "text-blue-600" }, - { id: "pass", prompt: "", text: "PASS src/agent.test.ts", color: "text-green-600" }, - { - id: "t1", - prompt: "", - text: " \u2713 handles weather query (42ms)", - color: "text-gray-900", - }, - { id: "t2", prompt: "", text: " \u2713 retries on failure (118ms)", color: "text-gray-900" }, - { id: "total", prompt: "", text: "Tests: 2 passed, 2 total", color: "text-green-600" }, -]; - -export function SandboxVisual() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.5 }); - - return ( -
- {lines.map((line, i) => ( - - {line.prompt && {line.prompt}} - {line.text} - - ))} -
- ); -} diff --git a/apps/docs/app/[lang]/(home)/components/visuals/subagents.tsx b/apps/docs/app/[lang]/(home)/components/visuals/subagents.tsx deleted file mode 100644 index a2f7fbfaa..000000000 --- a/apps/docs/app/[lang]/(home)/components/visuals/subagents.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client"; - -import { Bot, Code, Database, Search } from "lucide-react"; -import { motion, useInView } from "motion/react"; -import { useRef } from "react"; - -const agents = [ - { icon: Search, label: "Researcher", color: "text-purple-600" }, - { icon: Code, label: "Coder", color: "text-blue-600" }, - { icon: Database, label: "Analyst", color: "text-green-600" }, -]; - -export function SubagentsVisual() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.5 }); - - return ( -
- - - Parent Agent - - - - -
- {agents.map((agent, i) => ( - - - {agent.label} - - ))} -
-
- ); -} diff --git a/apps/docs/app/[lang]/(home)/home/page.tsx b/apps/docs/app/[lang]/(home)/home/page.tsx new file mode 100644 index 000000000..11a49dcd8 --- /dev/null +++ b/apps/docs/app/[lang]/(home)/home/page.tsx @@ -0,0 +1,9 @@ +import { HomeContent, homeMetadata } from "../components/home-content"; + +// Temporary alias of the landing page so the preview is reachable while `/` +// is being forwarded at the domain level. +export const metadata = homeMetadata; + +const HomeAliasPage = () => ; + +export default HomeAliasPage; diff --git a/apps/docs/app/[lang]/(home)/layout.tsx b/apps/docs/app/[lang]/(home)/layout.tsx index 1e1c5298e..8f8717528 100644 --- a/apps/docs/app/[lang]/(home)/layout.tsx +++ b/apps/docs/app/[lang]/(home)/layout.tsx @@ -7,7 +7,10 @@ const Layout = async ({ children, params }: LayoutProps<"/[lang]">) => { return ( -
{children}
+ {/* Marker so the global footer can be tinted on home routes only, + via the `body:has([data-home-route]) footer` rule in geistdocs.css. */} +