Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200import type { InstallPrompt } from "../types.ts";import { simulateBody } from "./body.ts";import { modelsBase, olderBrain } from "./brain.ts";import { CFG, DAY, HOUR, MIN } from "./config.ts";import { stageFor } from "./simulation.ts";import { save } from "./storage.ts";import { brain, emit, store, ui } from "./store.ts";
// sw.js (beside the page) keeps the page, the app and the runtime for offline// use; the brain itself already lives in Cache Storage. The page tells it// what to keep, so it never needs to know the site's layout.export const swReady = CFG.sw && "serviceWorker" in navigator ? navigator.serviceWorker .register(new URL(CFG.sw, location.href)) .then(() => navigator.serviceWorker.ready) .catch((e) => { console.warn("service worker", e); return null; }) : Promise.resolve(null);const APP_FILES = ["main.js", "brain.js"].map((f) => new URL(f, import.meta.url).href);export function pageResources(more: string[] = []) { const page = location.href.split(/[?#]/)[0]; const seen = performance.getEntriesByType("resource").map((e) => e.name); return [...new Set([page, ...APP_FILES, ...seen, ...more])].filter( (u) => u.startsWith(location.origin) && !u.startsWith(modelsBase) && !/\/sw\.js$/.test(u.split("?")[0]), );}// Called once the brain is up: by then everything the app needs has loaded,// so the worker can drop anything older (a previous app version or runtime).export async function keepOffline(workerResources: string[]) { const reg = await swReady; reg?.active?.postMessage({ type: "keep", urls: pageResources(workerResources), prune: true });}swReady.then((reg) => reg?.active?.postMessage({ type: "keep", urls: pageResources(), prune: false }),);
export const standalone = () => matchMedia("(display-mode: standalone)").matches || navigator.standalone === true;export const isIOS = () => /iPhone|iPad|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);export let installEvent: InstallPrompt | null = null;addEventListener("beforeinstallprompt", (e) => { e.preventDefault(); installEvent = e as InstallPrompt; emit();});addEventListener("appinstalled", () => { installEvent = null; emit();});export async function installApp() { if (!installEvent) return; installEvent.prompt(); try { await installEvent.userChoice; } catch { /* dismissed */ } installEvent = null; emit();}export const ALERT_TAG = "tokenling-alerts";async function alertsUrl() { const reg = await swReady; return reg && new URL("tokenling-alerts.json", reg.scope).href;}export function alertPlan() { if (!store.state.pet) return []; const state = structuredClone(store.state), n = store.state.pet.name, items: { kind: string; at: number; title: string; body: string }[] = []; const add = (kind: string, at: number, title: string, body: string) => { if (!items.some((i) => i.kind === kind)) items.push({ kind, at, title, body }); }; let t = Date.now(); const hungry0 = state.stats.tummy < 20, sick0 = state.sick; let poop = state.poop, stage = state.pet!.stage; for (const end = t + DAY; t < end && items.length < 4; t += 10 * MIN) { simulateBody(state, t, t + 10 * MIN, true); const at = t + 10 * MIN; if (!hungry0 && state.stats.tummy < 20) add("hungry", at, `${n} is getting hungry`, "A snack would help."); if (state.poop > poop) add("poop", at, `${n} made a mess`, "Time to clean up."); if (!sick0 && state.sick) add("sick", at, `${n} isn't feeling well`, "Medicine helps."); const st = stageFor(at - state.pet!.hatched); if (st !== stage) { stage = st; add("grew", at, `${n} grew into a ${st}!`, "Come and see."); } poop = state.poop; } return items;}// Rewritten whenever the page hides or shows. While it is visible nothing is// due (the worker also skips alerts while a window is in front).export async function planAlerts() { if (!store.tabOwned) return; const url = await alertsUrl(); if (!url) return; try { const c = await caches.open(ALERT_TAG); if (!store.state.settings.alerts || !store.state.pet) { await c.delete(url); return; } const icon = new URL(CFG.icon || "", location.href).href; await c.put( url, new Response(JSON.stringify({ icon, items: alertPlan() }), { headers: { "content-type": "application/json" }, }), ); } catch { /* optional */ }}// While the tab is hidden but still running, ask the worker once a minute.setInterval(async () => { if (!document.hidden || !store.state?.settings.alerts) return; (await swReady)?.active?.postMessage({ type: "alerts" });}, 60e3);export async function setAlerts(on: boolean) { store.state.settings.alertsAsked = true; if (!on) { store.state.settings.alerts = false; save(); planAlerts(); emit(); return; } const reg = await swReady; if (!reg || !("Notification" in self)) { ui.alertsNote = "This browser can't show alerts from here."; emit(); return; } const perm = Notification.permission === "granted" ? "granted" : await Notification.requestPermission(); if (perm !== "granted") { ui.alertsNote = "Notifications are blocked for this site; you can allow them in your browser's site settings."; store.state.settings.alerts = false; save(); emit(); return; } store.state.settings.alerts = true; save(); try { await reg.periodicSync?.register(ALERT_TAG, { minInterval: HOUR }); } catch { /* not offered: alerts work while the page runs */ } planAlerts(); emit();}
export function getTip() { const s = store.state; if (!s?.pet || brain.state !== "ready" || olderBrain()) return null; if (!standalone() && !s.settings.installAsked && (installEvent || isIOS())) { return { text: `Keep ${s.pet.name} close: install tokenling as an app. It opens from your home screen, works offline, and your browser is less likely to clear its storage.${installEvent ? "" : " Tap Share, then Add to Home Screen."}`, yesLabel: installEvent ? "install" : "got it", yes: () => { s.settings.installAsked = true; save(); void installApp(); emit(); }, no: () => { s.settings.installAsked = true; save(); emit(); }, }; } if (standalone() && !s.settings.alertsAsked && "Notification" in self) return { text: `Want a heads-up when ${s.pet.name} is hungry, made a mess or isn't feeling well? Alerts stay off unless you turn them on.`, yesLabel: "turn on alerts", yes: () => setAlerts(true), no: () => { s.settings.alertsAsked = true; save(); emit(); }, }; return null;}