diff --git a/.gitignore b/.gitignore
index 9c5209d9cc..66b2addeb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,3 +67,6 @@ tsconfig.tsbuildinfo
playwright-report/
.pnpm-store/
+
+# Personal, per-developer agent instructions
+CLAUDE.local.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 120000
index 0000000000..681311eb9c
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1 @@
+CLAUDE.md
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000000..87a6c3d83c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,47 @@
+# CLAUDE.md
+
+graphql.org source. Next.js 14 — App Router in `src/app/`, Pages Router in
+`src/pages/` for the Nextra docs. TypeScript, Tailwind, pnpm.
+
+Main branch `source`. Trailing slashes enforced. Redirects in `vercel.json`.
+
+Commands live in `package.json`.
+
+Design rules: @DESIGN.md.
+
+## Rules
+
+Rules people break. Everything else should match surrounding code.
+
+**React**
+
+- No `useCallback`. React Compiler ready. `useMemo` only if measured.
+- No `renderX()`. No two-line helpers. No extracting single-use code. Inline
+ it.
+- `"use client"` costs bytes. Push it to the leaf that needs the handler. Rest
+ stays on the server.
+- Heavy things — CodeMirror, schemas, editors — load through `next/dynamic`
+ plus a viewport check, never at module scope. See
+ `src/components/index-page/how-it-works/index.tsx`.
+
+**Content**
+
+People cite this site. A number needs a source you can link, or it stays off
+the page. Uncited "up to N%" is worse than silence. Reviewers catch it.
+
+**TypeScript**
+
+`@/` alias for `src`. Strict. Named exports. No explicit return types.
+
+## Working here
+
+Dev server may already run on :3000. Check first. Leave it up.
+
+## Gotchas
+
+- `nextra`, `nextra-theme-docs` and `mermaid-isomorphic` are patched. Check
+ `patches/`.
+- SVGs in `src/icons/` and `**/pixelarticons/` import as components with
+ `?svgr`. `.mdx?raw` gives raw source to a server component.
+- `prebuild` fetches GitHub stats and syncs the landing schema and working
+ groups. Cold build needs network, takes minutes.
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000000..1ef9bd693d
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,212 @@
+---
+name: graphql.org
+description: The GraphQL Foundation's site — flat, high-contrast, hairline-ruled.
+colors:
+ pri-lightest: hsl(319 100% 96%)
+ pri-lighter: hsl(319 100% 90%)
+ pri-light: hsl(318 100% 80%)
+ pri-base: hsl(319 100% 44.1%)
+ pri-dark: hsl(319 100% 30%)
+ pri-darker: hsl(319 100% 20%)
+ sec-lighter: hsl(79 80% 90%)
+ sec-light: hsl(79 82% 80%)
+ sec-base: hsl(79 90% 65%)
+ sec-dark: hsl(79 98% 37%)
+ sec-darker: hsl(79 98% 23%)
+ neu-0: hsl(0 0% 100%)
+ neu-50: hsl(75 57% 97%)
+ neu-100: hsl(75 15% 95%)
+ neu-200: hsl(77 14% 90%)
+ neu-300: hsl(76 14% 85%)
+ neu-400: hsl(77 14% 80%)
+ neu-500: hsl(74 14% 70%)
+ neu-600: hsl(76 15% 60%)
+ neu-700: hsl(76 15% 40%)
+ neu-800: hsl(77 14% 20%)
+ neu-900: hsl(75 15% 5%)
+typography:
+ display:
+ fontFamily: "Host Grotesk, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "48px"
+ lineHeight: "1"
+ headline:
+ fontFamily: "Host Grotesk, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "40px"
+ title:
+ fontFamily: "Host Grotesk, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "32px"
+ body:
+ fontFamily: "Host Grotesk, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "14px"
+ lineHeight: "1.5"
+ label:
+ fontFamily: "Commit Mono, ui-monospace, monospace"
+ fontSize: "14px"
+ lineHeight: "1"
+rounded:
+ none: "0"
+ md: "6px"
+ full: "9999px"
+spacing:
+ section-y: "2rem"
+ section-x: "1rem"
+ container: "120rem"
+components:
+ button-primary:
+ backgroundColor: "{colors.neu-900}"
+ textColor: "{colors.neu-0}"
+ rounded: "{rounded.none}"
+ height: "56px"
+ padding: "0 32px"
+ button-primary-hover:
+ backgroundColor: "{colors.neu-800}"
+ button-secondary:
+ backgroundColor: "{colors.neu-100}"
+ textColor: "{colors.neu-900}"
+ rounded: "{rounded.none}"
+ button-tertiary:
+ backgroundColor: "{colors.neu-100}"
+ textColor: "{colors.neu-900}"
+ rounded: "{rounded.none}"
+ code-block:
+ rounded: "{rounded.md}"
+ typography: "{typography.label}"
+ tag:
+ backgroundColor: "{colors.sec-light}"
+ textColor: "{colors.neu-900}"
+ rounded: "{rounded.none}"
+---
+
+# graphql.org design
+
+## Overview
+
+This is the specification's own site, run by a foundation. It is a reference,
+not a product being sold, and it gets cited. Restraint is the brand: flat
+surfaces, hairline rules, one accent used sparingly, and claims that survive a
+skeptical reader.
+
+Two families carry everything. Magenta (`pri-*`) is GraphQL's colour and marks
+the primary or active thing. Lime (`sec-*`) is the accent, for confirmation and
+highlights. Everything else is `neu-*`.
+
+## Colors
+
+Semantic tokens only, defined as HSL triples in `src/app/colors.css` and
+exposed through `tailwind.config.ts`.
+
+The `neu-*` ramp inverts between light and dark automatically — `neu-0` is
+white in light mode and near-black in dark. So `text-neu-900` is the correct
+foreground in both modes and needs no `dark:` variant. Reach for `dark:` only
+when the semantic token is genuinely wrong for one mode.
+
+`pri-*` and `sec-*` do not invert. A magenta surface stays magenta.
+
+Never write a hex value in a component.
+
+## Typography
+
+`Host Grotesk` for text, `Commit Mono` for code, both loaded as CSS variables
+in `src/app/fonts`.
+
+Use the `typography-*` utilities, never raw `text-[NNpx]`. Each carries its own
+responsive step — `typography-h2` is 32px on mobile and 48px from `md` up. That
+step is the design; a hand-rolled size breaks it. The full scale is `d1`, `h1`
+through `h4`, `body-lg` through `body-xs`, plus `button`, `menu` and `tagline`.
+
+## Layout
+
+`.gql-container` centres content at a 120rem maximum. `.gql-section` supplies
+horizontal and vertical rhythm, stepping up at `lg` and `xl`. Compose the two
+rather than reinventing padding.
+
+Breakpoints are Tailwind's, plus `xs` at 394px and `3xl` at 1920px.
+
+Write one markup tree that reflows. A `md:hidden` block beside a
+`hidden md:block` block is two things to keep in sync and two things to forget.
+
+## Elevation & Depth
+
+Depth comes from hairlines and tonal steps, not shadows. A 1px `neu-200` border
+(`neu-100` in dark), or adjacent background steps — `neu-0` against `neu-50`. A
+card grid is `gap-px` over a `neu-200` background so the gaps read as rules.
+
+There is no elevation ladder. Across 408 component files the whole codebase
+uses about 40 shadows, nearly all decorative glows on marketing surfaces, and
+none of the design-system primitives use one. There is no `sm`/`md`/`lg` scale
+to reach for, so don't reach.
+
+## Shapes
+
+Square is the default, and it is load-bearing. Buttons, cards, tags and section
+panels carry no radius. Across 408 component files there are roughly 80
+`rounded-*` usages in total — a rounded corner here is an exception, not a
+texture.
+
+Three radii are sanctioned:
+
+- `rounded-none` — buttons, cards, tags, panels. The default.
+- `rounded-md` — code blocks, via `@/components/pre`.
+- `rounded-full` — status dots, avatars, bullets, and genuine pills.
+
+Anything else is drift. If a new surface wants `rounded-lg`, the answer is
+square.
+
+## Components
+
+Reach for what exists before writing a new one:
+
+| Need | Use |
+| ------------------------- | ------------------------------------------------------------------- |
+| Button or link-as-button | `@/app/conf/_design-system/button` |
+| Internal or external link | `@/app/conf/_design-system/anchor` |
+| Section eyebrow | `_design-system/section-label` or `@/_design-system/eyebrow` |
+| Code block | `.mdx` partial + `@/components/pre` and `nextra/components`' `Code` |
+| Runnable GraphQL | `@/components/interactive-code-block` |
+| Icon | `**/pixelarticons/*.svg?svgr` |
+| Horizontal card row | `EventsScrollview` |
+
+Buttons come in `primary`, `secondary` and `tertiary`, at `md` and `lg`. They
+are square, 56px tall (48px at `md`), and pick their element from the props:
+`href` renders an anchor, otherwise a `button`.
+
+Code blocks are highlighted at build time by Shiki using the themes in
+`src/_design-system/syntax/`. That is the only highlighter. Don't add a second
+one, and don't put `dangerouslySetInnerHTML` on a `
`.
+
+Every interactive element gets `gql-focus-visible`, which applies the shared
+`gql-focus-outline` on `:focus-visible`.
+
+## Motion
+
+Motion is decoration and must be optional. Every animation carries a
+`motion-reduce:` variant, and anything driven by JavaScript checks
+`prefers-reduced-motion` before it starts.
+
+Nothing animates forever. A loop that runs while off screen or in a background
+tab is a bug; gate it on an `IntersectionObserver` and `visibilitychange`.
+
+Never simulate work. If the answer is already known, render it. Artificial
+delay, fake progress and typewriter effects spend the reader's time to look
+busy.
+
+New keyframes belong in `theme.extend.keyframes`, not loose in `globals.css`.
+
+## Do's and Don'ts
+
+**Do**
+
+- Use semantic colour tokens and let `neu-*` invert.
+- Use the `typography-*` scale.
+- Separate with hairlines and background steps.
+- Give every interactive element a visible focus state.
+- Say what is true. This site gets cited; an uncited number costs more than it
+ earns.
+
+**Don't**
+
+- Add a radius or a shadow to a new surface without being asked.
+- Hardcode a hex, a pixel font size, or a breakpoint.
+- Ship a second solution for a problem the repo already solves.
+- Build separate mobile and desktop trees.
+- Animate without a reduced-motion path.
diff --git a/src/app/(main)/ai/components/by-the-numbers.tsx b/src/app/(main)/ai/components/by-the-numbers.tsx
index 778cddd981..76e6dd6696 100644
--- a/src/app/(main)/ai/components/by-the-numbers.tsx
+++ b/src/app/(main)/ai/components/by-the-numbers.tsx
@@ -1,173 +1,110 @@
-"use client"
-
import { SectionLabel } from "@/app/conf/_design-system/section-label"
+import { Anchor } from "@/app/conf/_design-system/anchor"
import {
- highlightGraphQLSchema,
- highlightGraphQL,
- highlightJSON,
- SYNTAX_CSS,
-} from "./syntax-highlight"
-
-const schemaWithDocs = `"""A product in the catalog."""
-type Product {
- """Human-readable display name."""
- name: String!
-
- """Price in minor units (cents)."""
- price: Int!
-
- """Units in stock. 0 means unavailable."""
- stock: Int!
-}
-
-type Query {
- """Full-text search across the catalog."""
- products(query: String!): [Product!]!
-}`
-
-const introspectionQuery = `{
- __type(name: "Product") {
- description
- fields {
- name
- description
- }
- }
-}`
-
-const introspectionResponse = `{
- "__type": {
- "description": "A product in the catalog.",
- "fields": [
- { "name": "name", "description": "Human-readable display name." },
- { "name": "price", "description": "Price in minor units (cents)." },
- { "name": "stock", "description": "Units in stock. 0 means unavailable." }
- ]
- }
-}`
+ NumbersIntrospectionSnippet,
+ NumbersResponseSnippet,
+ NumbersSchemaSnippet,
+} from "./snippets"
const stats = [
{
- label: "Token reduction",
- graphQL: "90%",
- graphQLDesc: "fewer tokens",
- rest: "10× more",
- restDesc: "token waste",
+ label: "Discovery",
+ graphQL: "Introspection",
+ graphQLDesc: "from the endpoint itself",
+ rest: "OpenAPI document",
+ restDesc: "published alongside",
explanation:
- "GraphQL lets AI agents request only the fields they need. REST endpoints return fixed payloads — often 10x the data an LLM actually needs to process. Every extra token costs money and context window space.",
+ "Most REST frameworks generate an OpenAPI document from the code, so this is not about hand-writing a schema. The difference is that introspection is part of the GraphQL spec and answers on the same endpoint the agent already calls, with no second artifact to locate or keep in sync.",
},
{
- label: "API calls per task",
- graphQL: "1",
- graphQLDesc: "single request",
- rest: "3–7",
- restDesc: "sequential calls",
+ label: "Response shape",
+ graphQL: "The query names the fields",
+ graphQLDesc: "caller decides",
+ rest: "The endpoint decides",
+ restDesc: "sparse fieldsets optional",
explanation:
- "GraphQL's composability means agents can fetch nested, related data in one query. REST requires multiple endpoints, forcing agents to make sequential calls and stitch responses client-side.",
+ "A GraphQL response contains the fields the query asked for. A REST endpoint returns its payload, and narrowing it means a sparse-fieldset convention or another endpoint. An agent pays for the difference in context window.",
},
{
- label: "Tool definitions",
- graphQL: "0",
- graphQLDesc: "auto-discovered",
- rest: "3",
- restDesc: "files to wire",
+ label: "Traversal",
+ graphQL: "One query, many types",
+ graphQLDesc: "follows relationships",
+ rest: "One endpoint per resource",
+ restDesc: "client stitches",
explanation:
- "REST frameworks can auto-generate OpenAPI, so this isn't about hand-writing schemas. The edge is plug-and-play: GraphQL's introspection and per-field, per-type, and per-query documentation are built into the spec and discoverable from one endpoint. With REST, an agent needs the API, its schema, and an instruction file (AGENT.md) — and you must point it to each. One GraphQL schema replaces all three.",
+ "A GraphQL query walks relationships across types, so the agent never has to hold the whole type graph in context at once. With REST the relationships live in the agent's head, and it composes the result itself.",
},
{
- label: "Type safety",
- graphQL: "100%",
- graphQLDesc: "typed responses",
- rest: "100%",
- restDesc: "if used with correct tooling",
+ label: "Documentation",
+ graphQL: "On types and fields",
+ graphQLDesc: "returned by introspection",
+ rest: "In the spec document",
+ restDesc: "plus an instructions file",
explanation:
- "Both are typed — OpenAPI gives REST schemas too. The real difference for agents is traversal: one GraphQL query follows relationships across types, so an agent never needs to hold the entire type graph in context at once. REST splits data across endpoints, forcing agents to remember deep, nested relationships to compose what one field resolves.",
+ "Descriptions attach to the type, every field and every argument, and come back through the same introspection call. There is no separate docs file to point the agent at.",
},
]
export function ByTheNumbers() {
return (
- By the numbers
-
GraphQL vs REST for AI
+ GraphQL and REST
+
+ What changes when the API describes itself
+
- Where GraphQL pays off when AI agents talk to your API.
+ Both can be typed and both can be documented. The difference is where
+ that description lives, and how much of it an agent has to carry.
@@ -175,12 +112,36 @@ export function ByTheNumbers() {
-
- Both protocols are typed; the gap is in traversal and discoverability,
- not in whether types exist.
+
+ Both protocols are typed, and both can describe themselves. The gap is
+ in traversal and discoverability, not in whether types exist. Where a
+ task maps cleanly onto one purpose-built endpoint, REST is the simpler
+ thing for an agent to call.
- {/* Documentation is part of the schema, queryable via introspection */}
+
+
+ The public numbers we can point at come from Apollo, who report{" "}
+
+ around 40% less schema context and 40–75% fewer tool calls
+ {" "}
+ when an MCP server exposes a curated set of operations instead of a
+ whole schema. Those are Apollo's own measurements of their own
+ server, not an independent benchmark, and they describe tool selection
+ rather than GraphQL against REST.
+
+
+ If you have reproducible figures for agents against a GraphQL API, the{" "}
+
+ AI Working Group
+ {" "}
+ would like to see them, and this page will cite them.
+
+
+
Docs live in the schema — and agents can query them
@@ -198,49 +159,12 @@ export function ByTheNumbers() {
introspection query — no separate docs file or AGENT.md to point it
to.
-
-
- When AI agents need to interact with APIs, GraphQL's
- self-describing schemas, strong typing, and composable queries make
- it the natural choice. No hand-written tool definitions. No
- token-wasting REST payloads. Just structured, predictable data.
+ An agent that can reach a GraphQL endpoint can read its types, its
+ field arguments and its documentation, then ask for exactly the
+ fields it needs. Nothing to publish alongside it and keep in sync.
@@ -95,111 +92,3 @@ export function Hero() {
)
}
-
-/* ─────────────────────────────────────────────
- Subtle animated schema grid in background
- ───────────────────────────────────────────── */
-
-function SchemaGrid() {
- const canvasRef = useRef(null)
-
- useEffect(() => {
- const canvas = canvasRef.current
- if (!canvas) return
- const ctx = canvas.getContext("2d")
- if (!ctx) return
-
- let animId: number
- let time = 0
- const nodes: { x: number; y: number; r: number; vx: number; vy: number }[] =
- []
-
- function resize() {
- if (!canvas) return
- canvas.width = canvas.offsetWidth * (window.devicePixelRatio || 1)
- canvas.height = canvas.offsetHeight * (window.devicePixelRatio || 1)
- ctx!.setTransform(1, 0, 0, 1, 0, 0)
- ctx!.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1)
- }
-
- function init() {
- resize()
- const w = canvas!.offsetWidth
- const h = canvas!.offsetHeight
- nodes.length = 0
- for (let i = 0; i < 30; i++) {
- nodes.push({
- x: Math.random() * w,
- y: Math.random() * h,
- r: Math.random() * 2 + 1,
- vx: (Math.random() - 0.5) * 0.3,
- vy: (Math.random() - 0.5) * 0.3,
- })
- }
- }
-
- function draw() {
- if (!canvas || !ctx) return
- const w = canvas.offsetWidth
- const h = canvas.offsetHeight
-
- ctx.clearRect(0, 0, w, h)
- time += 0.005
-
- // Update & draw nodes
- for (const n of nodes) {
- n.x += n.vx
- n.y += n.vy + Math.sin(time + n.x * 0.02) * 0.1
- if (n.x < 0) n.x = w
- if (n.x > w) n.x = 0
- if (n.y < 0) n.y = h
- if (n.y > h) n.y = 0
- }
-
- // Draw connections
- const maxDist = 150
- for (let i = 0; i < nodes.length; i++) {
- for (let j = i + 1; j < nodes.length; j++) {
- const dx = nodes[i].x - nodes[j].x
- const dy = nodes[i].y - nodes[j].y
- const dist = Math.sqrt(dx * dx + dy * dy)
- if (dist < maxDist) {
- ctx.beginPath()
- ctx.moveTo(nodes[i].x, nodes[i].y)
- ctx.lineTo(nodes[j].x, nodes[j].y)
- const alpha = (1 - dist / maxDist) * 0.08
- ctx.strokeStyle = `rgba(255,204,239,${alpha})`
- ctx.stroke()
- }
- }
- }
-
- // Draw nodes
- for (const n of nodes) {
- ctx.beginPath()
- ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2)
- ctx.fillStyle = "rgba(255,204,239,0.15)"
- ctx.fill()
- }
-
- animId = requestAnimationFrame(draw)
- }
-
- init()
- draw()
- window.addEventListener("resize", init)
-
- return () => {
- cancelAnimationFrame(animId)
- window.removeEventListener("resize", init)
- }
- }, [])
-
- return (
-
- )
-}
diff --git a/src/app/(main)/ai/components/how-it-works.tsx b/src/app/(main)/ai/components/how-it-works.tsx
index 7e1bcbd591..f27e81d10f 100644
--- a/src/app/(main)/ai/components/how-it-works.tsx
+++ b/src/app/(main)/ai/components/how-it-works.tsx
@@ -1,4 +1,4 @@
-"use client"
+import { clsx } from "clsx"
import { SectionLabel } from "@/app/conf/_design-system/section-label"
import SearchIcon from "@/app/conf/_design-system/pixelarticons/search.svg?svgr"
@@ -6,12 +6,11 @@ import CodeIcon from "@/app/conf/_design-system/pixelarticons/code.svg?svgr"
import PlayIcon from "@/app/conf/_design-system/pixelarticons/play.svg?svgr"
import CheckIcon from "@/app/conf/_design-system/pixelarticons/check.svg?svgr"
import {
- highlightGraphQL,
- highlightGraphQLSchema,
- highlightJSON,
- highlightPrompt,
- SYNTAX_CSS,
-} from "./syntax-highlight"
+ StepIntrospectionSnippet,
+ StepPromptSnippet,
+ StepQuerySnippet,
+ StepResponseSnippet,
+} from "./snippets"
const steps = [
{
@@ -20,10 +19,7 @@ const steps = [
icon: PlayIcon,
description:
'A user gives an AI agent a natural language instruction — "Show me Q4 revenue by region." The agent needs to access business data through an API to fulfill this request.',
- codeLabel: "User prompt",
- code: `> Show me Q4 revenue broken down by region
- for the top 5 performing product categories`,
- highlight: highlightPrompt,
+ Snippet: StepPromptSnippet,
},
{
number: "02",
@@ -31,23 +27,7 @@ const steps = [
icon: SearchIcon,
description:
"Using GraphQL introspection, the agent queries `__schema` and discovers the available types: `Product`, `Order`, `Region`, `RevenueMetrics`. It learns field names, arguments, and relationships automatically.",
- codeLabel: "Introspection result → discovered types",
- code: `type Product {
- name: String!
- category: Category!
-}
-type RevenueMetrics {
- amount: Float!
- region: Region!
-}
-type Order {
- product: Product!
- revenue: RevenueMetrics!
-}
-type Query {
- orders(from: Date!, to: Date!): [Order!]!
-}`,
- highlight: highlightGraphQLSchema,
+ Snippet: StepIntrospectionSnippet,
},
{
number: "03",
@@ -55,56 +35,25 @@ type Query {
icon: CodeIcon,
description:
"The LLM maps the user's intent to the discovered schema. It constructs a precise GraphQL query that fetches exactly the right data — revenue by region, top 5 categories, all in a single request — with no over-fetching.",
- codeLabel: "AI-generated GraphQL query",
- code: `{
- orders(from: "2024-10-01", to: "2024-12-31") {
- product {
- name
- category {
- name
- }
- }
- revenue {
- region {
- name
- }
- amount
- }
- }
-}`,
- highlight: highlightGraphQL,
+ Snippet: StepQuerySnippet,
},
{
number: "04",
title: "Structured response returned",
icon: CheckIcon,
description:
- "The API returns typed, predictable JSON that exactly matches the query shape. The agent processes the results with confidence — every field is validated, every type is known. No parsing ambiguity, no hallucinated or missing fields.",
- codeLabel: "Structured response (JSON)",
- code: `{
- "orders": [{
- "product": {
- "name": "Widget Pro",
- "category": {
- "name": "Electronics"
- }
- },
- "revenue": {
- "region": {
- "name": "North America"
- },
- "amount": 45230.50
- }
- }]
-}`,
- highlight: highlightJSON,
+ "The response is JSON in the query's shape. A nullable field can still come back null with an `errors` entry, but the agent already knows the shape, so it can use a partial result as-is.",
+ Snippet: StepResponseSnippet,
},
]
export function HowItWorks() {
return (
-
-
+
+
How it works
From natural language
@@ -115,71 +64,47 @@ export function HowItWorks() {
Here's what happens when an AI agent uses a GraphQL API to answer
a real business question — from initial request to typed response.
+
- {/* Dot connector for sm (2-col) */}
-
- >
- )}
-
- ))}
+ ))}
+
diff --git a/src/app/(main)/ai/components/interactive-demo.tsx b/src/app/(main)/ai/components/interactive-demo.tsx
index 7d3e1c2848..b7b9871611 100644
--- a/src/app/(main)/ai/components/interactive-demo.tsx
+++ b/src/app/(main)/ai/components/interactive-demo.tsx
@@ -1,653 +1,146 @@
"use client"
-import { useState, useRef, useEffect, useCallback } from "react"
-import { graphql } from "graphql"
-import { StarWarsSchema } from "@/components/interactive-code-block/swapi-schema"
-import { SectionLabel } from "@/app/conf/_design-system/section-label"
-import SparklesIcon from "@/app/conf/_design-system/pixelarticons/zap.svg?svgr"
-import CheckIcon from "@/app/conf/_design-system/pixelarticons/check.svg?svgr"
-import SearchIcon from "@/app/conf/_design-system/pixelarticons/search.svg?svgr"
-import CodeIcon from "@/app/conf/_design-system/pixelarticons/code.svg?svgr"
-import CloseIcon from "@/app/conf/_design-system/pixelarticons/close.svg?svgr"
-import { highlightGraphQL, highlightJSON, SYNTAX_CSS } from "./syntax-highlight"
-
-/* ─────────────────────────────────────────────
- Demo data
- ───────────────────────────────────────────── */
-
-type DemoPrompt = {
- id: string
- prompt: string
- icon: string
- query: string
- explanation: string
- discoveredTypes: string[]
-}
-
-const demoPrompts: DemoPrompt[] = [
- {
- id: "all-characters",
- prompt: "Find all Star Wars characters",
- icon: "👥",
- query: `{
- allHumans: __type(name: "Human") {
- name
- }
- search(text: "") {
- ... on Human {
- name
- height
- appearsIn
- }
- ... on Droid {
- name
- primaryFunction
- }
- ... on Starship {
- name
- length
- }
- }
-}`,
- explanation:
- "The AI used introspection to discover the Human, Droid, and Starship types, then composed a single query with inline fragments to fetch across all types — one request instead of three.",
- discoveredTypes: ["Human", "Droid", "Starship"],
- },
- {
- id: "luke-friends",
- prompt: "Show me Luke's friends and their starships",
- icon: "🚀",
- query: `{
- human(id: "1000") {
- name
- friends {
- name
- ... on Human {
- starships {
- name
- }
- }
- }
- }
-}`,
- explanation:
- "The AI followed the schema relationships: human → friends → Character. It used an inline fragment to conditionally fetch starships only when the friend is a Human, showing how GraphQL handles polymorphic types naturally.",
- discoveredTypes: ["Human", "Character", "Starship"],
- },
- {
- id: "r2-c3po",
- prompt: "What movies did R2-D2 and C-3PO appear in?",
- icon: "🤖",
- query: `{
- r2d2: droid(id: "2001") {
- name
- appearsIn
- }
- c3po: droid(id: "2000") {
- name
- appearsIn
- }
-}`,
- explanation:
- "Using GraphQL aliases, the AI fetched both droids in a single request — no over-fetching, no multiple API calls. Each alias produces a separately keyed result for easy processing.",
- discoveredTypes: ["Droid", "Episode"],
- },
- {
- id: "tallest",
- prompt: "Find the tallest Star Wars character",
- icon: "📏",
- query: `{
- search(text: "") {
- ... on Human {
- name
- height
- }
- }
-}`,
- explanation:
- "The AI requested only the fields it needs (name, height) from the Human type. This minimized payload — no unnecessary starship or planet data was transferred, unlike a typical REST endpoint.",
- discoveredTypes: ["Human"],
- },
- {
- id: "starships-feet",
- prompt: "Compare starship lengths in meters and feet",
- icon: "📐",
- query: `{
- search(text: "") {
- ... on Starship {
- name
- lengthInMeters: length
- lengthInFeet: length(unit: FOOT)
- }
- }
-}`,
- explanation:
- "The AI discovered the `length` field, then used field arguments and aliases to compute both units in one query. No client-side conversion needed — the schema handles it server-side via GraphQL arguments.",
- discoveredTypes: ["Starship"],
- },
- {
- id: "heroes",
- prompt: "Show me the hero of each episode and their friends",
- icon: "⚔️",
- query: `{
- newHope: hero(episode: NEWHOPE) {
- name
- friends { name }
- }
- empire: hero(episode: EMPIRE) {
- name
- friends { name }
- }
- jedi: hero(episode: JEDI) {
- name
- friends { name }
- }
-}`,
- explanation:
- "The AI composed three parallel queries using aliases and the `episode` argument. All three datasets return in a single network round-trip — REST would typically need 3+ sequential calls.",
- discoveredTypes: ["Character", "Episode"],
- },
-]
-
-/* ─────────────────────────────────────────────
- Step state machine
- ───────────────────────────────────────────── */
+import { useRef, useState } from "react"
+import { useInView } from "motion/react"
+import dynamic from "next/dynamic"
-type Step =
- | { kind: "idle" }
- | { kind: "introspecting"; prompt: DemoPrompt }
- | { kind: "types-discovered"; prompt: DemoPrompt }
- | { kind: "composing"; prompt: DemoPrompt; chars: number }
- | { kind: "executing"; prompt: DemoPrompt; query: string }
- | { kind: "result"; prompt: DemoPrompt; query: string; result: string }
-
-/* ─────────────────────────────────────────────
- Component
- ───────────────────────────────────────────── */
+import { SectionLabel } from "@/app/conf/_design-system/section-label"
+import { Button } from "@/app/conf/_design-system/button"
+import { useCodeAnimation } from "@/components/code-animation/use-code-animation"
+
+import UsersIcon from "@/app/conf/_design-system/pixelarticons/users.svg?svgr"
+import HumanIcon from "@/app/conf/_design-system/pixelarticons/human.svg?svgr"
+import RobotIcon from "@/app/conf/_design-system/pixelarticons/robot.svg?svgr"
+import ScaleIcon from "@/app/conf/_design-system/pixelarticons/scale.svg?svgr"
+import ShipIcon from "@/app/conf/_design-system/pixelarticons/ship.svg?svgr"
+import SwordIcon from "@/app/conf/_design-system/pixelarticons/sword.svg?svgr"
+
+import { demoPrompts, type DemoPrompt } from "./demo-prompts"
+
+const icons = {
+ users: UsersIcon,
+ human: HumanIcon,
+ robot: RobotIcon,
+ scale: ScaleIcon,
+ ship: ShipIcon,
+ sword: SwordIcon,
+} satisfies Record
+
+const DemoEditor = dynamic(
+ () => import("./demo-editor").then(m => m.DemoEditor),
+ { ssr: false },
+)
export function InteractiveDemo() {
- const [step, setStep] = useState({ kind: "idle" })
- const [isRunning, setIsRunning] = useState(false)
- const containerRef = useRef(null!)
- const mountedRef = useRef(true)
- useEffect(() => {
- mountedRef.current = true
- return () => {
- mountedRef.current = false
- }
- }, [])
-
- const activePrompt = step.kind !== "idle" ? step.prompt : null
-
- const runDemo = useCallback(
- async (prompt: DemoPrompt) => {
- if (isRunning) return
- const update = (next: Step) => {
- if (mountedRef.current) setStep(next)
- }
- setIsRunning(true)
-
- // Step 1: Introspection (show types being discovered)
- update({ kind: "introspecting", prompt })
- await delay(600)
- update({ kind: "types-discovered", prompt })
- await delay(1000)
-
- // Step 2: Composing (type out the query with a cursor)
- update({ kind: "composing", prompt, chars: 0 })
- for (let i = 1; i <= prompt.query.length; i++) {
- await delay(Math.random() * 12 + 5)
- update({ kind: "composing", prompt, chars: i })
- }
- await delay(250)
-
- // Step 3: Executing
- update({ kind: "executing", prompt, query: prompt.query })
- await delay(400)
-
- // Step 4: Result
- try {
- const execResult = await graphql({
- schema: StarWarsSchema,
- source: prompt.query,
- })
- update({
- kind: "result",
- prompt,
- query: prompt.query,
- result: JSON.stringify(execResult, null, 2),
- })
- } catch (error) {
- update({
- kind: "result",
- prompt,
- query: prompt.query,
- result: JSON.stringify({ error: String(error) }, null, 2),
- })
- }
- if (mountedRef.current) setIsRunning(false)
- },
- [isRunning],
- )
+ const sectionRef = useRef(null)
+ const panelRef = useRef(null)
+ const inView = useInView(sectionRef)
+ const hasEntered = useInView(sectionRef, { once: true })
+ const [selected, setSelected] = useState(demoPrompts[0])
+ const [editedQuery, setEditedQuery] = useState(null)
+ const [animationKey, setAnimationKey] = useState(0)
+ const animation = useCodeAnimation(selected.query, {
+ active: inView && editedQuery === null,
+ restartKey: animationKey,
+ initialDelay: 400,
+ })
+ const query = editedQuery ?? animation.code
return (
-
Interactive Demo
See GraphQL + AI in action
-
- Pick a prompt below and watch an AI agent introspect the schema,
- compose a precise GraphQL query, and fetch structured results — all in
- real time.
+
+ Pick a question an agent might get, and see the query it composes
+ against the Star Wars schema. The editor is live: change the query and
+ the response updates.