Stage
A tutor that draws while it talks.
Stage is a live whiteboard. You give it a document or a subject; it tells you the story first, draws the ideas as it explains them, and asks you to do something before moving on.
The idea
A board, not a chat
Most AI tutors reply with paragraphs. Stage draws: equations that build up, graphs, diagrams, molecules, maps, code. It points at what it is talking about, and the whole lesson is a sequence of small, replayable steps.
Story first
A lesson starts with why the subject exists and where you have met it, then a map of what is coming, and only then the first idea, introduced from an everyday scene rather than a definition.
Prove it before moving on
Every objective ends with a check you answer on the board. The tutor does not move on because you said "ok"; it moves on because you showed you got it. Spaced review brings the idea back later.
Your documents are the spine
Upload a book or notes and Stage plans from them: what is taught and what is checked comes from your pages, with the page numbers. The wider story around it comes from general knowledge and is never used to grade you.
What it looks like



How it is built
A Spring Boot server holds the lesson and the tutor; a browser board draws what it is told. They talk over one WebSocket, in one small language of "beats".
The model explains. Code decides.
The model chooses how to explain and draw. Plain code chooses which objective comes next, asks the plan's own check, records the grade and completes the lesson. This split was added because cheaper models skipped checks.
A session is a list of beats
Each beat is a sentence plus drawing operations, stored with a sequence number. A board that reconnects, or opens an old lesson, rebuilds itself from the list.
One schema, two languages
The beat format is defined once in TypeScript. The server validates every beat against the same schema before the learner sees it, so a bad drawing is retried, not shown.
What talks to what
flowchart LR
subgraph Browser["Learner's browser (apps/stage)"]
Board["Board: queue, player, scene"]
Panels["Panels: topics, plans, usage, history"]
end
subgraph App["robimoller.com (Spring Boot)"]
WS["StageSocketHandler
/stage/ws"]
API["StageApiController
/stage/api/*"]
Core["Services: Hub, Tutor, LessonDriver,
Planner, Plans, Materials, Learners"]
end
DB[("Postgres
15 tables")]
S3[("S3
PDFs, narration clips")]
LLM["Claude or DeepSeek
tutor and planner"]
Small["Jev, Haiku or Mercury
small judgements, section maps"]
Voice["ElevenLabs
speech and word timings"]
Emb["OpenAI
embeddings"]
Pics["Iconify, Wikimedia Commons
pictures and images"]
Board <-->|"stage:* events"| WS
Panels -->|"HTTP"| API
WS --> Core
API --> Core
Core --> DB
Core --> S3
Core --> LLM
Core --> Small
Core --> Voice
Core --> Emb
Core --> Pics
One tutor turn
sequenceDiagram
autonumber
participant B as Board
participant S as SocketHandler
participant T as StageTutor
participant D as LessonDriver
participant R as Retrieval and Plans
participant M as Claude or DeepSeek
participant V as StageProtocol
participant H as StageHub
B->>S: stage:ask (text, askId, where it cut in)
S-->>B: stage:turn received
S->>T: ask(owner, text, askId, at, kind)
T->>T: abort the previous turn, drop beats not yet heard
T-->>B: stage:turn thinking
T->>R: search the topic's passages, status note
T->>D: classify(topic, kind, text)
D-->>T: a Step (type and instruction) or nothing for a free turn
T->>M: stream (system prompt, history, this turn's context)
loop every tool call
M-->>T: tool_use beat
T->>V: normalise, then validate
alt invalid
T-->>M: tool_result error, fix and resend
else valid
T->>H: post(beat)
H-->>B: stage:live (numbered entry)
T-->>M: tool_result queued
end
end
T->>T: contract check, corrective round if the step fell short
T->>D: finishStep: ask the check, mark the step told
D->>H: post the check card
B-->>H: stage:ack (seq, board snapshot, layout)
The lesson driver: what comes next
flowchart TD S["Message or event
kind: lesson, resume, fresh, restart, or typed text"] --> R{"restart?"} R -->|"yes"| R1["clear the topic's position and what was told"] R1 --> Q R -->|"no"| Q{"A check is pending?"} Q -->|"yes, typed answer"| Q1["probe -> familiarity answer
warm-up -> grade the warm-up
check -> ANSWER step, model must call grade"] Q -->|"expired"| Q2["drop it, keep awaitingNext"] Q2 --> O Q -->|"no"| O{"Right after a lesson opening?"} O -->|"again"| O1["OPEN again, said differently"] O -->|"affirmative"| L O -->|"something else"| FREE["Free turn: the model answers, the lesson waits"] O -->|"not applicable"| P{"Partway up a ladder?"} P -->|"again"| P1["same ladder step again"] P -->|"affirmative"| P2["next ladder step"] P -->|"something else"| FREE P -->|"not applicable"| W{"A lesson event, or an affirmative
with something waiting?"} W -->|"no"| FREE W -->|"yes"| L{"orientedOn empty?"} L -->|"yes"| L1["ORIENT: story, map, familiarity card"] L -->|"no"| L2{"prologue never told
and the plan has one?"} L2 -->|"yes"| L3["PROLOGUE: the story, briefly"] L2 -->|"no"| L4{"reviews due?"} L4 -->|"yes"| L5["REVIEW: one quick check"] L4 -->|"no"| L6{"next objective's lesson
not yet opened?"} L6 -->|"yes"| L7["OPEN: scene, question, preview"] L6 -->|"no"| L8{"next objective exists?"} L8 -->|"yes"| L9["TEACH: ladder step 0 or the step to resume"] L8 -->|"no"| L10["COMPLETE: recap and offer the next lesson"]
Seen from the code, in object-oriented terms: a StageHub holds each learner's live session; a StageTutor runs one turn at a time against a model; a StageLessonDriver classifies what the learner just did and picks the next step; a StagePlanner turns documents into a plan; a StageProtocol validates every beat.
Want to try it?
Stage is by invitation while I tune it, because every lesson costs real tutor time. Ask and I will set you up with your own account: your topics, documents and progress stay yours.