Designing your own tablet

Our sample app has every feature at once, making it a bad interface. It exists to exercise the whole protocol on real hardware: ten controls, a hidden admin menu, a language popover, a rating card, an error banner. No real installation wants all of this, and those that mimicked it ended up worse than ones built from scratch.

Using the library directly is the how; this page is the what. Take the library and the patterns, then build only what your visitors need — for most kiosks, three or four controls. What we would build today:

[[1-one-huge-start-button]] == 1. One huge Start button

If visitors start the conversation from the tablet, make Start the only thing on the idle screen. Make it enormous, centre it, and animate it so passers-by see the kiosk is waiting. Ours is a card in a row of four; yours should be the screen.

It is one command and one state key:

sends

session + true (or false to stop)

reads

sessionActive — Unreal says whether a conversation exists

goes quiet while

sessionBusy (a start is already in flight) or blackout

Keep those last two. A Blueprint connect takes a few seconds. Without sessionBusy, a visitor taps four times and starts four conversations.

Tip: do not make the button the only way in. The best installations start the conversation when somebody approaches, keeping the button as a fallback. Presence detection — computer vision, a depth camera, or proximity sensors; an Intel RealSense depth camera has been the most reliable of these for us — hooks in either way. A sensor inside Unreal fires a Blueprint event straight into the Connect and Disconnect nodes, while an external one sends session on the control channel like any client. Both achieve the same result, so pick whichever side your hardware is on. A visitor who never has to press anything differentiates a kiosk people use from one they walk past.

You do not need our component for this. If you do use it, the press animation lives on the inner button precisely so you can animate the outer element yourself:

pk-button#btnSession {
    --pk-control-min-height: 45vh;      /* make it huge */
    --pk-font-size: 42px;
    animation: invite 2.2s ease-in-out infinite;
}
@keyframes invite {
    50% { transform: scale(1.04); }
}
/* Somebody will be looking at this screen all day. */
@media (prefers-reduced-motion: reduce) {
    pk-button#btnSession { animation: none; }
}

[[2-big-flags-on-the-first-screen]] == 2. Big flags, on the first screen

This is the mistake we most want you not to repeat. In our sample, language is a small round button in the top-left corner opening a popover. On the kiosks we run, changing language turned out to be one of the most used controls on the tablet — yet it remains the smallest target on the glass, unlabelled, in the corner, and easily missed.

Display your supported languages on the idle screen as large flag tiles next to Start. Visitors pick a language, then press play. This natural order means nobody has to discover a hidden corner.

sends

locale + a code, e.g. cs-CZ

reads

locale (the current one) and locales (the list on offer)

goes quiet while

sessionBusy

Which languages exist is a host setting, not a tablet oneAvailableLocales on the PromethistLink component. The tablet merely draws whatever arrives in locales. Adding Polish requires just one Unreal property plus a flag image. In the sample, ICONS in tablet.ts maps a code to its flag, while LANGUAGE_NAMES provides a name for screen readers. The setUpLanguagePicker() function in main.ts builds the tiles; read it before writing your own.

Two crucial things the sample gets right:

  • Changing language restarts the conversation, so the tiles must lock while a session change is in flight. This is what sessionBusy is for.

  • The tablet’s own labels follow the chosen language, so picking a flag re-translates the entire screen, not just the avatar.

[[3-call-assistance—​only-if-somebody-will-answer]] == 3. Call assistance — only if somebody will answer

This is worth having when on-site staff can receive notifications. Otherwise, omit it entirely: a button summoning an absent human is worse than no button.

The wait is the interesting part, and the protocol handles it for free:

On Tablet Command ("assistance")
   ->  Set Tablet State Bool ("assistancePending", true)
   ->  ... your HTTP call: a Teams incoming webhook, a pager, a bell in the back office ...
   ->  Set Tablet State Bool ("assistancePending", false)

While that key is true, the button lights up, spins where its icon was, says "Calling assistance…", and refuses a second press — on every tablet in the room. This works because it is published state, not a private reply. A tablet reloading mid-wait comes back still showing it.

Two obligations:

  • Publish false on BeginPlay too. If a level travel destroys your graph mid-wait, nothing is left to clear the key. The tablet will hold a spinner forever over an action nobody is doing.

  • Do not grey it out on agentUnready. That key means "there is no conversation running right now" — true for an idle kiosk, and true the moment anything goes wrong. Locking the button on this key disables it for most of the day, including when somebody actually needs a human. It is the right key for buttons acting on a conversation, like Pause or Restart, but the wrong key for this one.

[[4-keep-the-hidden-admin-menu]] == 4. Keep the hidden admin menu

This is the one part of the sample we would not change. A three-second press on the logo — lacking visible affordance, so visitors cannot stumble into it — opens an admin menu with controls for whoever manages the kiosk:

Volume

volume + 0–100. Unreal saves and restores it at start-up, returning the kiosk to its previous level.

Microphone mute

micMuted + bool, reads muted. Pausing mutes it automatically, keeping it muted until you resume. Unreal refuses an unmute while paused, preventing a live microphone behind a frozen avatar from capturing speech without visible replies. Grey the button on paused to make this refusal visible, avoiding a press that snaps back.

Manual blackout

Takes the kiosk out of service. blackout + bool, and it must read blackoutManualnot blackout. There are two separate switches, yours and the kiosk’s own. This toggle controls yours. Rendering the wrong key lights it up for faults nobody touched. The complete mechanism is in the next section.

Worth knowing: the same press works while the tablet is fogged, hitting the exact same spot on the glass. An absent host must not lock an operator out of their own controls. Manual blackout lives inside the admin menu, so an admin menu closing itself when fogged would trap the person trying to lift the lockout they just applied.

If building your own admin menu, copy the two gesture rules from gestures.ts rather than reinventing them (next section).

What we would leave out

Nothing in the sample is wrong; everything exists because some installation requested it. However, start from zero and add:

Sample control Add it when

Pause / Resume

staff need to hold a conversation mid-sentence — a tour group arriving, say

Subtitles

your project actually draws subtitles on the big screen

Restart

conversations get stuck often enough that somebody wants a fast reset

Joke ("say something")

you are demoing, or want canned phrases. It is say + any text, making it a good template for scripted lines

The five-star rating

you want feedback from visitors — see Asking the visitor something below