ysoseri.us

实习日志 / English / UI Refactoring / Where did the user go after logging in: a claimant front-end refactor

Where did the user go after logging in: a claimant front-end refactor

中文 | English

On 2 September I walked through the most ordinary flow on the claimant side: open the page, log in, start a claim. By the end I found that the login step itself lost people.

A small bug

At the time, a successful login or sign-up sent the page to a Your account page with a Back to claims button. Clicking it took you to the entry page, and the chat workspace you had just been in was gone.

Users were confused three times in a row here: they could not tell whether login had worked; they thought they were still stuck in the login and sign-up flow; and they could not find the claim they had just started or the earlier conversation.

It went wrong the other way too. A user already chatting in the workspace who clicked the profile at the bottom left sometimes saw nothing change at all. The account page only rendered when "no claim has started yet"; once a claim had started, clicking the profile changed some state and nothing on the page.

Both bugs pointed to the same thing: what the page showed was inferred implicitly from a boolean called hasStarted. None of the views, entry, login, sign-up, workspace and account, was explicitly defined as a state.

Dig further and it is the structure of the whole front end

Following that thread, the problem turned out to be much bigger than login. The old claimant app had been built up piece by piece: a marketing-style entry page, a fixed web form, a step-by-step motor claim wizard, polling refreshes, control buttons for dev demos and a chat workspace, all piled into one HTML file and one set of page logic. The result:

  • the Agent never became the main entry; users first had to guess which preset flow the system expected;
  • components, styles and business state were tangled together, so changing one button could break another page;
  • page state was held together by local variables and the browser's back button, so a refresh or going back could land on a page that did not match;
  • internal fields such as delivery, audience and retry reason leaked straight into the user interface;
  • static fixture data and the old layout hid problems in the real API, login, evidence upload and human handoff paths.

So the goal of this refactor was set clearly: stop adding features to the old page, and turn the claimant side into a front end built around the real user journey and real backend state, one that can be verified again and again.

Draw the journey first

I set a few principles first. The Agent is the main visual and the only main entry; users start in their own words, without knowing insurance terms or following a fixed questionnaire. The dynamic form on the right is a projection of the backend state machine and field registry; users see what has been noted so far but are not led by a fixed list of questions. The web form, talking to a person, file upload, voice and accessibility all stay, as secondary entries that do not compete with the Agent.

For login, the principle was: people who are not logged in must not be blocked. They can send messages, upload files and ask for human help. What they lack is only a long-term profile, history and long-term file storage.

With that principle, the target journey was:

Logged-in user: open the page → straight into the Agent workspace
Logged-out user: open the page → can start chatting right away
Logged-out user sends a message → stays in the Agent workspace
Logged-out user clicks the profile at bottom left → shows "Log in to save your chat"
Click log in → overlay or dedicated auth view
Auth succeeds → back to the current workspace, and the current temporary session (if any) is linked to the account

Your account is no longer a stopover after login. Account settings can only be reached from the profile, and going back returns to the view you came from. Resume a claim appears only when there really is an unfinished claim that can be resumed.

Three parts of this journey needed to be spelled out in the implementation.

Anonymous sessions. When a logged-out user sends their first message, the front end generates a random anonymous session and creates the claim at the same time. The anonymous identity goes in the X-Northwind-Anonymous-Session request header; an empty bearer token is never sent to pretend the user is logged in.

Transfer after login. If there is an anonymous claim at login time, after successful auth the promotion endpoint moves the claim, session, messages, evidence and related records to the account. If the transfer fails, the current conversation must not be silently lost.

Login state must be restorable. Login state cannot live only in a JavaScript module variable, or a refresh wipes it. When users open the page again they should not have to log in every time, just as with the ChatGPT and Claude web apps. So the front end has to handle three things: restoring a session, an expired session, and logging out on purpose.

Make the URL and the page state say the same thing

The real fix for the hasStarted bug was to let each page be decided by the URL and React state together:

URLPageWhen it may be entered
/Entry, anonymous startNo claim in progress, or the user chose to go back to the entry
/auth/loginLoginNot yet authenticated; may carry an anonymous claim's context
/auth/registerSign-upSame as above
/claims/{claim_id}Conversation workspaceThe current claim in memory matches the claim_id in the URL
/accountProfileAuthenticated; opened as a tool view inside the workspace, not a modal
/how-it-worksTutorialNo claim started yet, or opened by an explicit helper action

A route table alone was not enough; navigation itself needed guards:

  • setPage must update React's page state and the browser history together, never just the address bar.
  • Browser popstate passes permission and context checks before it is accepted; an invalid path is immediately replaced with the route the user should be on.
  • Visiting /account without auth goes to login; visiting login or sign-up while authenticated goes back to the chat page.
  • After a claim has started, the back button cannot take the user to an entry state that would overwrite the current workspace.
  • Logging out is an explicit lifecycle operation: clear the account, claim, session, messages, fields, attachments, handoff state and tool views, then replace to /.
  • Auth pages, the entry page, the workspace and tool pages use mutually exclusive render branches, so no background layer is left over across pages.
  • On refresh, the URL must not be taken to mean "the business state has loaded". If neither memory nor the backend has the matching context, go back to a safe entry or explicitly reload the claim projection; never render half a page.

Split the components, and remove the old things

A set of things in the old code was either deleted or replaced:

Old thingActionReasonReplaced by
Going to Your account after loginDeletedInterrupts the main task, login state unclearStraight into the workspace
Back to claims returning to the entry pageDeletedWrong destination, loses the workspace contextBack to the view before the account page
Marketing-style entry with a green gradientDeletedClashes with the warm tone we choseWarm layers with a single sage accent
Step-by-step motor wizard entryDeletedClashes with the Agent-led adaptive conversationOne natural-language input, driven by the Agent
Three claim-type cardsDeletedTurned reporting into a multiple-choice questionAn optional claim-type drop-down next to the input
Unicode placeholder buttons for files, voice, accessibilityReplacedPlaceholders cannot serve as featuresReal components and states; unimplemented ones show the real reason they are disabled
A flat list of fieldsReplacedCannot express the business hierarchy of the dynamic formGrouped display with source, status, new marker and editing

The front end moved to a Node, Vite and React structure, and JSX, business logic, CSS and routing are no longer stuffed back into one static HTML file. Components are split by responsibility: MessageComposer for input (text, attachments, voice, claim type, model choice, sending state), ExternalServiceAction for third-party services (service description, consent, request, result and limits), plus the conversation, field panel, history, auth pages and tool pages. Each component owns only its own display and event interface; claim ownership, revision numbers, authentication, action execution and persistence stay in the API and service layers.

The input bar is one example. The text area, attachment cards and toolbar belong to one surface, without separate frames and without extra dividers between them. It starts about two lines tall, grows with text and attachments, and has a maximum height. The toolbar has a round attachment button, voice, optional claim type and model choice on the left, and send on the right. For logged-out users none of voice, read-aloud, send or upload is disabled; only capabilities that need long-term storage prompt them to log in.

Tokens have to be executable

The visual spec had lived in the design document as tables of colour values. This refactor added a requirement: tokens must be the single executable source in code. The claimant and staff apps share one set of base tokens; their density and layout may differ, but neither may keep its own copy. The file sits where both front ends can import it:

frontend/shared/design-tokens.css
frontend/shared/design-tokens.js   # only if JS needs to read them

A few constraints:

  • no hex colour values directly in component CSS;
  • one meaning has one token; no keeping several "success greens" at once;
  • tokens are named by purpose: --color-action-primary yes, --green-button-1 no;
  • state tokens define text, background, border, and focus and hover behaviour together;
  • state meanings are unified into one set: confirmed, proposed, pending, new, incomplete, handoff, error, success, disabled;
  • after removing old tokens and hard-coded values, leave a lint or test rule so they cannot come back.

When the tokens went into code, a few values were adjusted from the design draft: the accent went lighter from #5F8F80 to #7A9E8E, a card surface colour #FFFDF9 was added, and a warning colour #A3682F was added. Sizes became a minimum of 14px, body 16px, messages 18px, section headings 20px, page titles 24px and the hero 28px.

Front-end changes pull on the backend contract

This refactor was not a reskin. It changed route state, the auth API, the claim and session contracts, and the metadata needed to display fields. The front end depends on the backend for:

  • an auth session the browser can restore, with a clear signal to the front end when it expires;
  • anonymous claims and sessions that can be linked to a user after login;
  • claim history that returns a title, summary, last update and whether it can be resumed;
  • every field carrying its group, source, status, whether it is new or changed, whether it can be edited, and which branch it belongs to;
  • once the Agent confirms a claim branch, the front end knowing that the branch is locked;
  • handoff and third-party service states delivered by the backend as messages or status cards, without the front end guessing.

On the database side this means persistent sessions with expiry, the link status between anonymous sessions and users, can_resume and last activity on claims, and field metadata. None of these may be assembled on the fly to make the front end look good. Where the backend has no reliable source, the front end clearly shows that the thing is unavailable and never fakes a finished state.

Tests follow the journey

Most old tests still tested the motor step form and did not match the new main path; they were deleted or moved into an explicit legacy fixture scope. New tests were written by journey, for example:

  • sending a message without logging in enters the workspace and creates a temporary session;
  • logging in during the same visit links the anonymous claim, and the messages are still there;
  • a successful login does not go to Your account;
  • opening the account page from the profile and going back returns to the same workspace;
  • login state is restored after a refresh, and an expired session goes back to logged-out;
  • Resume a claim appears only when a resumable claim exists;
  • refresh, forward, back and direct URL access never show the wrong page or a leftover background layer;
  • claimants never see internal fields such as audience or delivery.

Order and next steps

The order of work followed the dependencies too: first stabilise routing, auth, the anonymous claim transfer and the session lifecycle; then split the components; then move all colours, spacing, states and buttons onto the shared tokens; then connect evidence, retrieval, database and third-party services to the real API; finally verify the three paths (motor, home, contents) in parallel, without waiting for motor to finish before expanding.

This spec later became the reference for the staff workbench refactor. I left the workbench one constraint: do not copy the claimant app's three-column layout; first decide the functions and information hierarchy by the order in which staff handle a claim, then decide what the shell looks like.

The whole thing started from a small "wrong page after login" bug. Digging in showed that what looked like an interface problem was really a state model that had never been clearly defined, and once that state model has to be defined clearly, it pulls all the way through to authentication, sessions and the backend contract.