Topic 0.5
Build: Mitra v0
In one line
The first version of Mitra, the shop assistant this course builds: one question box, one streamed answer, a button that is disabled while an answer runs, and a clear message when something fails.
Think of it like this
A market stall on its first day. One table, one product, one person serving. It is small, but it is a real shop: people can ask, get an answer and pay. Each week the owner adds something. Mitra v0 is that first-day stall, and each phase of this course adds to it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Mitra
- The shop assistant built step by step through this course. The name means "friend".
- Controlled input
- A React input whose text lives in state:
valueshows the state andonChangeupdates it. - Submit handler
- The function a form runs when it is submitted, by clicking the button or pressing Enter.
- Busy
- A request is in flight: the status is waiting or streaming, and a new submit is refused.
- role="alert"
- An attribute that makes screen readers announce a message as soon as it appears.
- Scripted visitor
- A few lines at the end of an example that type and click for you, so the example demonstrates itself.
Step by step
01The route
The server checks the question and streams a reply. An empty question is refused with 400 before any "model" work. A question containing crash makes the handler throw, so you can practise the failure path whenever you like.
serve("/api/chat", async (req) => {
const { question } = await req.json();
if (typeof question !== "string" || !question.trim()) return Response.json({ error: "Please type a question." }, { status: 400 });
if (/crash/i.test(question)) throw new Error("model provider is unreachable");
const match = ANSWERS.find(([pattern]) => pattern.test(question));
return new Response(tokenStream(match ? match[1] : "I can help with orders, shipping and returns."));
});02The state
Five values. busy is derived from status, not stored separately, so it can never disagree with it.
const [question, setQuestion] = useState(""); // what is being typed
const [asked, setAsked] = useState(""); // the question that was sent
const [answer, setAnswer] = useState(""); // the answer so far
const [status, setStatus] = useState("idle"); // idle | waiting | streaming | done | error
const [error, setError] = useState("");
const busy = status === "waiting" || status === "streaming";03Sending
The form's submit handler guards first: no text, or already busy, and it returns. Then it clears the old answer, moves to waiting, sends the request, checks res.ok, and reads the stream, moving to streaming on the first piece and done at the end.
Using a <form> with onSubmit means both the button and the Enter key send, through one code path.
04Failing
One catch handles all three failure shapes from Topic 0.4. If some text had arrived (received is not empty), the message says the answer was cut off; otherwise it shows the message for the failure. Either way the status becomes error, which re-enables the form.
05The screen
Before the first question there is a hint. Then the user's question, "Mitra is thinking…" while waiting, and the growing answer with a cursor. The error line has role="alert". The input has an aria-label, because a placeholder alone is not a label.
06Watch it run
The example ends with a scripted visitor that asks two questions and then one that crashes the server. The console shows two answers and one failure; the page ends in the error state with the form ready to use again. Then type your own questions.
Try it yourself
- 1
Teach Mitra something new
Add a row to
ANSWERS:[/hours|open/i, "We answer messages from 9 am to 6 pm, Monday to Saturday."]. Run, then ask "What are your opening hours?".The row is a stand-in for what a real model would work out by itself. The screen code didn't change at all, which is the point: the UI doesn't care where the words come from.
- 2
Find the weak spot in the route
Mitra v0's route does
const { question } = await req.json();with no.catch. Compare it with the route in the other example. What would Mitra v0 answer to a body that is not JSON?req.json()would throw, so the visitor gets a500instead of a400. It still "works", but a 500 says your server is broken when in fact the request was bad. The second example shows the fix, and Phase 1 applies it. - 3
Slow the first token
Change the last line of the route to
tokenStream(…, { firstDelayMs: 1500 })and ask a question."Mitra is thinking…" now stays for a second and a half. Without that line the user would see a blank box and wonder whether the click registered.
Code & diagrams
The page disables Send for an empty question, but the route cannot rely on that. This route is a slightly hardened version of Mitra's: it survives a body of any shape. Phase 1 makes this the standard. Mitra v0, the whole page, is the next example.
Preview
Console
Nothing logged yet.Expected console output
the page's request: 200 Orders ship in 1 to 2 working days.
empty question: 400 Please type a question.
wrong type: 400 Please type a question.
no question field: 400 Please type a question.
not JSON: 400 Please type a question.The last few lines are a scripted visitor that types and clicks for you. After it finishes, ask your own questions: try "What tea do you sell?", an empty question, and the word crash.
Preview
Console
Nothing logged yet.Expected console output
answered: "Orders ship in 1 to 2 working days."
answered: "You can return any unopened item within 7 days of delivery."
(server) Error: model provider is unreachable
failed: Mitra could not answer. Please try again.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget to reset the status on failure
In the catch block of Mitra v0, delete the line setStatus("error");. Run and wait for the scripted visitor to finish.
Myth vs fact
Myth
A first version can skip error handling and add it later.
Fact
The failure path decides whether the form unlocks. Without it, one failed request leaves the feature dead until a reload, which is the first thing real users hit.
Myth
A placeholder is enough to label an input.
Fact
Placeholders disappear when typing starts and are not reliably read by screen readers. Use a <label> or aria-label.
Myth
The assistant needs a real model before the UI can be built.
Fact
A scripted stand-in is enough to build and test every state of the screen. Many teams build the UI against a fake first, on purpose.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Derive, don't duplicate:
busyis computed fromstatus. Each extra stored flag is a chance for two pieces of state to disagree after an error. Phase 4 moves this state into a reducer where the transitions are explicit. - ▸
Clearing the input on send feels right for chat, but if the request then fails the user has lost what they typed. Mature products restore the text on failure, or keep it until the first token arrives. Topic 3.4 covers retrying a failed request, and Topic 10.5 covers the prompt box and keeping what was typed.
- ▸
This version stores one question and one answer, so asking again overwrites the last exchange. That is a deliberate limit of v0, not a bug: a conversation needs a list of messages with ids, and a decision about how much history to resend (Phase 4).
Remember this
- 1
The problem this solves: Topics 0.1 to 0.4 showed the ideas separately. A real feature needs them together: input, a request through your own route, streaming, states and failure, in one small program you can keep growing. This topic assembles that program and sets out what the rest of the course adds to it.
- 2
Mitra is a help assistant for a small online shop, *Chai & Co*, which sells tea and snacks. Customers ask about orders, shipping and returns. The same product runs through all sixteen phases, so each new idea has a place to land, and the names stay the same: the route is
/api/chat, the input is#prompt, the button is#send, the conversation area is#messages, and errors appear in#error. - 3
Recall from Topic 0.4: the single
statusvalue and the reader loop are exactly what this component uses; Topic 0.2's server checks are what the route does. - 4
The server part. The route reads
questionfrom the request, refuses an empty one with400, and streams an answer. In this phase the "model" is a small table of patterns and replies, which is all a first version needs for you to build the screen. In a real app this is the one place you call the provider with your key (Topic 0.2). - 5
The browser part is one component with five pieces of state: the text being typed, the question that was asked, the answer so far, the
status, and an error message.statusis the single source of truth for what the screen shows and whether the button is enabled. - 6
Every scenario this small version already handles. *Empty input:* the button is disabled until there is non-space text, and the submit handler checks again. *Double submit:* a second submit while busy is ignored. *Waiting:* "Mitra is thinking…" appears before the first token. *Streaming:* text grows with a cursor. *A server failure:* a message a person can act on, and the controls unlock. *A broken stream:* the partial text stays and is marked as cut off.
- 7
What Mitra v0 can do: answer one question at a time, show the answer as it arrives, and fail politely. What it cannot do yet, and where it is added: recover with timeouts and retries, tell the failure types apart and limit very long questions (Phase 1); parse the real stream format providers use (Phase 2); be stopped halfway (Phase 3); remember the conversation (Phase 4); use an SDK instead of hand-written plumbing (Phase 5); format answers safely (Phase 6); show cards, call tools and ask for approval (Phases 7 and 8); and resist hostile input, stay within budgets and work for everyone (Phases 9 to 13).
Explain it without notes
Walk through what happens in Mitra v0 from pressing Enter to the answer finishing.
Which scenarios does Mitra v0 handle beyond the happy path?
Why is busy computed instead of stored in its own state variable?
What is missing from Mitra v0 that a real assistant needs?
Practice
Make Mitra v0 restore the user's text into the input when a request fails before any answer arrived, so they can retry without retyping.
Add a character counter under the input that shows n / 200 and disables Send above 200 characters. Where else must this limit be enforced?
Trade-offs
- ↔
One question and one answer keeps v0 small enough to read in one sitting, at the cost of not being a real conversation yet. The course adds complexity only when a phase needs it.
- ↔
A hand-written reader loop teaches what is happening and is fine for one stream. As features grow (stop, retries, tools), the plumbing multiplies, which is the case for an SDK in Phase 5.
Done when you can
I can explain the two parts of Mitra v0: the route and the browser component.
I can list the states Mitra v0 shows and what is on screen in each.
I can explain why the form stays locked if the error path forgets to change the status.
I can say why the route must validate even though the page already does.
I can name three things Mitra cannot do yet and the phase that adds each.