ysoseri.us

实习日志 / English / UI Refactoring / Main path first, shell last: how one front-end refactor discussion converged

Main path first, shell last: how one front-end refactor discussion converged

中文 | English

On 1 September I decided not to touch the front-end code yet, and to spend a whole day laying out the interface problems of both the claimant app and the staff app. The reason was simple: both front ends had grown to the point where nobody dared change anything, and every fix felt like robbing Peter to pay Paul.

The day left three things behind: an inventory of problems, a discussion draft that worked through nine groups of topics, and, the next day, an authoritative task document merging all the earlier material. Below, in the order of the discussion, is how each conclusion came about.

See the current state clearly first

For the inventory I used a few principles from the book Refactoring UI as the yardstick: design the feature before the shell, use hierarchy instead of decoration, limit choices, and set up a spacing and type system first. Measured against these, the problems lined up quickly.

The claimant home page offered six things at once: a natural-language input, three claim types, a step-by-step wizard that only worked for motor, resuming a saved report, login, and talking to a person. Having many features was not the issue; users did not know what to do first. Worse, a user who picked Home or Contents was then told the guided flow had not been configured. That option had no value for them and only created a letdown.

The first screen of the motor wizard asked for 9 fields: customer number, identity, contact preference, date, location, what happened, loss, plate and damage summary. The repository's own spec said "at most 5 fields per form group" and "one main question per turn". For someone who has just had an accident, the "smart guide" had degraded into a traditional long form.

A hard-coded Step 1 of 3 progress bar. Our product decides the next step dynamically from confirmed facts, missing information, claim type and current state. A fixed three steps made users think every claim has only three steps, and once a claim went to human assistance, waited for documents or waited for a third party, the bar could not express the real state.

Type was generally too small. The spec required claimant body text of at least 16px; in code, message text was 14px, author names and status notes 11px, form labels 12px, and helper text 10px to 12px. And among that "helper" text were error explanations, current status, next-step guidance and field sources.

Design values did not form a system. The claimant app defined only six local variables; other colours, spacing and radii were written straight into components: #2c806a, #246f5c, #edf8f3, and radii running from 7px, 8px, 9px, 10px, 14px, 15px all the way to 999px. The staff app was a single static HTML file with about 150KB of inline CSS, a dark purple sidebar, a dark blue-grey background and a teal accent; it looked like a product from a different company.

There were interaction problems as well. The date picker had role="dialog" but no Esc to close, no focus management and no full keyboard support; staff case cards showed the incident summary and next step only on mouse hover, invisible on touch screens; the staff sidebar mixed in dev demo buttons such as Load workbench demo queue and Reset and load demo queue; and the floating staff assistant was described in the docs as "a local, non-authoritative prototype interaction" while looking in the interface like a global assistant able to act on claims.

The inventory also recorded what was worth keeping: starting a claim in natural language, fields that can be checked and edited with their source shown, save and resume, human assistance status, sending and retry-on-failure states, staff messages to customers being edited and confirmed before sending, and writes carrying revision numbers and idempotency keys. The meaning of all of these was right; what needed redoing was the hierarchy and the visual expression.

First conclusions: there is only one main path

The discussion started from product direction. On the claimant side the Agent is the main product experience: the user first says what happened in natural language, the Agent understands, calls authorised actions and retrieval, generates the next step from the current state, and turns the result into something the user can understand, confirm or correct. Which suggestions may change real business state is decided by the runtime and backend.

That gave the first set of conclusions:

  • The claimant app is an Agent conversation workspace; the Agent is the main visual and the main entry.
  • The motor wizard and the fixed three-step flow leave the main path; the dynamic form sits inside the conversation and is generated from the current state.
  • Claim type stays as an optional drop-down next to the input. It is controlled by the Agent and the claim state: if the user has not chosen, the Agent can propose a type from the description; once the Agent confirms the claim has entered a branch, the drop-down locks; if the user's choice conflicts with confirmed facts, the interface explains why and guides the user, never overwriting silently.
  • The staff app is fully componentised; the single static HTML file is no longer a base to build on.
  • Both sides share one base design language, while density and layout emphasis may differ.

The Continue and Resume buttons were removed in this group. If conversation history and the claim session persist and restore correctly, continuing the conversation is continuing the claim, and opening a past conversation is resuming it. There is no need for another set of duplicate buttons creating extra branches.

Whether to keep the big headline on the home page was not decided by taste either. The test was whether it helps users understand the task in front of them.

Second group: the input area and dynamic fields

The copy of the main prompt took a while. GPT-style products often use a very neutral functional prompt asking what you need. We did not want to copy that, because here the prompt has two jobs: tell users they can just start talking, and make them feel this is professional, friendly help in an insurance setting. We ended with two layers: a brand line above, Understand insurance. Understand you better., and a short action hint inside the input, Tell us what happened.

The input area is one unified toolbar holding text, files, voice, accessibility mode and send. These are ways of giving input, and none of them is promoted to a new entry. With accessibility mode on, the Agent's replies are read aloud by default, with play, pause and stop controls, and browser autoplay restrictions have to be handled. File upload shows type and size limits, uploading, success, failure and delete, and tells users how the data will be used for this claim.

Whether users should see the dynamic fields took two rounds. The answer was yes, but only fields users can understand, confirm and change, with their current status and the necessary source; internal rules, confidence, tool calls and staff-only information are never exposed. On desktop it is a collapsible What we have so far panel; on mobile, an expandable Details captured block in the conversation. Each time the Agent updates fields, only what was added or changed this turn is highlighted; users can open a field to edit it, or simply correct it in natural language.

Date input got its own conclusion: it must not block the conversation. Users can just say "yesterday evening"; a date control appears only when an exact date is really needed, preferably the browser's native date input or a control that fully follows the ARIA APG. Adding an ARIA role alone does not make something accessible.

The traditional web form and human assistance stay, in one secondary navigation area, as text links. They look like text, but their clickable area must still meet the claimant app's touch target size.

The claimant app does not need a big navigation bar

Navigation was discussed separately once. The staff app has several workspaces and needs global navigation; the claimant app has one workspace, and the Agent conversation itself is the product entry. By Fitts's law, the main usable area should go to the conversation and the input. The profile can be an avatar menu near the screen edge with a large touch area, and history can open from a drawer close to the conversation. The "light top navigation" proposed earlier was only one possible place for a few tool entries, not a required structure for the claimant app.

Another point became an acceptance condition for every page: every page, drawer, tab and detail view has an understandable parent. Claim details go back to the claim queue; closing a claim tab goes back to the previous claim or the queue; the current conversation goes back to the history list; the traditional form goes back to the same claim's conversation. Leaving anywhere must not lose saved state or make users collect confirmed information again. What the back button looks like can be decided later; "you can get back to the previous view in the flow or in time" is fixed.

The front end expresses, it does not judge

When the discussion reached staff queue ordering, responsibility and risk alerts, I noticed the front-end discussion had started defining business rules for the backend. So I added a group in the middle just to draw the line.

The backend, based on code, specs, persistence models, APIs and tests, maintains stable business contracts: the definitions of workflow state, urgency, evidence, fraud signals and next actions; the priority and blocking relationships of missing fields; how risk signals, human review, queue ordering and the main next step are computed from each other; and which things are persisted facts and which are computed projections.

The front end designs hierarchy and interaction from the projections the backend provides, clearly expressing lifecycle, responsibility, missing information, risk alerts and the main next step. Where backend capability is not ready, it uses explicit loading, empty or unavailable states. The front end never computes priority, responsibility, risk conclusions, field blocking or business state on its own.

When a backend question came up in the discussion, we first checked the code, specs, APIs and tests, and sorted the question into three kinds: facts already implemented, designs written in docs but not yet in code, and questions not yet defined that need a product decision. Only the third kind needed discussing.

"Reliable" has to be proven by behaviour

Both sides share one product character: professional, friendly and clear, reassuring, respectful, and never creating needless tension. The hardest word to make real was "reliable". Our definition: never promise uncertain outcomes, but always give clear, honest help that lets the user keep going.

This means reliability cannot be created with colours and copy; it is built by concrete behaviour: showing what has been noted and what is missing, explaining why the next step is needed, separating information, suggestions, actions awaiting confirmation and actions already taken, keeping the conversation and claim context, explaining limits and giving a way forward on failure, and being explicit about safety, privacy and human assistance. On the claimant side this character shows as gentle, easy to understand and patient; on the staff side, as traceable, judgeable and actionable.

Login is something the backend has to make real

The login page needed redesigning, but we did not want authentication to stay at demo level just because we were using test accounts. The identity store at the time was an in-memory implementation for development and tests. A full user system needs at least server-side user records, securely stored password credentials, server-side session records, expiry and revocation, user profiles and communication preferences. A login error must not reveal whether an account exists. The front end and domain layer depend first on an authentication interface independent of the identity provider, so the production identity provider can be swapped later. A user's claim data links through customer_id and is not stuffed into the identity table.

Logged-out users can start a claim right away. At the start a random, unpredictable temporary session ID is generated, representing only this visit. Only if the user logs in while that session is still valid does the server link it to the account, and the link must be verified on the server, one-time, idempotent and audited. If the user leaves without logging in, the temporary session stays orphaned; nobody can claim it with a random ID, and after it expires or is cleaned up it cannot be restored as the user's claim.

Merged into one authoritative document

The next day I merged all the earlier material, six documents in total, into one authoritative task document: the problem inventory, the discussion draft, the visual spec, the entry page design, the remediation notes on the login journey, and the reassessment notes. The HTML prototype was kept only as a visual and interaction reference, not a business contract.

The first thing written into the merge was the rule for conflicts, resolved in this order:

  1. decisions the user explicitly confirmed in the latest discussion;
  2. final conclusions already organised in this document;
  3. HTML prototype behaviour that has not been overturned;
  4. suggestions from older discussion drafts.

Right after that came one sentence: old documents must not become a second spec. Implementation, tests and code review all follow this one; where code disagrees with it, record a gap rather than stacking more override styles on top. I wrote that because we had paid for it the week before: old styles were covered layer by layer by later CSS, and wherever the covering failed, they leaked through.

The document also finished two things the discussion draft had not.

Control categories and a state model. Every control must belong to a category and take its styles from shared tokens: navigation, entry and auth, input bar, primary action, secondary action, field editor, service card, status, overlay, and staff Agent, ten categories in all. Every category defines at least eight interaction states, default, hover, focus-visible, active, disabled, loading, error and success, and business states are defined separately: confirmed, proposed, pending, new, incomplete, handoff and unknown.

How a state is expressed is fixed as one combination:

status text + icon or symbol + semantic colour + an actionable next step

An error message must answer three questions: what happened, why, and what can be done now. "Outcome unknown" is handled on its own: prompt the user to resync or retry safely, and never show it as a failure or as a success.

Tokens set to executable values. The colour table was tightened from the design draft: the accent moved from #5F8F80 to #7A9E8E, and the old greens #2C806A, #246F5C and #EDF8F3 were all retired. The other groups:

CategoryTokens and values
Spacing--space-1 4px, -2 8px, -3 12px, -4 16px, -5 20px, -6 24px, -8 32px, -10 40px, -12 48px, -16 64px
Radius--radius-sm 4px, -md 6px, -lg 8px, -xl 10px; --radius-round 999px only for avatars and status tags, never for main actions
Border--border-region 2px, --border-component 1.5px, --border-focus 1.5px accent, --border-action 1.5px accent-light, --border-danger 1.5px danger
Shadow--shadow-none; --shadow-subtle 0 1px 3px rgba(26,24,20,.08); --shadow-popover 0 8px 24px rgba(26,24,20,.14); --shadow-modal 0 16px 40px rgba(26,24,20,.18)
Weightregular 400, medium 500, semibold 600, bold 700 (only for strong states and clear warnings)

Buttons have six levels: primary is the one main action at the moment, secondary is a lesser but executable action, tertiary is low emphasis, text is for secondary navigation and edit links, danger is for deletion and irreversible actions, and disabled lowers contrast but keeps the explanation of why.

Each business state also got colours and behaviour:

StateTextBackgroundRequirement
confirmedaccent-strongaccent-softShows it is noted and the user can continue
proposedtext-mutedpage or surfaceMust let the user confirm or change it
pendingtext-faint, italictransparentShows waiting, without feeling like an error
new / changedaccent-strong plus a new markeraccent-softOnly marks what was added or changed this turn
incompletetext-secondary plus explicit textwarning-soft or surfaceStates what is missing and what the next step is, never hidden
handofftext-secondary plus status textsurfaceStates who is responsible, the saved state and the next step
errordangerdanger-softStates what happened, why, and an actionable fix
unavailabletext-mutedpanel or pageTells the user plainly that this capability is temporarily unavailable

For type size, 14px may not be used for error explanations, current status, next steps, field sources or main actions. The staff app may use 14px for dense metadata, but business text, risk, missing information and next steps may not go below 14px. 10px to 13px is no longer used for any main information.

The staff app: hierarchy first, layout not final

The staff app was also discussed at length in this round. The main line was that when staff open the workbench, the first screen must answer "which claim needs handling, why, and who does what now", with information in three layers: work summary, what can be acted on now, and reference material. Fraud is only a risk tag, not a default queue of its own, so that a judgement the Agent has not confirmed does not become a staff member's preconception. Unfinished reports are real claims with low priority, but they are not collapsed or hidden. The staff Agent is a workbench-level global capability, and typing @agent inside the customer conversation was dropped.

These conclusions later went into a separate, complete workbench refactor document. At the time I deliberately left one thing open: the workbench's final layout is not locked into three columns or a fixed sidebar by a sketch. First decide the content from the functions and the state machine, then decide the spatial relationship of sidebar, queue, details, tabs and the global Agent.

Order

The discussion draft ended with an order of discussion, which I think matters more than any single conclusion: first confirm what the product claim concretely means on each side, then confirm the overall state machine of claim branches and states, then the claimant main path, the staff information hierarchy and the tone shared by both, after that tokens, state semantics and the error model, and only at the end break the work into implementation tasks.

The authoritative document's implementation order followed the same idea: fix page state and where users land after login first, then build the shared tokens, then split components, build the claimant workspace, fill in states and help behaviour, clean up old code, and only then the staff workbench and the responsive and accessibility acceptance.

Before this round we were used to drawing an interface first and then stuffing features into it. After it, the order was reversed: the main path comes first, the shell last.