Topic 10.4
Empty States and Suggested Prompts
In one line
An empty chat with only a text box leaves people unsure what to ask. A good first screen says what the assistant can and cannot do, offers a few real starter questions that send exactly once, and handles the first visit without breaking when browser storage is blocked.
Think of it like this
Walking into a shop where the counter is empty and nobody speaks. You do not know whether to ask for help or wait. A friendly sign, "Ask me about orders, delivery and returns", and three examples of questions turn a confusing silence into a start. An empty chat box is that silent counter.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Empty state
- What the screen shows when there is nothing to show yet, such as a chat with no messages.
- Starter prompt
- A suggested question shown as a button, which sends or fills in a full prompt.
- Onboarding
- The first-visit guidance that tells a new user what the product does and how to use it.
- First-run hint
- A short tip shown once to a new user and remembered afterwards.
- Ref
- A React
useRefvalue: a box whose content changes immediately and does not cause a re-render. - Stale state
- A state value read inside a function before React has updated it, so it still shows the old value.
- SecurityError
- The error a browser throws when storage or another feature is blocked by privacy settings.
- setMessages
- A
useChatfunction that replaces the message list locally.setMessages([])starts an empty chat.
Step by step
01Write the welcome line
One sentence for what Mitra does, one for what it does not do. No exclamation marks, no "I am your AI-powered assistant". Say the facts plainly.
02Offer a few starters that you have tested
Choose questions your assistant handles well. Put them in a list with an accessible name (aria-label="Suggested questions") and real <button> elements, so keyboard and screen reader users can reach them like any control.
03Send once, even on a double click
In the first example the scripted visitor clicks the "Return something" starter twice in the same instant. The server logs one question. The ref used is set in the click handler, so the second click returns before it can send.
When the chat is emptied (the example calls setMessages([]) once the first answer is done), the ref is reset and the starters are visible again. The last log line shows starters visible: true | messages: 0.
04Build the list from facts
The second example's startersFor takes what the app knows (signed in? last order?) and returns at most four unique questions. Read the three outputs: the order starter appears only for a signed-in user with an order, and never for a signed-out visitor, even if an order id happens to be in memory.
05Remember a first-run hint safely
firstRun(storage) returns whether to show the hint. With working storage it is true once and false afterwards. With blocked storage it is true every time and nothing throws. The example passes a storage object that throws, to prove the second case; the browser's own localStorage throws in the same way when blocked.
06Translate the first screen last, not never
The welcome line and starters are the first things a user reads, so they are the first things to translate. Starters are not model output: you control them, so you can keep a translated list per language (Topic 10.7).
Try it yourself
- 1
Fill instead of send
In the first example change the starter's click to put the prompt in a text box instead of sending it: add
const [draft, setDraft] = useState(""), an<input id="prompt" value={draft} onChange={(e) => setDraft(e.target.value)} />, and useonClick={() => setDraft(s.prompt)}.Nothing is sent until the user presses Send, so there is no double-send risk, but the user needs a second action. Use filling when the user has to complete the question.
- 2
Add a starter for an order the user does not have
In the second example call
startersFor({ signedIn: true, lastOrderId: "" }).An empty string is falsy, so the order starter is skipped. Test the empty value as well as
null: bad data from a server often arrives as"". - 3
Fit the first screen on a phone
Write your welcome line on a phone-width screen (390 px). If it needs scrolling before the starters are visible, shorten it.
The first screen should fit without scrolling: the welcome line, starters and input box all visible at once.
Code & diagrams
The visitor clicks a starter twice in the same instant, and the server gets one question. The messages go to 0 again at the end because the example presses New chat after the first answer, and the starters return. Remove the `used` guard to see the double send (the first Break it).
Preview
Console
Nothing logged yet.Expected console output
starters visible: true | messages: 0 | status: ready
(server) question: "How do I return an item?"
starters visible: false | messages: 1 | status: submitted
starters visible: false | messages: 2 | status: streaming
starters visible: false | messages: 2 | status: ready
starters visible: true | messages: 0 | status: readyThe list has duplicates and five candidates on purpose: the output shows four unique ones. Blocked storage is simulated with an object that throws, as a browser does in some privacy modes. Without the try/catch the script stops with an uncaught SecurityError (see Break it).
Preview
Console
Nothing logged yet.Expected console output
[ 'Where is my order ORD-1042?', 'How do I return an item?', 'How long does delivery take?', 'What tea do you sell?' ]
[ 'How do I return an item?', 'How long does delivery take?', 'What tea do you sell?', 'Do you ship abroad?' ]
[ 'How do I return an item?', 'How long does delivery take?', 'What tea do you sell?', 'Do you ship abroad?' ]
first visit: true
second visit: false
storage blocked, first: true
storage blocked, second: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Rely on state instead of a ref for the double click
In the first example delete the if (used.current) return; used.current = true; part of the starter's click handler.
Break #2
Let a blocked storage crash the page
In the second example, remove the try { line and the } catch { block from firstRun (keep the code inside).
Myth vs fact
Myth
Users will figure out what to ask.
Fact
Many won't. A blank box makes people type nothing, something vague, or something the assistant cannot do. A sentence about scope and three examples remove most of that.
Myth
Disabling the button on status is enough to stop double sends.
Fact
Two events in the same instant both read the old status. Use a ref set synchronously in the handler, or disable the control and also guard in the handler.
Myth
localStorage always works.
Fact
It can be blocked, full, or missing, and then it throws. Treat it as optional and wrap it in try/catch.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Treat the starters as part of your test suite: each one should have a stored expected-quality check (Phase 13). They are the questions most users see first, so a regression there is the most visible one.
- ▸
Measure starter use: the click rate per starter tells you what people actually want, and a starter nobody clicks is dead weight. Do not log the free text of questions without a reason (Topic 9.7).
- ▸
Keep the empty state in one component with the starters as props. The same component then serves the first screen, a "New chat" and a cleared chat, and you avoid three slightly different welcome screens.
Remember this
- 1
The problem this solves: A blank text box asks the user to invent a good question with no clue what the assistant knows. Many type nothing, or type something too vague ("help"), or ask for something the assistant can never do and leave annoyed. This topic gives you the first-screen pieces that fix all three, and the small bugs that hide in them.
- 2
Define "empty" precisely. There are three different empty situations. *A new conversation*:
messages.length === 0. *A chat list with no chats* (Topic 4.5): there is nothing to open yet. *A result with no content*: the model answered with an empty string. Each needs its own message. This topic is about the first: the conversation exists and has no messages. - 3
Say what it can and cannot do. The first example's welcome line reads: "I can answer questions about orders, delivery and returns. I cannot change an order: our team does that." The second sentence matters as much as the first. People who know the limit do not waste a turn finding it, and fewer ask for things that would need an unsafe tool (Phase 8).
- 4
Starter prompts. A starter is a button with a short label and a full prompt behind it. Show three or four. Make them real questions your assistant answers well, not marketing lines: they are also a test, because every user who clicks one is running exactly that prompt. Keep the label short enough to fit on a phone, and let the prompt be more specific than the label.
- 5
Send or fill? Clicking a starter can send the prompt at once or only put it in the input box for the user to edit. Sending is faster; filling is better when the starter has a blank to complete ("Where is order ___?"). Pick one rule per starter and tell the user which by the wording: "Ask: Where is my order?" or "Fill in your order number".
- 6
A starter must send exactly once. The first example logs one
(server) questionline for two clicks in the same instant. Without a guard it logs two, and the user pays for and reads two answers. Why does a plain check ofstatusnot work? Because two clicks inside one browser task both seeready: React has not re-rendered yet. Set a ref (used.current = true) inside the click handler itself, before anything async happens. Topic 4.7 explains the same race for double submits. - 7
Starters can depend on who is asking. The second example builds the list from facts: a signed-in user with a recent order sees "Where is my order ORD-1042?". A signed-in user with no orders does not. A signed-out visitor never sees a private order id. Remove duplicates, and cap the list at four so the buttons never push the input box off a phone screen.
- 8
First-run hints show once. A one-time hint ("Tip: press Enter to send, Shift+Enter for a new line") is remembered in
localStorage. But storage can be missing or blocked (private mode, a full quota, a policy), and then even reading it throws. Wrap it intry/catchand treat failure as "not seen". It is better to show a hint twice than to crash the page. - 9
Where the empty state lives. When the first message is sent the welcome line and starters go away, and they come back if the user starts a new chat (
setMessages([])). The starters are hidden whenever a request is running, so nothing can be clicked while the answer streams. Keep focus in the input after you hide them so keyboard users do not lose their place (Topic 10.6).
Explain it without notes
What should an empty chat screen contain and why?
Why can two quick clicks on a starter send two questions even when the button is disabled by status?
Why wrap localStorage in try/catch for a first-run hint?
When should a starter fill the input instead of sending?
Practice
Make startersFor return the order starter first only if the order is less than 30 days old. Add a field lastOrderDays to the input and log three cases.
Change the first example so that each starter is disabled while status is not ready, in addition to the ref guard. Why keep both?
Trade-offs
- ↔
Starters help new users but push the input box down and can feel patronising for people who know what they want. Hide them after the first message, and consider showing fewer on small screens.
- ↔
Sending a starter at once is fast but costs a model call on every click, including accidental ones. Filling the box costs the user one more tap.
Done when you can
I can write a welcome line that says what the assistant can and cannot do.
I can build starters as real buttons that send exactly once.
I can build the starter list from facts and never show private data to a signed-out visitor.
I can show a first-run hint once without crashing when storage is blocked.
I can bring the empty state back on a new chat.