Skip to content

Final assignment

  • After the case studies
  • about 3 hr

What you will be able to do

Build a second screen for Larkfield on your own: a small ticket assistant for the billing team. It has a paginated list, a streamed draft view with Stop, the status of a background job and clear error states. Then prove four things with evidence: clear component and state boundaries, correct loading, error and cancellation states, a measured performance improvement, and interactions that work by keyboard.

Before you startWhat to know and to have readyAbout 180 minutes
  • You completed Modules 1 to 6. Each part below names the lessons that it uses.
  • You do not need the case studies for this assignment. The certificate asks for at least one of them: Fast search over a public dataset or Make an AI chat interface accessible.
  • The project: your ticket-desk at the end of Test and deliver, or a copy of the snapshot end-of-m06 (see the snapshots). npm test prints ℹ pass 27 and Tests 48 passed (48), and npm run test:e2e prints 6 passed.
  • Node.js 24 (LTS), VS Code and a Chromium-based browser with its developer tools.
  • No account, no model call and no money are needed. The AI drafts are recorded answers of real models from earlier courses, replayed by the mock API. Its slow, failing and interrupted answers are simulated. Say so wherever you show one.
  • You can work in more than one session. Run npm test after each change.

The situation: a screen for the billing team

Section titled “The situation: a screen for the billing team”

Grace’s agents use the desk every day. Now the billing team asks for a screen of its own. Its agents answer only open billing tickets: double charges, refunds, invoices. They want a short list of those tickets and the AI draft of the selected ticket as it is written. They also want a quick way to log a new billing ticket and see its AI work finish.

You build that screen in the same project, at the address #/assistant, in a new folder src/features/assistant/. The rest of the desk stays as it is.

You may reuse the parts of the desk that draw nothing. These are the API functions and types in src/api/ (listTickets, getTicket, createTicket, addMessage), and the hooks useTicket, useAddMessage, useDraftStream, useJobPolling and useJobActions. describeError, renderMarkdown and LiveRegion are yours to reuse too. You write the list query, because the billing list has pages of 20, and every component of the screen. You decide where each piece of state lives. Copying TicketList, TicketView, DraftPanel or JobStatus into the new folder is not your work: the reviewer looks for your own boundaries and your own reasons.

Uses React basics, Organize by responsibility, Local versus server state and Pagination and forms.

  1. Add the route. In src/router.ts, add { page: 'assistant' } to Route, and make parseRoute return it for #/assistant. Add a link Billing assistant to the header in App.tsx. Load the new screen with lazy(), as the labs page is loaded.
  2. The list. Show the open billing tickets (status=open, team=billing), in server pages of 20 with a cursor and a Load more button. Add a search box. An old search answer must never replace a newer one.
  3. The selected ticket. A click or Enter on a row selects it. Show its subject and the customer’s last message.
  4. The draft view. For the selected ticket, stream the AI draft with one Write a draft / Stop button. Show the eight states of the draft, as the desk does. Only done offers Use this draft, which fills a reply box. Show the sources, needs_human, and the confidence when it is not high. Show the finished draft’s Markdown only through renderMarkdown.
  5. A new billing ticket. A small form (customer ID, subject, message) sends POST /v1/tickets with an idempotency key. Then it follows the job by polling and says its six states in words. Offer Cancel while queued, and Try again only for dead_letter.
  6. Draw the component tree of your screen in docs/assistant.md, as in Who owns each value. For each piece of state, name its owner and its kind. The kinds are local state (the search text, the selected ticket), server state in the cache (the list pages, the ticket, the job), and the draft’s state machine. Give one sentence of reason for each.

Part 2: loading, error and cancellation states

Section titled “Part 2: loading, error and cancellation states”

Uses Query lifecycle, Streaming interfaces, Background processing and Trustworthy feedback.

Every state that the screen can be in needs its own words, its own buttons and a test. Write component tests with the fake API (installFakeApi, sseResponse, openStream) for at least these situations:

Part of the screen Situations to test
The list loading; the first page; Load more adds 20; no ticket matches; a busy server (503 with Retry-After) with Try again
The draft finished (done); stopped by the agent; an interrupted connection; an error event; a refused draft (invalid); a ticket without a draft (no_draft, with no Try again)
The job queued with Cancel; succeeded with the team; failed with no button; dead_letter with Try again
The forms a new ticket that the API refuses (invalid_request, with the field marked); a reply that fails to send (503), which keeps what the agent typed

Then check the same states in the browser, with the mock API’s simulated modes: --list fail, --stream interrupt, --stream error, --jobs fail, --jobs dead_letter and --writes fail (for the reply). Take one screenshot of each, and label each one simulated.

Stop must close the connection, not only change the words. Show it: after Stop, http://127.0.0.1:8100/admin/stats counts one more stopped_by_client.

Part 3: a measured performance improvement

Section titled “Part 3: a measured performance improvement”

Uses Measure the browser, Load less work and Architecture decisions.

Find one real wait in your screen, change one thing, and measure again in the same way. Choose one of these, or one of your own:

  • Drawing. Let the billing list keep every page that the agent loads. With the large data, load 1,000 rows or more, and compare drawing every row with drawing only the rows in view.
  • Download. Compare the first JavaScript file with your screen in it and with your screen loaded lazily. Use npm run size, and first rows on the phone-like profile.
  • Work while typing. Use the React Profiler on the profiling build (npm run build:profile) while you type in the search box or the reply box. Then move the state that causes the extra drawing.
  1. Write the question first, in one sentence: what the agent waits for, and how you will measure it.
  2. Measure the version before the change: a production or profiling build, never npm run dev; the same throttling every time; at least 3 runs; the median and the range.
  3. Make one change. Add no memo unless a measurement asks for it.
  4. Measure after the change, in exactly the same way.
  5. Add a row to docs/performance-note.md: the question, the computer, the conditions, the number before and after, and the change. If the gain is smaller than the spread between runs, say so.

The course’s own results can help you judge yours, but they are not targets. On the course team’s laptop, drawing every row of 10,000 took 274.3 ms, against 29.6 ms for the rows in view; up to 1,000 rows, there was no real gain. Code splitting moved 30.8 kB out of the first file: first rows came 144 ms sooner on the phone-like profile, and no sooner on a fast network. An honest “no real gain, and here is why”, with good measurements, can be demonstrated. A big number measured on the development server cannot.

Uses HTML and CSS essentials, Accessible interaction and Test and deliver.

  1. Do the main task by keyboard only: from the skip link to the search, to a ticket, to Write a draft, Stop or Use this draft, to the reply box. Write down where the focus is after each key press.
  2. Move the focus only where the agent asked for new content: the selected ticket’s heading, the result of a sent form. A job that finishes in the background gets a status message, never the focus.
  3. Give the draft one live region that says each change of state once. Never put the streamed text in a live region.
  4. In Chrome’s Accessibility pane, check that every control has an accessible name. Check that every text colour that you add has a contrast of at least 4.5 to 1.
  5. Add one end-to-end flow, e2e/assistant.spec.ts. By keyboard, it selects a billing ticket, writes a draft and uses it, and it checks the focus and the status at each step. It must pass on the desktop and on the phone screen.

Show each one with an output, a test or a screenshot, and say which mode of the mock API made it. Label every constructed or simulated case.

  1. Never the whole dataset. The Network panel shows one list request per page of 20, never 100 requests for all the tickets.
  2. Never an old search. With --list slow, type a word fast. The list shows the results of the whole word, and the requests for the shorter words were cancelled.
  3. Never half a draft as finished. With --stream interrupt, the partial text is grey, the words say that it is not complete, and there is no Use this draft.
  4. Never a secret in the browser. Put VITE_PROVIDER_API_KEY=demo-not-a-real-key in .env, build, and show that npm run check:bundle fails. Remove the line, build, and show that the check passes.
  5. Never code from a draft. Open the practice ticket T-80010 in your draft view. Its finished draft shows a formatted list, and no dialog opens.

Before you submit, prove that the project works from nothing:

  1. Copy the project to a new folder, without node_modules, dist and .env.
  2. Run npm ci.
  3. Run npm run check. It ends with the tests, the build, No secret found in … files of dist/. and the size report.
  4. Run npm run test:e2e. All the flows pass, yours included.
  5. Start npm run api and npm run dev, and open http://localhost:5173/#/assistant.

Paste the commands and the last lines of their outputs into your report. If a step needed something that is not in the README, add it to the README, and run the steps again.

One improvement to choose: the focus after Send reply

Section titled “One improvement to choose: the focus after Send reply”

Accessible interaction left one gap, and the handoff lists it as a known limit. After Send reply, the button is disabled while the reply is sent, and the focus falls to the page. A screen reader still hears “Sent.”, but a keyboard user starts again at the top of the page.

Fix this gap in your screen’s reply form, or in the desk’s MessageForm.tsx. Move the focus to the result, as the new-ticket form does. Prove it with a component test that checks the focus after sending, and with your end-to-end flow. Then remove the limit from docs/handoff.md. This improvement is optional. It can lift a keyboard item that is partially demonstrated, but it does not replace a required step.

Your project folder (without node_modules, dist and .env), with src/features/assistant/, its tests, e2e/assistant.spec.ts, docs/assistant.md, the new row of docs/performance-note.md and REPORT.md:

# Larkfield billing assistant
## Component and state boundaries (the tree; each piece of state with its owner, its kind and
why; what you reused from the desk, and what you wrote)
## Loading, error and cancellation (the states table with the test for each; the simulated
modes with a screenshot each; Stop closes the connection)
## Performance (the question; before and after, with the computer, the
conditions, the runs, the median and the range; the change)
## Keyboard and screen reader (the focus after each key press; the live region's sentences;
names and contrast; the end-to-end flow's output)
## Failure paths (each of the five, with its evidence)
## Fresh-environment run (the commands and their last lines)
## Limits (recorded and simulated answers, your computer and browser,
what you did not test)

A reviewer marks each required item demonstrated, partially demonstrated or not yet demonstrated, with a concrete reason. To complete the course, all four must be demonstrated. You can improve an item and submit it again.

Required item Demonstrated Partially demonstrated, for example Not yet demonstrated, for example
Clear component and state boundaries The tree in docs/assistant.md matches the code. Each piece of state has one owner and the right kind: server data in the cache, the search text and the selection as local state, the draft in its state machine. Each choice has a reason. The screen’s components are your own. The boundaries are right, but the document gives no reasons. Or one value is kept twice, for example the list in the cache and again in local state. Server data is copied into local state and goes out of date. Or the new folder holds the desk’s components with new names.
Correct loading, error and cancellation states Every situation in the table has its words, its buttons and a passing test. The simulated modes are shown on screen. Only a finished draft offers Use this draft, Try again appears only where it can help, and Stop closes the connection. Most states are tested, but one ending is missing. Or Stop changes the words, but the request stays open. A stopped or interrupted draft can be used as if it were finished. Or every error shows the API’s message with Try again.
Measured performance improvement A stated question, measured before and after in the same way: a production or profiling build, the same throttling, at least 3 runs, the median and the range. One change, and an honest note. The change is measured, but only once, or the conditions are not written down. Measured on the development server. Or “it feels faster” is the only evidence. Or memo was added everywhere without a measurement.
Keyboard-accessible interactions The main task works by keyboard only. The focus goes to the new content, the live region says each change of state once, every control has a name, and the end-to-end flow passes on both screens. The task works by keyboard, but the focus is lost after one step. Or there is no end-to-end flow. A step needs the mouse. Or the streamed text is in a live region. Or a control has no name.

Whatever else is strong, the work is not complete while a secret is in dist/, while a finished draft can run code from the model, or while the screen downloads every ticket to show one page. A fast or good-looking screen does not make up for any of them.

Common mistake

#/assistant shows the ticket list

Symptom: you open http://localhost:5173/#/assistant, and the desk shows its ticket list instead of your screen.

Cause: parseRoute returns the ticket list for every address that it does not know. It does not know assistant yet.

Fix: add a line before its last return: if (parts[0] === 'assistant') return { page: 'assistant' };. Add the page to the Route type, and add a check to src/router.test.ts.

Common mistake

The search shows the results of an old word

Symptom: with --list slow, you type “refund” fast, and the list shows the results for “r”.

Cause: the list loads its data in an effect without cleanup, or with a query key that does not hold the search. The slow answer for “r” arrives last and wins.

Fix: use a query whose key holds the search, as useTicketList does, and pass the query’s signal to listTickets. An old answer arrives last shows the race and the fix.

Recap

In this course, you built Larkfield’s ticket desk, one module at a time:

  • The web: HTML, CSS and JavaScript in the browser, then npm, Vite and TypeScript, and the CORS check between the desk and the API.
  • Components: React components and state, sorted by responsibility, with effects that clean up.
  • Server data: a cache with keys, retries and cancellation, pages with a cursor, and forms that are never sent twice.
  • AI states: a streamed draft with Stop and eight states, a background job in words, and drafts with their sources and their uncertainty.
  • Performance: measure first, then less to download and less to draw, and a written rendering decision.
  • Delivery: safe Markdown, no secret in the browser, a desk that works by keyboard, tests from the parts to one real flow, and a handoff with evidence.

If you have not done one yet, choose a case study for the certificate: Fast search over a public dataset or Make an AI chat interface accessible.