Vercel Logo

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.

The browser chat uses the same HTTP session API you drove by hand in 1.3. The generated web channel gives those routes a customer-facing interface while leaving the dispatcher unchanged. Two pieces handle the integration: withEve wires the agent's routes into a Next.js app, and useEveAgent drives them from React.

In this lesson, you'll inspect the generated chat UI and give it the shop's identity. The next two lessons add Slack and real authentication.

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:

npx eve channels add web

That command generates a Next.js app around your agent, including a next.config.ts that mounts the routes and an app/ directory with the React chat. Run git status afterward to see what it added. File paths can shift between eve versions, so use your generated tree as the source of truth.

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

next.config.ts
import type { NextConfig } from "next";
import { withEve } from "eve/next";
 
const nextConfig: NextConfig = {};
 
export default withEve(nextConfig);

withEve mounts the agent's /eve/v1/* routes on the Next.js app's origin. The browser can reach the agent without crossing a CORS boundary or reading an environment variable. In development, npm run dev starts the eve runtime alongside next dev and forwards those routes.

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. You can still open the TUI with npx eve dev; npm run dev now defaults to the web app.

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

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):

const AGENT_NAME = "Spoke & Mirror";

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

export const metadata: Metadata = {
  title: "Spoke & Mirror Dispatcher",
  description: "Book bike repairs at Spoke & Mirror Cyclery.",
};
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:

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:

my rear brake feels spongy on the way down the hill

The reply streams into the page in the front-desk voice you wrote in 1.1. The agent is now answering in a browser without changes to any tool.

Now run it through the rest of its paces:

  1. Ask "what's a brake bleed cost?" The lookup_service tool runs and returns the same catalog price as the TUI.
  2. Ask it to book a flat repair. The booking goes straight through.
  3. Ask for the Full Overhaul. The approval gate from 3.2 appears as approve / deny buttons in the chat. Click approve to resume the turn and complete the booking.

Only the surface changed. The dispatcher, tools, and approval logic stayed in place.

The approval crossed surfaces unchanged

You wrote approval once in the tool. The TUI rendered it as a text prompt, while the browser used buttons. Each channel can render the same tool behavior for its platform.

Blank page or a connection error in the console? Make sure you ran npm run dev; npx eve dev opens the TUI and does not serve the web app. If the chat loads but every message errors, the agent could not reach a model. Use the credential checks from 1.1: 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:

const AGENT_NAME = "Spoke & Mirror";

and the page metadata in layout.tsx:

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

The generated channel gives you a working Next.js interface. The dispatcher's tools, state, skill, and approval logic remain unchanged. Next, you'll expose it through Slack.

Was this helpful?

supported.