---
title: "A Web Dashboard"
description: "Run the dispatcher behind a browser dashboard. Understand how withEve mounts the agent same-origin and how useEveAgent drives it, then theme it for the shop."
canonical_url: "https://vercel.com/academy/building-agents-with-eve/a-web-dashboard"
md_url: "https://vercel.com/academy/building-agents-with-eve/a-web-dashboard.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-06-30T14:45:43.271Z"
content_type: "lesson"
course: "building-agents-with-eve"
course_title: "Building Agents with eve"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# A Web Dashboard

# A Web Dashboard

You've talked to the dispatcher in a terminal and over `curl`. Neither is something you'd hand a customer. The front door most people expect is a web page: a text box, a conversation, a send button.

Here's the good news you earned back in 1.3: a browser chat is *just another client* of the same HTTP session API you already drove by hand. Nothing about the agent changes. We're not rebuilding the dispatcher for the web, we're putting a nicer door on the routes it already serves. Two pieces make that door, and one command generates both: `withEve` wires the agent's routes into a Next.js app, and `useEveAgent` drives them from React.

So this lesson is less "build a chat UI" and more "understand the one you've got, then make it the shop's." The interesting front-door work, Slack and real auth, is the next two lessons.

## Outcome

The dispatcher answers in a browser dashboard, approvals included, and the page wears the Spoke & Mirror name instead of the scaffold's default.

## Hands-on exercise

**Add the web channel.** One command scaffolds the whole front end:

```bash
npx eve channels add web
```

That generates a Next.js app around your agent: a `next.config.ts` that mounts the routes, an `app/` directory with a chat component, and the React wiring. Run `git status` afterward to see exactly what it added, the file paths can shift between eve versions, so trust your generated tree over any path printed here.

**How the door is wired.** The generated `next.config.ts` wraps your config with `withEve`:

```ts title="next.config.ts"
import type { NextConfig } from "next";
import { withEve } from "eve/next";

const nextConfig: NextConfig = {};

export default withEve(nextConfig);
```

That one wrapper is the whole integration. `withEve` mounts the agent's `/eve/v1/*` routes onto your Next.js app's own origin. The browser never crosses a CORS boundary or reads an env var to find the agent, the routes are *there*, same-origin. In dev, `npm run dev` boots the eve runtime right next to `next dev` and forwards those routes to it.

\*\*Warning: npm run dev changed meaning\*\*

Until now you ran the agent with `npx eve dev`, the terminal UI. Now that there's a web app, `npm run dev` runs `next dev` and serves the dashboard at `localhost:3000`. The TUI is still there whenever you want it (`npx eve dev`); the web app is just the new default `dev`.

**Now, how the browser drives it.** Open the generated chat component. It leans on one hook:

```tsx
const agent = useEveAgent();
```

That single call opens a durable session, sends turns, streams replies, and hands you render-ready state. The component reads from it:

- `agent.data.messages`, the conversation (an `EveMessage[]` following the AI SDK `UIMessage` convention), mapped to message bubbles.
- `agent.status`, `"ready" | "submitted" | "streaming" | "error"`, used to disable the composer while the agent is working.
- `agent.send({ message })`, to send a turn. The same `send` carries your answer back when the agent stops for an approval.

The approval gate you built in 3.2 shows up here automatically. When `book_repair` parks on a big booking, the message renders approve/deny controls, and clicking one sends your answer back through the hook. You wrote zero approval-UI code; `useEveAgent` surfaces the parked request and the generated component renders it.

**Make it the shop's.** The generated app names the agent after your project. Give it the shop's identity instead. Open the generated chat component, `app/_components/agent-chat.tsx` in this eve version, and rename the agent (the constant the scaffold uses for the display name):

```tsx
const AGENT_NAME = "Spoke & Mirror";
```

Then set the page metadata in the app's `layout.tsx`:

```tsx
export const metadata: Metadata = {
  title: "Spoke & Mirror Dispatcher",
  description: "Book bike repairs at Spoke & Mirror Cyclery.",
};
```

\*\*Note: Match your generated files\*\*

The exact paths and the display-name constant come from whatever `eve channels add web` generated for your eve version. If your component names the agent differently, rename whatever it actually uses. A quick `git status` plus a grep for your project name points you at both spots.

## Try It

Boot the dashboard:

```bash
npm run dev
```

Open `http://localhost:3000`. You'll see the Spoke & Mirror chat: a header with the shop's name and a message box at the bottom. Click into the box, type a real customer question, and press Enter:

```text
my rear brake feels spongy on the way down the hill
```

The reply streams into the page, the dispatcher diagnosing in the same front-desk voice you wrote in 1.1. That's the "hey, it's working" moment: the agent you built and drove from a terminal is now answering in a browser, same brain, new door, and you didn't touch a single tool to get here.

Now run it through the rest of its paces:

1. Ask "what's a brake bleed cost?", the `lookup_service` tool runs and you get the real price, same as the TUI.
2. Ask it to book a flat repair, it books straight through.
3. Ask for the Full Overhaul, and the approval gate from 3.2 appears as **approve / deny buttons** right in the chat. Click approve and the booking completes; the turn resumed exactly where it parked, this time driven by a click instead of typing "approve."

Same dispatcher, same tools, same approval logic. The only new thing is the surface.

\*\*Note: The approval crossed surfaces unchanged\*\*

You wrote `approval` once, in a tool. It rendered as a text prompt in the TUI and as buttons in the browser, with no per-surface code. That's the portability payoff: behavior lives in the tool, and each channel renders it however suits the platform.

Blank page or a connection error in the console? Make sure you ran `npm run dev` (not `npx eve dev`, that's the TUI and doesn't serve the web app). If the chat loads but every message errors, the agent couldn't reach a model, the same credential check from 1.1 applies (`eve link` or `AI_GATEWAY_API_KEY`).

## Done-When

- [ ] `npm run dev` serves the dashboard at `localhost:3000`.
- [ ] The dispatcher answers in the browser and calls tools (you see real prices/slots).
- [ ] An expensive booking renders approve/deny controls, and approving completes it.
- [ ] The page and chat header show "Spoke & Mirror", not the scaffold default.

## Solution

`eve channels add web` did the wiring; `withEve` and `useEveAgent` came with it. Your only edits are the shop's identity, the display name in the chat component:

```tsx
const AGENT_NAME = "Spoke & Mirror";
```

and the page metadata in `layout.tsx`:

```tsx
export const metadata: Metadata = {
  title: "Spoke & Mirror Dispatcher",
  description: "Book bike repairs at Spoke & Mirror Cyclery.",
};
```

That's the point worth taking away: standing up a browser front end for an eve agent isn't a project, it's one command, a config wrapper, and a hook. The dispatcher you spent three sections building works in the browser without a single change to its tools, state, skill, or approval logic. Next we give it a second front door, Slack, and it'll be just as little new code.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
