The web app1 of 12
First run in the browser
On this page
The first time you open Kotoba in a browser it does not open the companion. It opens /setup, a
nine-step wizard that asks the questions the backend needs answered before she can think, and writes
each answer the moment you give it.
Screenshot placeholder: the
/setupbrain step — the step strip across the top, the chibi and her speech bubble on the left, the two provider cards on the right, and the dashed "About my brain" note underneath.
How you get there
/app asks GET /api/setup/status before it mounts anything. If the answer is needed: true it
replaces the URL with /setup (app/app/page.tsx:52-59). This is the same predicate the terminal
wizard runs, so a machine set up from a terminal and a machine set up from a browser can never
disagree about whether somebody has been set up (lib/first-run.ts:2-9).
The check has a 2-second deadline and fails open (lib/first-run.ts:11, 96-118). A refusal, a
thrown fetch and a backend that never answers all read as "already configured". Being wrong that way
costs you one visit to an app that reports its own trouble; being wrong the other way would trap a
configured user in a setup screen they do not need.
What each step asks
The nine steps are declared in one list (components/Onboarding.tsx:140-150).
| Step | Label | What it writes |
|---|---|---|
| 1 | Her brain | POST /api/setup/provider |
| 2 | The model | POST /api/setup/model |
| 3 | The key | POST /api/setup/provider, then POST /api/settings/llm-key |
| 4 | Your name | POST /api/settings/user-name |
| 5 | Her name | POST /api/settings/soul (name) |
| 6 | Language | POST /api/settings/soul (language) |
| 7 | Her face | POST /api/models/install/default or /api/models/install/upload |
| 8 | Her voice | POST /api/settings/voice-key |
| 9 | Ready | nothing — it only navigates |
1. Her brain
Two cards: OpenAI and xAI (Grok). A dashed panel below them reads "more brains soon~"
(Onboarding.tsx:1235-1263). Choosing one posts the provider, and the backend answers with a model
it actually serves; the screen adopts that answer rather than the one you clicked
(Onboarding.tsx:663-682).
2. The model
The model list comes from the backend's own catalogue, read at the door from
GET /api/setup/status (Onboarding.tsx:554, 559-560). A hard-coded copy of the same table stands in
until that answer arrives (Onboarding.tsx:108-133) — the frontend renders before setup status can
answer — and a backend test parses that copy out of the component and fails if it has drifted from the
provider table (api/tests/test_setup_model_step.py:41-60).
Each row carries a price hint relative to the cheapest model and a context size.
The model is chosen before the key, and the order is load-bearing: the key check validates against
the current companion model, so a key with no access to it fails at the key step — where you can still
paste another — rather than on your first real sentence
(api/tests/test_setup_model_step.py:1-9, api/src/kotoba/server.py:1193-1197).
3. The key
A masked field. What the button means is decided before anything is sent (lib/key-step.ts:16-20):
- Something typed → check it, and file the verdict under the provider the attempt carried, not the one on screen when the answer comes back.
- Empty, over a key the backend already holds → keep it. Nothing is sent at all.
- Empty, with no key held → the button is disabled (
Onboarding.tsx:1320).
The label and the placeholder tell you which of those three you are in: one is saved; type to replace it, or she is using $VAR; type to save one here instead, or nothing
(Onboarding.tsx:406-420). An environment variable counts as a key she has.
If a key is refused
The backend's own words are classified into five causes (lib/key-errors.ts:8, 17-25) and she says the
one that fits (Onboarding.tsx:281-286):
| Cause | What she says |
|---|---|
quota | The key is real; the account behind it has nothing left. Top it up and the same key works. |
network | It never reached them. A proxy, a firewall or a VPN on this machine — not your key. |
install | She could not store it safely on this machine, so she did not keep it. |
model | The key opened the door; the model you picked was not behind it. Go back one step. |
paste | It was probably copied incomplete, or belongs to a different account. |
Only a paste failure clears the field (Onboarding.tsx:737-738). Emptying the box over a key with
no credit behind it, or over a proxy that ate the request, would throw away something that was right.
Nothing is stored on a refusal. A refused key check is a non-2xx from the backend, not a 200 carrying a falsy flag.
4 and 5. The names
Your name is capped at 40 characters, hers at 24 (Onboarding.tsx:1346, 1374). Both writes are read
back: the backend silently refuses a value that does not look like a name, so the screen prints what
it ended up with, never what was clicked (lib/setup-writes.ts:33-37). A refused write is undone on
screen and says so.
6. Language
Six chips: Auto — match you, English, Español, 日本語, Français, Português (Onboarding.tsx:315).
The chips are labels; a two-letter code is what gets stored (Onboarding.tsx:318-325). Language
decides both halves — how she answers and how she hears you.
7. Her face
No Live2D model ships with Kotoba. Two doors, and both land a model on your own disk:
- Live2D's sample. The offer, its name and its licence link come from
GET /api/models/default(Onboarding.tsx:555, 561-564). The licence link is drawn before the button is offered. Pressing the button postsaccept_license: true(Onboarding.tsx:846). - A .zip of yours. Drag one in or pick one. The archive is the request body — there is no
multipart and no filename, because the installed folder is named from the model's own entry file
(
Onboarding.tsx:57-59). Anything that is not a.zipis refused in the browser before a request is made (Onboarding.tsx:862-875), and more than one file at a time is refused too.
A finished install draws a receipt card: the folder name, the file count and byte size, and the path
on your disk — with her actually drawn inside it from the files that just landed
(components/FaceReceipt.tsx). Files in the archive that she cannot wear are reported as left behind
(Onboarding.tsx:2186-2193). You leave the step with Keep this face.
The step has no skip. A way out appears only after a door has actually refused her, so it is never an
invitation (Onboarding.tsx:817, 886-893, 1501).
8. Her voice
An ElevenLabs key, optional and masked. Empty over a key the backend already holds keeps it and sends
nothing. Skip does not clear anything: it is declining to give a new key, not a claim about the
old one (Onboarding.tsx:930-940).
9. Ready
A recap of eight rows — Brain, Model, Key, You, Her, Language, Face, Voice
(Onboarding.tsx:2484-2513). The two secrets show as dots or as "not yet"; neither is ever printed.
Everything was already saved. The button only opens the door: it shows a loading screen for about
four seconds and then replaces the URL with /app (Onboarding.tsx:1602, 2662-2672).
When something is already configured
/setup is not reachable by accident on a configured machine. SetupDoor asks the same question
/app asks, and a machine that does not need first run is sent to /app with replace, so Back does
not bounce off it (components/SetupDoor.tsx:30-38).
The one way in is Settings → Brain → Run setup again, which navigates to /setup?reconfigure=1
(components/panels/SettingsPanel.tsx:719-721). That flag is guessable, which is honest: it stops an
accident — a bookmark, browser history, an old link — rather than a person, and /setup sits behind
the same password gate as /app (lib/first-run.ts:16-21).
Reconfiguring differs from a first run in three ways:
- The recap and the steps are seeded from what the install already holds, so you do not see seven
dashes over answers you are keeping (
lib/setup-nav.ts:82-96,Onboarding.tsx:571-572). - The step strip lets you jump to any step, not only the ones behind you
(
lib/setup-nav.ts:9-12). On a first run you can only reopen a step you have already answered. - The note on the first screen says so plainly: only the answers you actually give are overwritten,
and a step you walk past keeps exactly what it had (
Onboarding.tsx:391-397).
There is a second, narrower door: Settings → Brain → Give her a face goes straight to the face
step (/setup?reconfigure=1&step=face). It exists because an install done from a terminal cannot
fetch a model — the terminal wizard has no browser to draw the licence in — and walking the other
eight questions to reach that one step is why nobody did (lib/first-run.ts:27-32).
/app offers the face step itself, once, if she has no model. It is offered once per browser and not
once per tab, and the flag is kept in localStorage under kotoba.sent-for-a-face
(lib/first-run.ts:34-63). Declining is final until you use the Settings button.
