Front end
The front end is a Vite 8, React 19 and Tailwind v4 single-page application in frontend/. The Worker serves the built dist/ as static assets and falls back to index.html for client-side routes. A second build turns the chat into a custom element for embedding (see Embedding).
The shell
App.tsx wraps everything in three providers and renders the shell:
| Route | Page |
|---|---|
/ | RandomClaimRedirect: a random claim in the region's currency, then /claims/:id |
/claims | ClaimsListPage: filters, search, 50 per page (My Claims on UK, Home on US) |
/claims/:id | ClaimPage: MatterHome (UK) or the US Claim page, with the demo hint and Next claim |
/data | ClaimsDataPage: what is loaded, models in use, the scenario catalogue |
/prompt-lab | PromptLabPage |
/status | StatusPage: how the pilot is built. Unlisted on purpose |
| anything else | redirect to /claims |
Two layouts over one view
Every number on a claim screen comes from deriveClaimView(claim) in claimpilot/claimView.ts: open and overdue tasks, the next critical deadline, money billed and unbilled, parties, the next hearing, the checks, recent diary. UK MatterHome and the US Claim page are layouts over that view. Neither computes a number of its own, so the two products cannot disagree about a claim, and a change to what "overdue" means happens in one place.
Both layouts carry the same data-panel anchors (home, finance, involvements, …), which is what highlight_panel points at. See Two products for what else differs by region.
The chat widget
The widget is given the claim; it never goes looking for one. Whichever page is displaying a claim hands it to the dock with setActiveClaim. That is exactly how an embedding host drives it too.
useChat (chat/useChat.ts) owns the conversation for one claim:
lines: the visible transcript (user, assistant, activity chips, errors), each assistant line carrying its lookups, turn id, model, prompt fingerprint and vote;history: the model-facing conversation, text only, kept in a ref;send(text, { capability?, promptOverrides? }): one streamed turn, sending the claim, the history, the browser's active Prompt Lab versions, the region's locale, the brand andscreenActions;feedback(lineId, action): thumbs and reasons, which go to/api/feedbackand never intohistory;upsertandhandleEvent: how the voice session writes into the same transcript.
Switching claim aborts any turn in flight and clears the conversation.
Where the chat sits
ChatDockProvider (chat/ChatDock.tsx) holds the placement (closed, popover or docked) and remembers it per browser. Opening never undocks a docked panel. DockPlacement decides what docking means: layout in the app (the shell gives the panel a 25% column and the page reflows), overlay in the embed (the panel pins itself to the viewport), none to remove the option.
Pointing at the screen
highlightPanel(panel) (chat/highlight.ts) finds [data-panel="<panel>"], scrolls it to the centre of the view, pulses it, then leaves an amber outline and wash that fade over 60 seconds, long enough to still be there when the reader looks up from the chat. Highlighting again restarts the clock. The marker colour is the same in both products so it never blends into the UK greens or US oranges.
What lives in the browser
There are no logins, so per-user state is kept in localStorage:
| Key | Holds | Owner |
|---|---|---|
claimpilot-region | uk or us | RegionProvider |
claimpilot-chat-mode | the demo's chat placement | ChatDockProvider |
claimpilot-embed-chat-mode | the embed's chat placement, kept apart from the demo's | EmbeddedAssistant |
claimpilot-prompt-versions | every Prompt Lab version, per prompt id | promptStore.ts |
All four are read defensively: unavailable or corrupt storage falls back to defaults rather than breaking the page. Clearing site data resets everything.
Styling
Tailwind v4 runs as a Vite plugin. Theme tokens live in src/styles.css: --color-cp-* for ClaimPilot, --color-chat* for the assistant, with US overrides under [data-region="us"]. That file is shared with the embedded element. src/index.css holds the app's own page-level rules and is never loaded inside the element.
Keeping open tabs current
Each build stamps dist/version.json. UpdatePrompt polls it every minute and offers a refresh when a new deployment lands.
The API client
lib/api.ts is a thin typed client. Every call is relative, so development (through the Vite proxy) and production are the same. An embedding host sets an API base and key through setApiBase and setApiKey. api.chat() is the SSE reader described in The chat pipeline.