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
Open the web channel. You scaffolded this front end in 1.1 with --channel-web-nextjs and deliberately left it alone while building the agent. It includes a next.config.ts that mounts the routes and an app/ directory with the React chat. Run git status and inspect those files now. 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:
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.
Now, how the browser drives it. Open the generated chat component, app/_components/agent-chat.tsx. It leans on one hook:
const agent = useEveAgent({
agent: WEB_CHAT_AGENT,
prewarm: hasInputText,
// ...plus options that resume a session from the /s/<sessionId> URL
});That single call opens a durable session, sends turns, streams replies, and hands you render-ready state. The component reads from it:
agent.messages, the conversation as eve's ownEveMessage[], mapped to message bubbles. Each message's parts aretext,reasoning,file,tool, andinteraction. These are eve's types, not AI SDKUIMessages, so adapt them first if you hand them to an API that expectsUIMessage[].agent.status,"idle" | "busy" | "resuming", which describes outstanding work. The composer disables itself while the hook is"resuming"a session. Failures arrive separately onagent.error.agent.view, an immutable snapshot of the session. The component asks it questions with helpers fromeve/events, such asactiveTurn(agent.view)to decide whether a new message should steer a turn that's still open.agent.send(message), to send a text turn (or pass an array of text and file parts for richer input).agent.respond(answers), to answer a paused approval or other interaction.agent.cancel(), to cancel the active turn.
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. In app/_components/agent-chat.tsx, change the default display name:
const DEFAULT_AGENT_NAME = "Spoke & Mirror";The line below it, const AGENT_NAME = WEB_CHAT_AGENT ?? DEFAULT_AGENT_NAME;, stays as it is. WEB_CHAT_AGENT only has a value in a multi-agent workspace, so our single dispatcher uses the default.
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.",
};Try It
Boot the dashboard:
npm run devOpen http://localhost:3000. You'll see the Spoke & Mirror chat: the shop's name and a message box. Click into the box, type a real customer question, and press Enter:
my rear brake feels spongy on the way down the hillThe reply streams into the page in the front-desk voice you wrote in 1.1, and the address bar changes to /s/<sessionId>. That's the same session ID you copied by hand in 1.3, so reloading the page resumes the conversation instead of starting over. The agent is now answering in a browser without changes to any tool.
Now run it through the rest of its paces:
- Ask "what's a brake bleed cost?" The
lookup_servicetool runs and returns the same catalog price as the TUI. - Ask it to book a flat repair. The booking goes straight through.
- 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.
Blank page or a connection error in the console? Make sure you ran npm run dev; npm run dev:eve 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 devserves the dashboard atlocalhost: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
The web scaffold from 1.1 did the wiring; withEve and useEveAgent came with it. Your only edits are the shop's identity, the default display name in app/_components/agent-chat.tsx:
const DEFAULT_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?