Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292<!doctype html><!-- The tokenling page: a complete implementation of the contract in docs/host-page.md. Restyle freely, but keep the ids, the tab wiring and the config block.
Placeholders are the names below wrapped in at-signs; test/dev-site.sh (or your own build) fills them, and docs/host-page.md describes each one: RUNTIME, MODELS, MODEL_ID, SIZE, SIZE_MB, APP, ASSETS, TRAINED, SOURCE, FEATURES, NEWS, OLDER (JSON values), ANALYTICS, ANALYTICS_NOTICE The page is served at a URL ending in "/" with sw.js beside it (site/build.sh lays this out).--><html lang="en"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"> <title>tokenling</title> <meta name="description" content="A tiny virtual pet with a fine-tuned GPT-2 brain. Choose your device or a trusted server as its home."> <meta name="color-scheme" content="light dark"> <meta name="theme-color" content="#ffc9d8" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#221b29" media="(prefers-color-scheme: dark)"> <link rel="manifest" href="@ASSETS@/manifest.webmanifest"> <link rel="apple-touch-icon" href="@ASSETS@/icon-180.png"> <meta name="apple-mobile-web-app-title" content="tokenling"> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' shape-rendering='crispEdges'%3E%3Cpath fill='%234a3b4f' d='M5 2h6v1h2v2h1v6h-1v2h-2v1H5v-1H3v-2H2V5h1V3h2z'/%3E%3Cpath fill='%23ffd6e2' d='M5 3h6v1h1v2h1v5h-1v1h-1v1H5v-1H4v-1H3V6h1V4h1z'/%3E%3Cpath fill='%234a3b4f' d='M5 7h2v2H5zm4 0h2v2H9zM7 10h2v1H7z'/%3E%3C/svg%3E"> <style> @font-face { font-family: "Nunito"; font-style: normal; font-weight: 400 900; font-display: swap; src: url("@ASSETS@/nunito-latin.woff2") format("woff2"); }
:root { --bg: #fff5ee; --dots: #ffe1e8; --ink: #4a3b4f; --muted: #8b7a8e; --card: #ffffff; --line: #f2dde4; --shell: #ffc9d8; --shell-edge: #f3a4bb; --shell-hi: #ffe4ec; --bezel: #5b4a60; --accent: #f2668f; --accent-ink: #ffffff; --soft: #fff0f4; --mint: #6fbfac; --sun: #f6c552; --bubble-owner: #ffe0ea; --paper: #fffaf0; --paper-line: #f1e2c9; --paper-ink: #5a4636; --shadow: 0 2px 0 rgba(74, 59, 79, .08), 0 8px 24px rgba(242, 102, 143, .10); --font: "Nunito", ui-rounded, "SF Pro Rounded", "Hiragino Maru Gothic ProN", system-ui, sans-serif; } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #221b29; --dots: #2d2436; --ink: #f6eaf1; --muted: #bba9c0; --card: #2e2538; --line: #43374f; --shell: #b9638a; --shell-edge: #8f4669; --shell-hi: #d17ea2; --bezel: #1b1520; --accent: #ff86aa; --accent-ink: #2a1520; --soft: #3a2d45; --mint: #7fd0bd; --sun: #f6c552; --bubble-owner: #4b3150; --paper: #f7efdf; --paper-line: #e6d6b8; --paper-ink: #4f3d2f; --shadow: 0 2px 0 rgba(0, 0, 0, .25), 0 8px 24px rgba(0, 0, 0, .25); } } :root[data-theme="dark"] { --bg: #221b29; --dots: #2d2436; --ink: #f6eaf1; --muted: #bba9c0; --card: #2e2538; --line: #43374f; --shell: #b9638a; --shell-edge: #8f4669; --shell-hi: #d17ea2; --bezel: #1b1520; --accent: #ff86aa; --accent-ink: #2a1520; --soft: #3a2d45; --mint: #7fd0bd; --sun: #f6c552; --bubble-owner: #4b3150; --paper: #f7efdf; --paper-line: #e6d6b8; --paper-ink: #4f3d2f; --shadow: 0 2px 0 rgba(0, 0, 0, .25), 0 8px 24px rgba(0, 0, 0, .25); }
* { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; touch-action: pan-x pan-y; } body { margin: 0; color: var(--ink); font: 500 16px/1.55 var(--font); background: var(--bg) radial-gradient(var(--dots) 1.6px, transparent 1.7px) 0 0 / 22px 22px; } main { max-width: 1000px; margin: 0 auto; padding: 20px 16px 48px; } a { color: var(--accent); font-weight: 700; } h1, h2, h3 { font-weight: 900; letter-spacing: -.01em; } h2 { font-size: 17px; margin: 18px 0 8px; } .muted { color: var(--muted); } .small { font-size: 13px; } [hidden] { display: none !important; } .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } :focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; border-radius: 10px; }
/* header */ .hello { display: flex; align-items: center; gap: 12px; margin: 4px 0 18px; } .hello svg { width: 44px; height: 44px; flex: none; image-rendering: pixelated; } .hello h1 { margin: 0; font-size: 30px; line-height: 1.1; } .hello p { margin: 2px 0 0; color: var(--muted); font-size: 15px; }
/* The app stays hidden until main.js has booted (it adds .pet-js to <html>). */ .pet-app { display: none; grid-template-columns: minmax(0, 400px) minmax(0, 1fr); gap: 20px; align-items: start; } .pet-js .pet-app { display: grid; }
/* the toy */ .pet-device { position: relative; background: linear-gradient(160deg, var(--shell-hi), var(--shell) 40%); border: 3px solid var(--shell-edge); border-radius: 36px 36px 30px 30px; padding: 22px 16px 16px; box-shadow: var(--shadow), inset 0 -6px 0 rgba(0, 0, 0, .06); min-width: 0; } .pet-device::before { content: ""; position: absolute; top: -14px; left: 50%; width: 34px; height: 20px; margin-left: -17px; border: 4px solid var(--shell-edge); border-bottom: 0; border-radius: 18px 18px 0 0; } .pet-screen { background: var(--bezel); border-radius: 20px; padding: 10px; box-shadow: inset 0 3px 0 rgba(0, 0, 0, .25); } #pet-lcd { display: block; width: 100%; aspect-ratio: 3 / 2; image-rendering: pixelated; border-radius: 10px; cursor: pointer; touch-action: manipulation; } .pet-status { margin: 10px 4px 8px; min-height: 1.3em; font-size: 14px; font-weight: 800; color: var(--ink); text-align: center; } .pet-status:empty { min-height: 0; margin: 0 0 14px; } .pet-pad { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 10px; color: #f6eaf1; font-size: 13px; text-align: center; } .pet-pad { touch-action: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; } .pet-pad .pet-btn { flex: 0 0 64px; font-size: 18px; padding: 10px 0; touch-action: none; }
.pet-meters { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; margin: 0 0 12px; padding: 10px 12px; background: var(--card); border-radius: 18px; } .pet-meter { display: grid; grid-template-columns: 50px auto 1fr; align-items: center; gap: 8px; font-size: 13px; } .pet-meter-label { font-weight: 800; } .pet-meter-word { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pet-meter-pips { display: flex; gap: 3px; } .pet-meter-pips i { width: 9px; height: 12px; border-radius: 5px; background: var(--line); } .pet-meter:nth-child(1) .on { background: #f39a5b; } .pet-meter:nth-child(2) .on { background: var(--accent); } .pet-meter:nth-child(3) .on { background: var(--sun); } .pet-meter:nth-child(4) .on { background: #74b3e0; }
.pet-actions { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } .pet-food { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 10px; padding: 10px; background: var(--soft); border-radius: 18px; } .pet-btn { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; min-height: 58px; padding: 7px 2px 6px; font: 800 12px/1.2 var(--font); color: var(--ink); background: var(--card); border: 0; border-radius: 16px; cursor: pointer; box-shadow: 0 3px 0 var(--shell-edge); transition: transform .08s ease, box-shadow .08s ease; } .pet-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 0 var(--shell-edge); } .pet-btn:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 1px 0 var(--shell-edge); } .pet-btn:disabled { opacity: .5; cursor: default; } .px-icon { image-rendering: pixelated; flex: none; }
.pet-setup { background: var(--card); border-radius: 22px; padding: 14px 16px 16px; } .pet-setup p { margin: 0 0 8px; } .pet-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } input[type=text], input[type=url], input:not([type]) { flex: 1 1 150px; min-width: 0; font: 600 16px var(--font); color: var(--ink); background: var(--soft); border: 2px solid var(--line); border-radius: 14px; padding: 9px 13px; } input:not([type]):focus { border-color: var(--accent); outline: none; } label { display: block; font-size: 14px; font-weight: 800; margin: 10px 0 6px; } .pet-check { display: flex; gap: 10px; align-items: flex-start; font-weight: 600; } .pet-check input { width: 18px; height: 18px; margin-top: 2px; accent-color: var(--accent); flex: none; } .hosting-choices { border: 0; padding: 0; margin: 14px 0; min-width: 0; } .hosting-choices legend { font-weight: 800; margin-bottom: 8px; } .hosting-option { display: flex; gap: 10px; align-items: flex-start; padding: 10px; margin: 8px 0; border: 1px solid var(--line); border-radius: 12px; background: var(--soft); cursor: pointer; } .hosting-option input[type=radio] { width: 18px; height: 18px; margin: 3px 0 0; flex: none; accent-color: var(--accent); } .hosting-option span { min-width: 0; } .hosting-option small { display: block; font-weight: 500; line-height: 1.45; margin-top: 3px; } .hosting-choices input[type=url] { display: block; width: 100%; box-sizing: border-box; } .primary { font: 900 15px var(--font); color: var(--accent-ink); background: var(--accent); border: 0; border-radius: 999px; padding: 10px 18px; cursor: pointer; box-shadow: 0 3px 0 color-mix(in srgb, var(--accent) 60%, #000); } .primary:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 1px 0 color-mix(in srgb, var(--accent) 60%, #000); } .primary:disabled { opacity: .5; cursor: default; } .text-button { font: 800 13px var(--font); color: var(--ink); background: var(--soft); border: 0; border-radius: 999px; padding: 8px 14px; cursor: pointer; } .text-button:hover { color: var(--accent); } .pet-file { display: inline-block; margin: 0; } .pet-file input { position: absolute; width: 1px; height: 1px; opacity: 0; } .pet-file:focus-within { outline: 3px solid var(--accent); outline-offset: 3px; }
/* the panel */ .pet-panel { background: var(--card); border-radius: 28px; padding: 14px; box-shadow: var(--shadow); min-width: 0; } .pet-tabs { display: flex; gap: 4px; padding: 4px; margin-bottom: 12px; background: var(--soft); border-radius: 999px; overflow-x: auto; scrollbar-width: none; } .pet-tabs [role=tab] { flex: 1 0 auto; font: 800 14px var(--font); color: var(--muted); background: none; border: 0; border-radius: 999px; padding: 8px 12px; cursor: pointer; white-space: nowrap; } .pet-tabs [role=tab][aria-selected=true] { color: var(--accent-ink); background: var(--accent); } [role=tabpanel] { padding: 0 4px; } [role=tabpanel] > h2:first-child { margin-top: 4px; }
.chat-log { list-style: none; margin: 0 0 12px; padding: 4px 2px; height: min(54vh, 480px); overflow-y: auto; display: flex; flex-direction: column; gap: 8px; overscroll-behavior: contain; } .chat-log li p { margin: 0; } .chat-log:empty::before { content: "Hatch your pet and its chat will show up here."; margin: auto; padding: 0 24px; text-align: center; color: var(--muted); font-weight: 700; } .chat-pet, .chat-owner { max-width: 86%; padding: 8px 13px; border-radius: 18px; font-size: 15px; line-height: 1.45; overflow-wrap: anywhere; } .chat-pet { align-self: flex-start; background: var(--soft); border-bottom-left-radius: 6px; } .chat-owner { align-self: flex-end; background: var(--bubble-owner); border-bottom-right-radius: 6px; } .chat-who { display: block; font-size: 12px; font-weight: 900; color: var(--accent); } .chat-emote { color: var(--muted); } .chat-tool { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 5px; font-size: 12px; font-weight: 700; color: var(--muted); } .chat-tool::before { content: "✦"; color: var(--accent); } .chat-tool-detail { color: var(--ink); font-weight: 600; } .chat-event, .chat-act { align-self: center; max-width: 92%; font-size: 12px; font-weight: 700; color: var(--muted); text-align: center; background: var(--soft); border-radius: 999px; padding: 3px 12px; } .chat-act { background: none; font-style: italic; padding: 0 8px; } .chat-act .chat-tool { justify-content: center; margin-top: 2px; font-style: normal; } .live p { min-height: 1.4em; } .typing p { display: flex; gap: 4px; align-items: center; height: 1.45em; } .typing p span { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); opacity: .5; animation: pet-hop 1.1s ease-in-out infinite; } .typing p span:nth-child(2) { animation-delay: .15s; } .typing p span:nth-child(3) { animation-delay: .3s; } @keyframes pet-hop { 0%, 60%, 100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-5px); opacity: 1; } } @media (prefers-reduced-motion: reduce) { .typing p span { animation: none; } } .cta { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; font-size: 14px; background: var(--soft); border-radius: 18px; padding: 10px 12px; margin: 0 0 10px; } .brain-notice { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; padding: 16px 18px; background: var(--soft); border: 2px solid var(--accent); border-radius: 22px; box-shadow: var(--shadow); } .brain-notice p { flex: 1 1 300px; min-width: 0; margin: 0; font-weight: 700; overflow-wrap: anywhere; } .brain-notice .primary { flex: 0 0 auto; }
.diary-pages { display: grid; gap: 14px; margin-top: 12px; } .diary-page { background: var(--paper); color: var(--paper-ink); border-radius: 16px; padding: 12px 18px 14px; line-height: 27px; background-image: repeating-linear-gradient(transparent 0 26px, var(--paper-line) 26px 27px); box-shadow: 0 2px 0 var(--paper-line); } .diary-page header { display: flex; justify-content: space-between; gap: 10px; font-size: 13px; font-weight: 800; color: #b07b8c; } .diary-page p { margin: 0 0 2px; font-size: 16px; } .diary-doodles { display: flex; gap: 10px; margin-top: 6px; } .shelf-items { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 10px; } .shelf-items li { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 10px 4px; font-size: 13px; font-weight: 700; background: var(--soft); border-radius: 16px; } .shelf-items li.muted { grid-column: 1 / -1; align-items: flex-start; background: none; padding: 0; font-weight: 600; } .memory-list { list-style: none; padding: 0; margin: 0; } .memory-list li { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 2px dotted var(--line); font-size: 15px; } #brain-bar { width: 100%; height: 10px; accent-color: var(--accent); margin-bottom: 10px; } .pet-toasts { position: fixed; left: 50%; bottom: max(16px, env(safe-area-inset-bottom)); transform: translateX(-50%); display: grid; gap: 8px; z-index: 10; width: max-content; max-width: calc(100vw - 32px); pointer-events: none; } /* notices: another tab, the upgrade dialog, tips */ .pet-notice { background: var(--card); border-radius: 22px; padding: 14px 18px; box-shadow: var(--shadow); margin: 0 0 16px; } .pet-notice p { margin: 0 0 10px; } .warn { font-weight: 800; color: var(--accent); } .pet-dialog { width: min(440px, calc(100vw - 32px)); border: 0; border-radius: 26px; padding: 18px 20px; color: var(--ink); background: var(--card); box-shadow: var(--shadow); } .pet-dialog::backdrop { background: rgba(34, 27, 41, .55); } .pet-dialog h2 { margin: 0 0 8px; font-size: 20px; } .pet-dialog p { margin: 0 0 10px; } .pet-dialog .pet-row { justify-content: flex-end; margin-top: 6px; } .bad-egg p { margin: 0 0 8px; } .needs-brain { opacity: .45; } .pet-toast { display: flex; align-items: center; gap: 10px; background: var(--ink); color: var(--bg); border-radius: 999px; padding: 9px 16px; font-size: 14px; font-weight: 700; box-shadow: var(--shadow); }
/* the notes, readable without JavaScript */ .notes { margin-top: 28px; display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; } .notes section { background: var(--card); border-radius: 22px; padding: 14px 18px; box-shadow: var(--shadow); } .notes h2 { margin: 2px 0 6px; } .notes p { margin: 0 0 8px; font-size: 15px; } .details { margin: 0; display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; font-size: 14px; } .details dt { font-weight: 800; } .details dd { margin: 0; overflow-wrap: anywhere; } .noscript { background: var(--card); border-radius: 18px; padding: 12px 16px; }
@media (max-width: 860px) { .pet-js .pet-app { grid-template-columns: minmax(0, 1fr); } .pet-device { max-width: 460px; width: 100%; margin: 14px auto 0; } } @media (max-width: 420px) { main { padding: 12px 12px 40px; } .hello h1 { font-size: 26px; } .pet-device { padding: 18px 10px 12px; border-radius: 30px 30px 24px 24px; } .pet-screen { padding: 8px; border-radius: 16px; } .pet-meters { grid-template-columns: 1fr; } .pet-actions { gap: 6px; } .pet-btn { min-height: 54px; font-size: 11px; border-radius: 14px; } .pet-panel { padding: 10px; border-radius: 22px; } .chat-log { height: 52vh; } .brain-notice { padding: 14px; } .brain-notice .primary { width: 100%; } } @media (prefers-reduced-motion: reduce) { * { transition: none !important; } } .debug-pre { white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.45 ui-monospace, monospace; max-height: 60vh; overflow: auto; background: var(--soft); padding: 8px; border-radius: 8px; margin: 6px 0; } .debug-out { background: var(--accent); color: var(--accent-ink); } .debug-grid { display: grid; gap: 4px; } .debug-stat { display: grid; grid-template-columns: 55px 1fr 30px; gap: 8px; align-items: center; } .debug-grid input[type=range] { width: 100%; min-width: 0; } #panel-debug summary { cursor: pointer; overflow-wrap: anywhere; } </style></head><body><main> <header class="hello"> <svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"> <path fill="#4a3b4f" d="M5 2h6v1h2v2h1v6h-1v2h-2v1H5v-1H3v-2H2V5h1V3h2z"/> <path fill="#ffd6e2" d="M5 3h6v1h1v2h1v5h-1v1h-1v1H5v-1H4v-1H3V6h1V4h1z"/> <path fill="#4a3b4f" d="M5 7h2v2H5zm4 0h2v2H9zM7 10h2v1H7z"/> <path fill="#f2668f" d="M4 10h1v1H4zm7 0h1v1h-1z"/> </svg> <div> <h1>tokenling</h1> <p>a tiny pet with a GPT-2 brain, at home on your device or a server</p> </div> </header> <noscript><p class="noscript">tokenling is an interactive pet and needs JavaScript. How it works is below.</p></noscript> <div id="pet-root"></div>
<div class="notes"> <section> <h2>Its brain is in charge</h2> <p>The pet's brain is GPT-2, a 124M-parameter language model from 2019, fine-tuned to live as a pet. It decides what to say, where to toddle, how to react to pats, whether it wants the snack, bath or medicine you offer, when to nap, and what to write in its diary at bedtime.</p> <p>The page only runs its body (getting hungry, sleepy and messy) and its little room. GPT-2 is small, so your pet is a bit silly. That's part of the charm.</p> </section> <section> <h2>Choose your pet's home</h2> <p>Choose local or remote when you get an egg, and switch either way later in settings. Locally, the brain downloads once (@SIZE_MB@ MB, 4-bit), runs with WebAssembly, and your pet, chats and diary stay in this browser. Remote mode uses a server you choose and trust: it stores your pet's save, chat and memories and keeps the pet thinking when this app is closed. This browser keeps a recovery copy, and exported saves can continue locally.</p> @ANALYTICS_NOTICE@ <p>Pets grow up in real time: a kid after 12 hours, a teen after 2 days, an adult after 5. They never die.</p> </section> <section> <h2>Details</h2> <dl class="details"> <dt>Model</dt><dd>@MODEL_ID@, from <a href="https://huggingface.co/openai-community/gpt2">GPT-2</a> (124M)</dd> <dt>Download</dt><dd>@SIZE_MB@ MB: 4-bit body, 8-bit embeddings</dd> <dt>Trained</dt><dd>@TRAINED@</dd> <dt>Runtime</dt><dd>transformers.js 4.3.1, ONNX Runtime Web (WebAssembly)</dd> <dt>Source</dt><dd><a href="@SOURCE@">source code</a> · <a href="@MODELS@@MODEL_ID@/SHA256SUMS">checksums</a></dd> </dl> </section> </div></main><script type="application/json" id="pet-config"> {"runtime": "@RUNTIME@", "models": "@MODELS@", "modelId": "@MODEL_ID@", "size": @SIZE@, "news": @NEWS@, "features": @FEATURES@, "older": @OLDER@, "remoteEndpoint": @REMOTE_ENDPOINT@, "sw": "sw.js", "icon": "@ASSETS@/icon-192.png"}</script><script type="module" src="@APP@/main.js"></script>@ANALYTICS@</body></html>