π¬ Chat & UX design
NanoResearchβs UI is one chat, one sidebar. There are no buttons for βstartβ, βsave profileβ, or βsubmit feedbackβ β every action is a natural-language message in the chat.
Layout
ββββββββββββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββ
β NanoResearch demo Β· ready β Profile β
β β βββββββββ β
β π¬ Chat thread (assistant + user bubbles) β archetype β
β β domain β
β βΈ user msg β β
β β assistant narration β Pipeline β
β β another narration β ββββββ β
β β paper-ready link β β
β β What I've β
β ββββββββββββββββββββββββββββββββββββββββββββββ β learned β
β β tell me a topic, or refine your profileβ¦ β β 3 skills β
β β SENDβ β 5 memories β
β ββββββββββββββββββββββββββββββββββββββββββββββ β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββ
Conversational flow
flowchart TD
Start([User opens UI]) --> Hello[Assistant greets +<br/>asks for name + field]
Hello --> User1[User describes themselves]
User1 --> Intent[/POST /api/intent/]
Intent -->|create_user| Saved[Profile saved + selected]
Saved --> Ask[Assistant: ready for a topic]
Ask --> User2[User: 'start a run on X']
User2 --> Intent2[/POST /api/intent/]
Intent2 -->|start_run| Run[/POST /api/runs/]
Run --> SSE{SSE stream open}
SSE -->|narration| Bubble1[Assistant bubble appears]
SSE -->|narration| Bubble2[Assistant bubble appears]
SSE -->|awaiting_feedback| Pause[Pause for feedback]
Pause --> User3[User types feedback]
User3 --> Intent3[/POST /api/intent/]
Intent3 -->|submit_feedback| FB[/POST /feedback/]
FB --> SSE
SSE -->|paper_ready| Link[Assistant: π download link]
Link --> Done([User downloads paper.tex / paper.pdf])
Why no slash commands?
We support /help, /start, etc. as a fast path, but theyβre documented
only via /help and never required. Every slash command is also expressible
in natural English; the backendβs intent classifier (POST /api/intent)
hands either form off to the same action enum.
This means:
- New users discover features through the welcome message.
- Power users still get keyboard speed.
- Voice-style commands (βswitch to my colleague Saadβs profileβ) just work.
Narrations vs. raw events
Each pipeline event the backend pushes onto the SSE queue is fanned out as two separate events:
- The technical
trajectory_eventβ keys likelabel,detail,metadata. Available to UIs that want the granular view. - A narration event β a single human-readable
textfield that the chat renders directly as an assistant bubble.
The Narrator is a pure function in
api/narrator.py β no LLM, no extra latency, just an event-label β English string table. Easy to localise later (swap the table per locale).
State persistence
Browser-side, we persist via localStorage:
| Key | Purpose |
|---|---|
nano.userId |
Active profile id |
nano.runId |
Active run id (auto-cleared when run terminates) |
nano.chat.thread |
The visible chat history (last 200 turns) |
This lets a user refresh the page mid-run and pick up where they left off in both the chat thread and the sidebar status.
Markdown subset rendered in chat
The chat renders a tiny, fast, dependency-free markdown subset:
**bold**,_italic_,`code`[text](url)links β used for paper downloads-bullets- Blank lines as paragraph breaks
No tables, no headings, no code-fences. Keeps the chat dense and on-brand.
Sidebar contents
| Card | Refresh policy |
|---|---|
| Profile | Updates on selectUser / upsertUser |
| Pipeline | Polled every 8 s while run is non-terminal; stops on completed/failed |
| What Iβve learned about you | Polled every 15 s while run is live; static otherwise |
The pipeline card shows six dots (ideation β planning β coding β analysis β writing β review) β done = green, active = amber pulse, awaiting feedback = sky-blue pulse with a βneeds feedbackβ badge.