Asking the visitor something
The tablet can put a question over a dimmed screen — the shipped one is a five-star rating after a
conversation ends. apps/tablet/src/prompts.ts contains the entire implementation and is the file
to edit or delete. A prompt is a name plus three answers: when it opens, how long it stays, and
what it draws, and all three live there. Nothing about it reaches the plugin, the wire format,
or the component library. Thus, a partner replacing the rating with a form, a poster, or a 3D viewer
changes only one file in their own app.
A prompt defines when it opens as a predicate over two consecutive snapshots. Two openers ship, and they compose — which is the interesting part:
/* The tablet noticed something: a conversation ended and the kiosk went idle. */
opensOn: afterConversation
/* The host asked: a Blueprint calling Set Tablet State String("prompt", "poster"). */
opensOn: whenHostAsks('poster')
/* Both - what the shipped rating uses. */
opensOn: (previous, next) =>
afterConversation(previous, next) && next.state.prompt === 'feedback',
The second is the general host→tablet route and requires nothing new: prompt is an ordinary state
key, like subtitles.
Prefer the third shape where it fits. On its own, whenHostAsks watches the key’s own edge,
bringing two obligations. First, the host’s state store compares before storing, so re-publishing
the same string emits no patch; the prompt opens exactly once until the key is cleared. Second, a
key left published is a question the next tablet to connect will find and ask, because hello
carries a complete snapshot. ANDing it with something the tablet can see for itself removes both.
The rating is armed by sessionActive going false, so prompt may sit at "feedback" for the life
of the process, and every visitor is still asked.
Thus, the entire Unreal side of the shipped rating is one node that is never undone. It belongs on
BeginPlay alongside the existing subtitles and assistancePending publishes:
BeginPlay -> ... -> Set Tablet State String ("prompt", "feedback")
Publish a different name, and a different card appears; publish nothing, and none does. This is the entire host-side vocabulary for this, and it uses a node you already had.
Placing it after Disconnect works identically — the sessionActive edge always reaches the tablet
in a later patch than a value the graph wrote synchronously — but it buys one thing BeginPlay
cannot: publishing per disconnect path. Thus, a failed conversation can be excluded from the
question, while a clean Stop still asks. Note that Disconnect in the reference graph is shared by
ten callers, so the shared tail asks after all of them.
The answer travels back via the ordinary command channel: answer('4') sends feedback with the
value "4", landing on On Tablet Command like any button press. The graph can then use or ignore
it. A prompt requiring a reply does not justify a new message type.
Where to send feedback off the box is marked in prompts.ts as Deployment hook — a fetch in
the renderer that collected the score. Alternatively, posting from the graph is usually the better
approach: it keeps the endpoint and any API key off a tablet sitting on a guest network, and the
score is already there.
The card closes on any of five events, and only the first is an answer: the visitor responds, they
tap anywhere off the card, the renderer’s timeoutMs runs out (20 s for the rating), a conversation
starts, or the host becomes unreachable. None of them needs wiring.