Skip to main content

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:

RoutePage
/RandomClaimRedirect: a random claim in the region's currency, then /claims/:id
/claimsClaimsListPage: filters, search, 50 per page (My Claims on UK, Home on US)
/claims/:idClaimPage: MatterHome (UK) or the US Claim page, with the demo hint and Next claim
/dataClaimsDataPage: what is loaded, models in use, the scenario catalogue
/prompt-labPromptLabPage
/statusStatusPage: how the pilot is built. Unlisted on purpose
anything elseredirect 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 and screenActions;
  • feedback(lineId, action): thumbs and reasons, which go to /api/feedback and never into history;
  • upsert and handleEvent: 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:

KeyHoldsOwner
claimpilot-regionuk or usRegionProvider
claimpilot-chat-modethe demo's chat placementChatDockProvider
claimpilot-embed-chat-modethe embed's chat placement, kept apart from the demo'sEmbeddedAssistant
claimpilot-prompt-versionsevery Prompt Lab version, per prompt idpromptStore.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.