Skip to content
KONIGI

AI Assistants / Input and invocation / First-run state

4 of 7

First-run state

The most common screen in the product is an empty box, and it has to teach without running a tour.

Updated September 12, 2026

Problem

The empty conversation is the screen people see most often, and it’s the same screen for someone on their first visit and someone on their four-hundredth. It has to explain an unbounded product to a newcomer and then get out of the way forever, without a modal, a tour, or a coach mark.

Solution

Treat it as a permanent screen that happens to be a newcomer’s first, rather than as onboarding that happens to recur. That inversion decides everything else: it has to be quiet enough to look at daily and dense enough to teach once.

Four things belong on it, and the ordering is by how expensive they’re to learn the hard way:

  1. What this assistant is for. One line, specific to this product. An in-product assistant should say what it can see, since that is the thing a viewer cannot guess and the thing that makes it worth using instead of a general chatbot.
  2. A few concrete examples. Prompt starters do this work, and they carry most of the capability message.
  3. What it cannot do, or does badly. HAX guideline 2 asks for this and almost nobody ships it, because it reads as an apology. Stated plainly and briefly it prevents the failure that actually loses people, which is asking a reasonable question, getting a confident wrong answer, and concluding the product lies.
  4. What happens to what they type. Retention, training, and whether the conversation is visible to an employer are questions people have at the moment of first input, and answering them somewhere other than a settings page is the difference between cautious use and none.

The hard constraint is that a returning user sees this every time they start a conversation. Anything that reads as instruction on the first view reads as nagging on the tenth. It wants to be small and dense, with nothing modal, nothing animated, and nothing that has to be dismissed. A veteran’s eye should be able to skip the whole composition and land on the input without effort, which in practice means the input is the visual centre and everything else is quieter than it.

The empty state also has a second audience nobody designs for: the person whose conversation list is empty because they just deleted everything, or because they’re in a fresh workspace. They know the product and have no history, so the screen shouldn’t congratulate them on arriving.

Use when

Any assistant with a persistent surface people return to.

Don’t use when

The assistant is summoned to a specific object and dismissed, like an inline rewrite or a command-bar invocation. There the selection is the context, the invocation carries the intent, and a teaching screen in front of it’s an obstacle.

Trade-offs

Everything on this screen competes with the input, and the input is what people came for. Capability claims age badly and are rarely revisited, so a screen written at launch quietly misdescribes the product a year later. Stating limitations is honest and depresses first use, which is why it keeps getting cut. A privacy line at the point of input is the right place for it and makes the first screen feel legal rather than welcoming. Every team wants to put an announcement here, because it’s the highest-traffic surface in the product. That’s exactly why it should be defended.

Checklist

  • Does the screen say what this assistant can see, not just what it can do?
  • Does it state a limitation, and does that survive review by someone protecting adoption numbers?
  • Is the data question answered here rather than in settings?
  • What does this look like on the four-hundredth visit?
  • Does anything require dismissing, and does the dismissal persist?
  • Is the composer the visual centre?
  • Is it pleasant for a returning user with an empty history?
  • Who owns updating the capability claims when the product changes?
  • Does it work at a phone width with the keyboard up?
  • Is there a path from here to previous conversations?

Compare

ChatGPT keeps the screen close to empty with the composer centred and a small rotating set of examples, betting that the product is well enough known that explanation costs more than it returns. Claude changes the screen inside a project, where it describes the project’s material and makes the assistant’s scope the subject rather than the product’s features. Perplexity fills the empty state with live example queries and trending searches, It teaches question shape, and it doubles as something to browse when the viewer hasn’t got a question yet. Microsoft Copilot inside an Office application spends the screen on what it can reach in the current document and tenant, which is the right call for an assistant whose main unknown is its scope rather than its ability.

Prompt starters are the part of this screen doing most of the teaching. Composer is the element everything else defers to. Conversation history is the other thing this screen has to offer a path into. Refusal is what a viewer meets when the capability claims here were too generous. Memory chip is where the data question raised here becomes concrete.

First-run state anatomy An empty conversation composed around the input: one line saying what the assistant can see, a few examples, a stated limitation, and a line about what happens to what gets typed. Nothing requires dismissing. Seen once by a newcomer, four hundred times by everyone else Ask about anything in this workspace it can read pages you have access to, not the whole company summarise a page find a decision we made draft from these notes It gets dates and figures wrong. Check anything you act on. Conversations are kept in this workspace and visible to admins. 1 2 3 4 1 SAY WHAT IT CAN SEE Scope is the thing a viewer cannot guess and the reason to use this over a chatbot. 2 THE INPUT IS THE CENTRE Everything else has to be quieter, so a veteran can skip the whole composition. 3 STATE A LIMITATION It reads as an apology and it prevents the failure that actually loses people. 4 ANSWER THE DATA QUESTION People want it at the moment of first input, not on a settings page. Nothing here should need dismissing. A dismissal is a cost paid by everyone who returns.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

An empty conversation composed around the input: what the assistant can see, a few examples, a stated limitation, and what happens to what you type. Nothing here needs dismissing, because everyone who is not new sees it four hundred times.

shadcn
npx shadcn@latest add button textarea
npm
lucide-react
Tokens
--background--card--card-foreground--primary--primary-foreground--accent--accent-foreground--muted-foreground--border--input--status-warn

Ask about anything in this workspace

it can read pages you have access to, not the whole company

It gets dates and figures wrong. Check anything you act on.

Conversations are kept in this workspace and visible to admins.

FirstRunState.tsxThe four lines are props, so the screen cannot ship without them. An example fills the input rather than sending it.

import { useRef, useState } from "react";
import { ArrowUp } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";

type Props = {
  /** One line on what the assistant can see. Scope is the thing a viewer
   *  cannot guess, and the reason to use this over a chatbot. */
  scope: string;
  /** Two or three. More than that is a menu, and a menu is a thing to read. */
  examples: string[];
  /** Stated up front. It reads as an apology and prevents the failure that
   *  actually loses people. */
  limitation: string;
  /** What happens to what gets typed. Asked at the moment of first input,
   *  not on a settings page. */
  dataNotice: string;
  onSend: (text: string) => void;
};

export function FirstRunState({ scope, examples, limitation, dataNotice, onSend }: Props) {
  const [draft, setDraft] = useState("");
  const ref = useRef<HTMLTextAreaElement>(null);

  const send = () => {
    const text = draft.trim();
    if (!text) return;
    onSend(text);
    setDraft("");
  };

  // An example fills the input rather than sending. A newcomer is here to
  // learn what a question looks like, and a question they can edit teaches
  // that better than one that fires on click.
  const useExample = (text: string) => {
    setDraft(text);
    ref.current?.focus();
  };

  return (
    <div className="rounded-lg border bg-card p-5 text-center">
      <p className="text-base text-card-foreground">Ask about anything in this workspace</p>
      <p className="mt-1.5 text-xs text-muted-foreground">{scope}</p>

      <div className="mt-4 flex flex-wrap justify-center gap-2">
        {examples.map((e) => (
          <Button key={e} variant="outline" size="sm" className="rounded-full" onClick={() => useExample(e)}>
            {e}
          </Button>
        ))}
      </div>

      {/* The input is the centre. Everything else on this screen has to be
          quieter than it, so a veteran can skip the whole composition. */}
      <div className="mx-auto mt-5 flex w-[464px] max-w-full items-end gap-2 rounded-lg border bg-background p-2">
        <Textarea
          ref={ref}
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(); }
          }}
          rows={1}
          placeholder="Ask a question"
          aria-label="Ask a question"
          className="min-h-0 resize-none border-0 px-2 py-1.5 text-left shadow-none focus-visible:ring-0"
        />
        <Button size="sm" onClick={send} disabled={!draft.trim()}>
          <ArrowUp className="size-4" /> send
        </Button>
      </div>

      <p className="mt-5 text-xs text-status-warn">{limitation}</p>
      <p className="mt-2.5 text-xs text-muted-foreground">{dataNotice}</p>
    </div>
  );
}

demo.tsxHow it is called: the four lines as props. Send shows what went.

import { useState } from "react";
import { FirstRunState } from "./FirstRunState";

/** The four lines a first run has to state, as props. Send echoes what was sent. */
export default function Demo() {
  const [sent, setSent] = useState<string | null>(null);
  return (
    <div>
      <FirstRunState
        scope="it can read pages you have access to, not the whole company"
        examples={["summarise a page", "find a decision we made", "draft from these notes"]}
        limitation="It gets dates and figures wrong. Check anything you act on."
        dataNotice="Conversations are kept in this workspace and visible to admins."
        onSend={setSent}
      />
      {sent && <p className="mt-3 text-xs text-muted-foreground">sent: “{sent}”</p>}
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No captures reference this pattern yet. Captures arrive product by product; see Products for what's in the gallery so far.