63. Mini project: Support Desk — a working agentic web app
Everything in this Part comes together in Support Desk, a customer
support app where a signed-in user chats with an agent that looks up
orders and proposes refunds, which the user approves in the UI. It has no
dependencies at runtime: a node:http server and plain ES modules. The
full source is on the companion page
Support Desk — full source, and it lives in the
wiki at JS/projects/support-desk/.
┌─────────────────────────── browser ───────────────────────────┐
│ index.html CSP: script-src 'self'; require-trusted-types… │
│ app.js login · orders · chat · idle-session dialog │
│ api.js fetch wrapper (+CSRF header) · SSE reader (§59) │
│ agent-ui.js component registry + prop validation (§61) │
└───────────────┬───────────────────────────────────────────────┘
│ POST /api/chat (cookie + X-CSRF-Token)
▼ text/event-stream: run-started, tool-call, text-delta, ui, run-finished
┌─────────────────────────── server.mjs ─────────────────────────┐
│ sessions (HttpOnly cookie, scrypt passwords, CSRF) §67 │
│ access control: you only see/refund your own orders §65 A01 │
│ rate limits: failed logins, chat per session §66 │
│ agent: picks a tool, streams words, emits UI events §60–§61 │
│ one error handler: generic message + request id §65 A10 │
└────────────────────────────────────────────────────────────────┘
Run it:
cd JS/projects/support-desk
npm start # http://127.0.0.1:3100 · alice@example.com / correct-horse-1
npm install && npx playwright install chromium
npm test # 26 E2E tests (§74–§78)
Try these in the chat: "where is A-1002?" (an order card), "list my orders" (several cards), "refund A-1001" (an approval card: approve it and watch the sidebar update), and "where is B-2001?" (Bob's order: "I couldn't find it", the same answer as for an order that doesn't exist).
The model is a stand-in on purpose. runAgent() in server.mjs is a
deterministic rule-based "model", so the app and its tests run offline and
the same way every time. Swapping in a real model changes one function,
not the protocol:
- On the server: replace
runAgentwith §60's loop, using amodel.complete()that calls your OpenAI-compatible server. Map its tool calls totool-callevents, its text totext-deltaevents, and alookupOrderresult to auievent withcomponent: "order-card". - In the browser: run §58's WebLLM engine in a worker, let it pick the
tool with constrained JSON, and call the same
/api/ordersendpoints with the user's session. The server stays the authority: it checks ownership and CSRF on every refund, whoever asked. - At the edge: put §62's gateway in front of the model so the key never reaches the browser.
🎯 Challenges. (1) Add a
track-shipmentcomponent with a progress bar, from schema to Playwright test. (2) Persist the conversation in IndexedDB (§45) so a reload restores it. (3) Add a "regenerate" button that aborts the current stream and replays the last user message. (4) Put §57's embedding search behind asearchHelpCentertool.