The page and its contract #
The repository ships the app's TypeScript and Preact components (web/) and the page that embeds it,
site/index.html, with its assets (site/assets/: the Nunito font, under the
SIL Open Font License, included as OFL-nunito.txt; the web app manifest; and
the app icons, drawn by node site/make-icons.mjs) and its service worker,
site/sw.js. site/build.sh puts them together (see deployment.md). The page owns layout
and look; the app owns behavior. Any other page can embed the pet if it
provides the mount point, config and styles below.
The shipped page #
site/index.html is a complete, standalone page: a pastel "toy" device with
the LCD, meters and care buttons, a panel with chat, diary, shelf and settings
tabs, and three short notes that explain the pet and list build details. It
hosts the app, model, runtime and font together, with an optional external
Cloudflare Web Analytics beacon configured at build time. It uses a rounded
font, follows prefers-color-scheme for light and dark, and is laid
out for phones first (one column below 860 px, tested at 360 px). The notes
are readable without JavaScript.
The brain download/upgrade notice spans the app above both the device and
tabs, directly below the page header on mobile. It remains visible when
switching tabs, with an accent border and a full-width button on small phones.
The viewport requests no touch zoom; touch-action: pan-x pan-y on the root
allows scrolling while suppressing pinch and double-tap zoom. Browser or
accessibility settings may override the viewport request. Text stays selectable
outside the game pad; its buttons suppress selection and touch callouts.
The hatch form suggests a pet name once when first shown during a visit. Deleting it leaves the field empty, including through download progress and other re-renders; a nonempty name is still required to hatch. When loading fails, a retry button appears beside the egg's error. It starts the brain again without resetting the egg, suggested/edited name or saved pet.
It is a template: @PLACEHOLDERS@ in it are replaced when it is built.
site/build.sh fills them from site/release.json; test/dev-site.sh uses
the same build for local previews. The page must be served at a URL ending in
/ with sw.js beside it, because it registers the worker by that relative
URL and the worker's scope is the page's directory.
| Placeholder | Value |
|---|---|
@RUNTIME@ |
URL of the runtime directory, no trailing slash |
@MODELS@ |
URL prefix holding model directories, with a trailing slash |
@MODEL_ID@ |
model directory name |
@SIZE@, @SIZE_MB@ |
size of onnx/model_q4.onnx in bytes, and in MB for the text |
@APP@ |
URL of the directory holding the bundled main.js, brain.js and tokenling_core_bg.wasm |
@ASSETS@ |
URL of the directory holding site/assets/* |
@TRAINED@ |
one line about the model build for the details list |
@SOURCE@ |
URL of the source code |
@ANALYTICS@ |
Optional Cloudflare Web Analytics module snippet; empty without a configured site token |
@ANALYTICS_NOTICE@ |
Visible traffic/performance analytics disclosure; emitted with the snippet |
@NEWS@ |
JSON string: what the current brain does better, for the upgrade dialog |
@FEATURES@ |
JSON list of feature names the current brain was trained for |
@REMOTE_ENDPOINT@ |
JSON string for an optional default remote-host address; empty by default |
@OLDER@ |
JSON list of older brains the app may keep running, {"id", "features"}, newest first |
test/dev-site.sh fills them for local previews and fails if any are left.
node test/contract.mjs fails if the app starts using an element id or config
key that the page or this document does not cover.
Local and remote choice #
Setup.tsx asks local or remote before creating an egg. Hosting.tsx keeps
both options in settings, with a trusted server address, transfer status,
reconnect and explicit local-checkpoint recovery. Remote mode explains that
the selected host receives saves, chats and memories. Local mode retains
its original offline/download behavior. The new hosting-choices and
hosting-option classes style accessible radio groups and their explanations;
URL inputs fit the phone width. Interactive markup stays in Preact.
The brain notice/status distinguishes browser download from server connection. Remote care/chat call the private host, and debug body edits are local-only. Save exports omit access keys. Static explanatory text describes both modes without requiring JavaScript.
Loading #
<div id="pet-root"></div>
<script type="application/json" id="pet-config">
{"runtime": "/pet/runtime/transformers-4.3.1",
"models": "/pet/models/",
"modelId": "tokenling-v6",
"size": 97276118,
"news": "Dreams, gentler sleep, and a teen voice with fewer catchphrases.",
"features": [],
"older": [],
"sw": "sw.js",
"icon": "/pet/assets/icon-192.png"}
</script>
<script type="module" src="/pet/app/<version>/main.js"></script>
| Config key | Meaning |
|---|---|
runtime |
URL of the vendored runtime directory (transformers.min.js plus the ONNX Runtime .wasm/.mjs files), no trailing slash |
models |
URL prefix that holds model directories, with a trailing slash |
modelId |
directory under models to load; also the cache key, so change it for every new model |
size |
size in bytes of onnx/model_q4.onnx, used to label downloads and the upgrade |
news |
optional one line about what modelId does better than older brains, shown in the upgrade dialog |
features |
optional list of feature names modelId was trained for (see "Brain features" below) |
older |
optional list of older brains, newest first, as {"id": …, "features": […]}. A visitor who still has one of them cached keeps using it until they accept the upgrade. List only brains that speak the app's current prompt format; leave the list empty after a format change |
sw |
optional URL of the service worker, relative to the page (sw.js); without it there is no offline copy and no alerts |
icon |
optional URL of the icon shown on alerts |
weatherSeed |
optional string mixed into the deterministic weather |
remoteEndpoint |
optional default remote-host address for setup/settings; local remains the default, and owners can supply their own trusted host |
main.js, brain.js and tokenling_core_bg.wasm must be served together
under the hashed app directory (.wasm as application/wasm). The main
bundle includes Preact, all components/controllers and the renderer; both
bundles load the shared rules module from beside themselves before running,
and the page starts brain.js as a module worker relative to itself. If the
module cannot load, #pet-root shows a short error instead of the app.
site/build.sh hashes the emitted bundles, maps and module, including
dependencies, so a build never mixes versions. Keep app, model and runtime on the same origin.
The app adds the class pet-js to <html> once it has booted. Use it to keep
the interactive area hidden when JavaScript is off while leaving the
explanatory content visible.
Host elements and component ids #
The host provides only pet-root (an empty mount element) and pet-config
(the JSON block). Components in web/components/ create and update all the ids
below; embedding pages must not duplicate their markup. The ids remain useful
for browser tests, labels and accessibility. CSS must keep [hidden] { display: none } winning over grid/flex rules.
| Area | Ids |
|---|---|
| Screen | pet-lcd (a <canvas width="96" height="64" tabindex="0">), pet-status, pet-meters, pet-actions, food-menu, game-pad, pad-left, pad-right |
| Setup | pet-setup, setup-owner (form; submitting starts a local brain download or remote connection), owner-name, setup-size (the download size note), setup-hatch (form), setup-hatch-note (download progress), setup-retry (button, visible after a brain load error), pet-name, hatch-btn |
| Hosting choice | setup-local, setup-remote, setup-server (initial mode/address); hosting-title, hosting-local, hosting-remote, hosting-server, hosting-status, hosting-switch, hosting-reconnect, hosting-recover (settings and outage recovery) |
| Bad egg | bad-egg (inside pet-setup, shown instead of the forms when the save cannot be read), bad-egg-why, bad-egg-save, bad-egg-retry, bad-egg-new |
| Another tab | pet-elsewhere (outside the app area; shown when another tab has the pet), elsewhere-here |
| Chat | chat-log (a list), chat-form, chat-input, chat-send, pet-tip, pet-tip-text, tip-yes, tip-no (a one-time suggestion: install, then alerts) |
| Brain notice | chat-cta (above the device and tabs, class brain-notice), chat-cta-text (a live status), chat-brain (download or upgrade button) |
| Upgrade | brain-upgrade (a <dialog>), upgrade-title, upgrade-text, upgrade-now, upgrade-later |
| App and alerts | install-row, install-btn, install-note, alerts-toggle (a checkbox), alerts-note |
| Diary | diary-pages, diary-today (the "today so far" line) |
| Shelf | shelf-items (a list), memory-list (a list) |
| Brain and save | brain-status, brain-bar (a <progress>), brain-load, brain-forget, brain-storage, chatter-toggle (a checkbox: "lively" idle turns), owner-name-edit, save-export, save-import (a file input), save-reset |
| Notifications | pet-toasts |
| Config | pet-config (the JSON block above) |
Care buttons are act-feed, act-play, act-bath, act-pat, act-clean,
act-meds, act-lights; the streamed diary uses diary-draft. With
?debug=1 (or #debug) in the URL it also adds a debug tab after the last
tab (tab-debug, panel-debug, debug-age); see docs/testing.md.
Tabs #
Tab buttons are role="tab" with ids tab-chat, tab-diary, tab-shelf,
tab-care, each with aria-controls naming panel-chat, panel-diary, panel-shelf or
panel-care (role="tabpanel"); all
panels except the first start hidden. The app handles clicks, arrow keys,
aria-selected, tabindex and showing and hiding the panels.
Classes the app emits #
The page styles these; the app only applies them. All art is drawn by the app
(the canvas and <img class="px-icon"> data URLs), so no image files are
needed.
| Class | Used for |
|---|---|
pet-btn, text-button, px-icon |
action, food and game-pad buttons, small buttons, pixel icons (style with image-rendering: pixelated) |
pet-meter, pet-meter-label, pet-meter-pips, pet-meter-word |
stat rows, always tummy, fun, energy, clean (STATS in web/app/config.ts), whatever key order a save has; pips are <i> elements and a full one has class on |
chat-pet, chat-owner, chat-event, chat-act, chat-who, chat-emote, chat-tool, chat-tool-detail, live, typing |
list items and parts of chat lines; chat-act is a silent pet line (the pet acting on its own), live is the line currently streaming, and typing marks a live line that has no words yet (its <p> holds three empty <span> dots to animate) |
diary-page, drafting, diary-doodles |
diary pages, the page being written, the sticker row |
pet-toast |
transient notifications inside pet-toasts |
muted |
a utility class on generated placeholder text |
warn |
a paragraph in the upgrade dialog that warns about mobile data |
needs-brain |
a control that is off because the running brain lacks the feature named in its data-needs |
The canvas should keep a 3:2 aspect ratio and use image-rendering: pixelated.
chat-log should scroll (the app keeps it scrolled to the newest line) and be
exposed to assistive technology as a live region. The page is responsible for
keyboard focus styles on the canvas, buttons and inputs.
During catch, the app sets the canvas's touch-action to none for dragging
and restores the page's style when the game ends. The pad uses touch-action: none and disables text selection so holding a direction stays a game input.
Brain features #
A brain can be trained for something older brains cannot do. List the
feature's name in the config (features, and in each older entry that has
it). In components, use brainHas(name) to set disabled/class/title props,
with data-needs="name" for inspection. While an older brain without that
feature runs, the component disables those controls, gives them
the class needs-brain and a "needs the new brain" title, and the upgrade
dialog lists what the new brain adds (FEATURE_TEXT in web/app/config.ts). Care
actions are gated by ACT_NEEDS in web/app/config.ts. No feature is gated yet.
Installing, offline use and alerts #
The page links assets/manifest.webmanifest (name, colors, icons; its
start_url and scope are ../, the page's directory) and an Apple touch
icon, so browsers can install it as an app. The app registers sw.js, which
keeps the page, the app modules and the runtime files the brain loaded for
offline use, and shows alerts. See pet-simulation.md for the behavior.
Isolation and headers #
The brain runs best with cross-origin isolation, which lets the WebAssembly
backend use threads. Serve the page and everything under the app's base path
with Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp. Cross-origin resources need
appropriate CORS or CORP permission. Keep the app, model and runtime
same-origin. The optional analytics beacon uses a module script and CORS;
it must not require weakening isolation. The remaining server requirements
are in deployment.md.
Explaining it to visitors #
The shipped page explains that decisions come from the brain. Local mode
stores the pet and brain in the visitor's browser; its first download is about
97 MB. Explicitly selected remote mode sends private saves, conversations and
diaries to the chosen host. Keep that distinction on
any page that embeds the app, readable without JavaScript. When page analytics
is configured, disclose its traffic/performance reporting separately; do not
claim that no information is sent. The beacon receives no pet names,
conversations, diaries or saves from the app. See deployment.md for the
optional analytics setting and verification.