import React from "react";
import { createRoot } from "react-dom/client";
import "../style.css";
const STORAGE_KEYS = {
artifacts: "home_artifacts",
collectibles: "home_collectibles",
plants: "home_plants",
visits: "home_visits",
notes: "home-memory-notes",
unlocks: "home-progression-unlocks",
};
const LEGACY_STORAGE_KEYS = {
artifacts: "home-artifacts",
collectibles: "home-collectibles",
plants: "home-garden-plants",
visits: "home-visit-count",
};
const sceneVideos = {
arrival: "/media/arrival-cottage.mp4",
kitchen: "/media/kitchen-late-night.mp4",
kitchenOriginal: "/media/kitchen-mom.mp4",
backyardA: "/media/backyard-cat-a.mp4",
backyardB: "/media/backyard-cat-b.mp4",
};
const fallbackNotes = [
{ title: "Urdu", body: "Tum mehfooz ho. Yeh ghar tumhari awaaz sunta hai." },
{ title: "Ammi", body: "Bring your tiredness inside. We will fold it gently." },
{ title: "Recipe", body: "Tea first. Then decisions. Then maybe more tea." },
{ title: "Tiny Win", body: "You came back. That counts more than you think." },
{ title: "Window", body: "Rain writes on the glass when no one knows what to say." },
];
const radioStations = [
{ id: "rain", icon: "π§οΈ", name: "Rain", caption: "generated pink-noise rainfall" },
{ id: "fireplace", icon: "π₯", name: "Fireplace", caption: "generated low crackle" },
{ id: "piano", icon: "πΉ", name: "Night Piano", caption: "generated slow pentatonic melody" },
{ id: "forest", icon: "π²", name: "Forest", caption: "generated forest bed and chirps" },
{ id: "lofi", icon: "π§", name: "LoFi", caption: "generated pulsing warm noise" },
];
const drawerItems = [
{ icon: "π₯£", title: "Old Recipe", body: "Soup for nights when the world asks too much." },
{ icon: "πͺ", title: "Cookie Recipe", body: "Add extra chocolate when courage feels thin." },
{ icon: "π", title: "Encouragement Letter", body: "You have survived every hard day so far." },
{ icon: "ποΈ", title: "Study Plan", body: "Twenty-five minutes. Tea. Five-minute window break. Repeat." },
{ icon: "πΌοΈ", title: "Childhood Photograph", body: "A small you laughing at a kitchen table." },
{ icon: "π", title: "Recipe Page", body: "How to begin again: warm water, clean page, tiny step." },
];
const drawerSlips = [
"Old Recipe: Cardamom chai for difficult days",
"Cookie Recipe: The one that fixes most things",
"Encouragement Letter: You were always going to be okay.",
"Study Plan: One concept. One hour. One cup of tea.",
"Note: The hard part is already behind you.",
];
const catCollectibles = [
{ icon: "πͺΆ", title: "Feather", unlock: "greenhouse", body: "Found near the wet porch." },
{ icon: "π", title: "Recipe Page", unlock: "study", body: "A page with flour on the corner." },
{ icon: "π»", title: "Pressed Flower", unlock: "greenhouse", body: "Still golden after being hidden." },
{ icon: "π", title: "Small Key", unlock: "attic", body: "It fits something upstairs." },
{ icon: "π", title: "Old Letter", unlock: "attic", body: "Folded twice and kept safe." },
{ icon: "π", title: "Lost Ribbon", unlock: "study", body: "A bookmark from another season." },
];
const reflectionQuestions = [
"What are you proud of today?",
"What do you wish your future self knew?",
"What are you ready to let go of?",
"What made today worth remembering?",
"What small thing deserves more credit?",
];
const momWelcomeMessage = `Welcome home.
The kettle is warm and I'm listening.
Tell me what happened today, what you're worried about, what you're proud of, or what you're trying to learn.
The house will remember what matters.`;
const momPromptChips = [
{ icon: "β", label: "I had a difficult day", text: "I had a difficult day today." },
{ icon: "π»", label: "I achieved something today", text: "I achieved something today." },
{ icon: "π", label: "Help me understand a topic", text: "Help me understand " },
{ icon: "π", label: "I want to remember something", text: "I want to remember " },
{ icon: "π―", label: "I have a goal I'm working on", text: "I have a goal I'm working on: " },
];
const kitchenRoomHints = [
{ icon: "π΅", label: "Kitchen", text: "talk with Mom" },
{ icon: "π", label: "Hallway", text: "keeps memories" },
{ icon: "π±", label: "Backyard", text: "grows goals" },
{ icon: "ποΈ", label: "Attic", text: "opens discoveries" },
{ icon: "π", label: "Cat", text: "finds hidden things" },
];
const objectMap = {
candle: { icon: "ποΈ", title: "Recovery Recipe Card", line: "Rest, one honest review, one kinder retry." },
sunflower: { icon: "π»", title: "Celebration Card", line: "The house saves the date in bright yellow ink." },
lesson: { icon: "π", title: "Handwritten Lesson Page", line: "Mom turns the question into a page you can keep." },
compass: { icon: "π§", title: "Next-Step Map", line: "Confusion becomes a path with one visible step." },
meal: { icon: "π", title: "Kitchen Meal Card", line: "A small warm meal appears on the kitchen table." },
tea: { icon: "β", title: "Comfort Tea Note", line: "A warm note appears where worry was sitting." },
};
const plantStages = [
{ name: "Seed", icon: "π±" },
{ name: "Sprout", icon: "πΏ" },
{ name: "Flower", icon: "πΈ" },
{ name: "Tree", icon: "π³" },
];
const itemStories = {
"Small Key": "The little key is not a trophy. It opens the locked trunk upstairs, and the trunk changes the hallway.",
"Old Letter": "The old letter remembers someone being proud before anyone knew how the story would end.",
"Recipe Page": "This flour-dusted page belongs in the kitchen recipe book. It can create a table recipe.",
"Pressed Flower": "The pressed flower can be planted in the greenhouse as proof that soft things can survive pressure.",
Feather: "The feather marks where the cat has walked. It unlocks a greenhouse clue.",
"Lost Ribbon": "The ribbon becomes a bookmark in the study, saving one useful page for later.",
};
function readStorage(key, fallback) {
try {
const parsed = JSON.parse(localStorage.getItem(key) || "null");
if (parsed !== null) return parsed;
const legacyKey = Object.values(LEGACY_STORAGE_KEYS).find((legacy) => {
if (key === STORAGE_KEYS.artifacts) return legacy === LEGACY_STORAGE_KEYS.artifacts;
if (key === STORAGE_KEYS.collectibles) return legacy === LEGACY_STORAGE_KEYS.collectibles;
if (key === STORAGE_KEYS.plants) return legacy === LEGACY_STORAGE_KEYS.plants;
return false;
});
if (legacyKey) {
const legacyParsed = JSON.parse(localStorage.getItem(legacyKey) || "null");
if (legacyParsed !== null) {
localStorage.setItem(key, JSON.stringify(legacyParsed));
return legacyParsed;
}
}
return fallback;
} catch {
return fallback;
}
}
function writeStorage(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
function dayOfYear(date = new Date()) {
const start = new Date(date.getFullYear(), 0, 0);
return Math.floor((date - start) / 86400000);
}
function classifyMessage(text) {
const lower = text.toLowerCase();
if (/^(hi|hello|hey|salam|assalamualaikum|assalamu alaikum|mom|mama|ammi)\b/.test(lower)) return "greeting";
if (lower.length < 2) return "unclear";
if (/(recursion|algorithm|explain|teach|lesson|learn|study|code|python|math|operating system|os\b|scheduling|data structure|linked list|stack|queue|tree|graph)/.test(lower)) return "lesson";
if (/(hungry|rice|food|eat|dinner|lunch|breakfast|roti|biryani|meal|cook|khana|bhook)/.test(lower)) return "meal";
if (/(selected|intern|offer|won|win|passed|success|achieved|achievement|got selected)/.test(lower)) return "sunflower";
if (/(confused|lost|don't know|dont know|direction|stuck)/.test(lower)) return "compass";
if (/(failed|fail|exam|sad|cry|broken|hard day)/.test(lower)) return "candle";
if (/(overwhelmed|tired|worked hard|anxious|scared|afraid|panic|exhausted)/.test(lower)) return "tea";
return "tea";
}
function artifactFromMessage(message, kind) {
const base = objectMap[kind] || objectMap.tea;
const hallwayMap = {
candle: { icon: "ποΈ", title: "Recovery Recipe Card", description: "The house remembered a hard moment and made a way back." },
sunflower: { icon: "π", title: "Acceptance Letter", description: "A bright milestone pinned where future-you can see it." },
lesson: { icon: "π", title: /recursion/i.test(message) ? "Algorithm Notebook" : "Handwritten Lesson Page", description: "A question became something you can revisit." },
compass: { icon: "π§", title: "Next-Step Map", description: "The hallway kept the first direction you found." },
meal: { icon: "π", title: "Kitchen Meal Card", description: "The kitchen remembered that care can be practical: something warm to eat." },
tea: { icon: "β", title: "Tea-Stained Note", description: "A tired day was not thrown away. It was cared for." },
};
const hall = hallwayMap[kind] || hallwayMap.tea;
return {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
kind,
icon: hall.icon,
tableIcon: base.icon,
title: hall.title,
tableTitle: base.title,
description: hall.description,
tableLine: base.line,
source: message.slice(0, 150),
date: new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }),
};
}
function requestedArtifactFromMessage(message) {
const lower = message.toLowerCase();
let artifact = { icon: "π―οΈ", title: "A Small Note", description: "Left on the table while the candle was still lit.", kind: "note" };
if (/(failed|sad|broken)/.test(lower)) {
artifact = { icon: "π", title: "Recovery Note", description: "A page torn gently from Mom's notebook.", kind: "candle" };
} else if (/(selected|won|internship|intern)/.test(lower)) {
artifact = { icon: "π»", title: "Celebration Card", description: "The kitchen smelled like sunflowers that day.", kind: "sunflower" };
} else if (/(explain|learn|study|recursion)/.test(lower)) {
artifact = { icon: "π", title: "Lesson Page", description: "Written in careful handwriting, twice underlined.", kind: "lesson" };
} else if (/(tired|overwhelmed|scared)/.test(lower)) {
artifact = { icon: "β", title: "Comfort Tea Note", description: "Steep for 3 minutes. Breathe for 3 more.", kind: "tea" };
}
return {
id: Date.now(),
type: "artifact",
kind: artifact.kind,
icon: artifact.icon,
tableIcon: artifact.icon,
title: artifact.title,
tableTitle: artifact.title,
date: new Date().toLocaleDateString(),
description: artifact.description,
tableLine: artifact.description,
source: message.slice(0, 150),
};
}
function makeArtifact(kind, source, overrides = {}) {
const base = objectMap[kind] || objectMap.tea;
return {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
kind,
icon: overrides.icon || base.icon,
tableIcon: overrides.tableIcon || overrides.icon || base.icon,
title: overrides.title || base.title,
tableTitle: overrides.tableTitle || overrides.title || base.title,
description: overrides.description || base.line,
tableLine: overrides.tableLine || overrides.description || base.line,
source,
date: new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }),
};
}
const CORRUPT_GLYPH_CODES = new Set([0x00f0, 0x00c3, 0x00e2, 0x00d8, 0x00d9, 0x00db, 0xfffd]);
function isCorruptGlyph(value) {
return !value || Array.from(String(value)).some((char) => CORRUPT_GLYPH_CODES.has(char.charCodeAt(0)));
}
function inferArtifactIcon(artifact, forTable = false) {
const title = forTable ? artifact.tableTitle || artifact.title : artifact.title || artifact.tableTitle;
const text = `${title || ""} ${artifact.kind || ""} ${artifact.description || ""} ${artifact.tableLine || ""}`.toLowerCase();
if (/feather/.test(text)) return "πͺΆ";
if (/pressed flower|celebration|acceptance|sunflower/.test(text)) return "π»";
if (/small key|key/.test(text)) return "π";
if (/old letter|letter/.test(text)) return "π";
if (/lost ribbon|ribbon/.test(text)) return "π";
if (/cookie/.test(text)) return "πͺ";
if (/drawer|note/.test(text) && !/tea/.test(text)) return "π";
if (/meal|rice|food/.test(text)) return "π";
if (/tea|comfort|steaming/.test(text)) return forTable ? "π΅" : "β";
if (/lesson|study|recursion|algorithm|notebook/.test(text)) return "π";
if (/recipe|recovery|page|book/.test(text)) return "π";
if (/candle|small note/.test(text)) return "π―οΈ";
if (/compass|map|confusion|direction/.test(text)) return "π§";
return "π";
}
function normalizeArtifact(artifact, index = 0) {
const safeArtifact = artifact && typeof artifact === "object" ? artifact : {};
const title = safeArtifact.title || safeArtifact.tableTitle || "House Memory";
const description = safeArtifact.description || safeArtifact.tableLine || safeArtifact.body || safeArtifact.source || "The house kept this memory.";
const icon = isCorruptGlyph(safeArtifact.icon) ? inferArtifactIcon({ ...safeArtifact, title, description }) : safeArtifact.icon;
const tableIcon = isCorruptGlyph(safeArtifact.tableIcon)
? inferArtifactIcon({ ...safeArtifact, title, description }, true)
: safeArtifact.tableIcon;
return {
...safeArtifact,
id: safeArtifact.id || `${Date.now()}-${index}`,
icon,
tableIcon: tableIcon || icon,
title,
tableTitle: safeArtifact.tableTitle || title,
date: safeArtifact.date || new Date().toLocaleDateString(),
description,
tableLine: safeArtifact.tableLine || description,
};
}
function dedupeArtifactsByTitleDate(list) {
const seen = new Set();
return list.filter((item) => {
const key = `${item.title || ""}-${item.date || ""}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function readArtifacts() {
try {
const saved = JSON.parse(localStorage.getItem("home_artifacts") || "[]");
if (Array.isArray(saved) && saved.length) {
const cleaned = dedupeArtifactsByTitleDate(saved.map(normalizeArtifact));
if (cleaned.length !== saved.length) {
writeStorage(STORAGE_KEYS.artifacts, cleaned);
}
return cleaned;
}
} catch {
// Fall back to the shared migration path below.
}
return dedupeArtifactsByTitleDate(readStorage(STORAGE_KEYS.artifacts, []).map(normalizeArtifact));
}
function plantFromReflection(text) {
const lower = text.toLowerCase();
if (/(achieved|goal|finished|selected|passed|won)/.test(lower)) return { title: "Memory Tree", body: text };
if (/(presentation|shared|brave|proud|confidence)/.test(lower)) return { title: "Courage Flower", body: text };
if (/(let go|release|forgive|rest)/.test(lower)) return { title: "Letting-Go Leaf", body: text };
return { title: "Small Seed", body: text };
}
function normalizePlant(plant) {
const stageIndex = Math.max(0, Math.min(3, Number.isFinite(Number(plant.stage)) ? Number(plant.stage) : Number(plant.growth || 0)));
const stage = plantStages[stageIndex];
return {
...plant,
text: plant.text || plant.body || "",
body: plant.body || plant.text || "",
growth: stageIndex,
stage: stageIndex,
stageName: stage.name,
icon: stage.icon,
history: plant.history || [`Started as ${stage.name.toLowerCase()}.`],
};
}
function growPlant(plant, reason) {
const nextGrowth = Math.min(3, Number(plant.stage ?? plant.growth ?? 0) + 1);
const stage = plantStages[nextGrowth];
return {
...plant,
growth: nextGrowth,
stage: nextGrowth,
stageName: stage.name,
icon: stage.icon,
history: [...(plant.history || []), `${stage.name}: ${reason}`].slice(-8),
};
}
function houseContextLine(context) {
const pieces = [];
if (context?.lastExamMemory) pieces.push("Last time, the hallway kept your exam worry safe.");
if (context?.confidencePlant) pieces.push(`Your ${context.confidencePlant.title} is now a ${context.confidencePlant.stageName || plantStages[context.confidencePlant.stage]?.name || "new stage"}.`);
if (context?.lastCollectible) pieces.push(`The cat recently brought ${context.lastCollectible.title}.`);
if (context?.lastLesson) pieces.push(`Your study room saved ${context.lastLesson.title}.`);
return pieces.length ? `${pieces[0]} ` : "";
}
function lessonFallback(text) {
const lower = text.toLowerCase();
if (/scheduling|operating system|os\b/.test(lower)) {
return "Come sit beside me. I made a scheduling note: an operating system chooses which process gets the CPU next, a little like deciding which pot on the stove needs attention first.";
}
if (/data structure|stack|queue|linked list|tree|graph/.test(lower)) {
return "Come sit beside me. I made a data structures note: choose the container that matches the job. A stack remembers last-in first-out, a queue remembers first-in first-out, and a tree keeps related choices branching neatly.";
}
return "Come sit beside me. I made a handwritten lesson page. Recursion is when a problem calls a smaller version of itself until it reaches a simple stopping point.";
}
function localMomReply(text, kind, context = {}) {
const prefix = houseContextLine(context);
const replies = {
greeting:
`${prefix}Hi jaan. I am here. Tell me what you need: comfort, food, study help, or just someone to sit with you.`,
unclear:
"I am listening, honey, but I only caught a tiny piece of that. Say a little more and I will understand.",
candle:
"Bring your bags inside, honey. I made a recovery recipe card for the table. A failed exam is a moment, not your whole story.",
sunflower:
"Oh, my bright one. I put a celebration card on the table. Tell me the little details so the house can remember them properly.",
lesson: lessonFallback(text),
compass:
"Confused is not lost forever. I placed a next-step map on the table. We only need one honest step at a time.",
meal:
"Hungry? Come to the kitchen. I can make rice warm and soft first, then we can add something simple beside it. Sit down, drink water, and tell me if you want plain rice, spicy rice, or comfort food.",
tea:
"You sound full to the brim. I made a comfort tea note. Let the room be quiet around you for a minute.",
};
return { object: kind, reply: replies[kind] || `${prefix}${replies.tea}` };
}
function getMomFallback(text) {
const t = text.toLowerCase();
if (/(fail|exam|sad|broken|cry)/.test(t))
return "Sit down, beta. A hard day is not a lost one. The tea is warm and so is this chair.";
if (/(selected|internship|won|passed|got)/.test(t))
return "Oh, my heart. I knew this was coming. Tell me everything - even the small parts.";
if (/(confused|stuck|lost|direction)/.test(t))
return "Confused means you are still thinking. That is not lost. That is searching.";
if (/(scared|afraid|anxious|panic)/.test(t))
return "Come closer to the light. Fear gets smaller near warm things. You are safe here.";
if (/(tired|exhausted|overwhelmed)/.test(t))
return "You have done enough for today. The house will still be here when you wake up rested.";
return "The house heard you. Whatever you are carrying, you can set it down here for a moment.";
}
async function askMom(userMessage, history = [], houseState = {}) {
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: userMessage,
history,
house_state: houseState,
}),
});
if (!response.ok) throw new Error(`Mom API returned ${response.status}`);
const data = await response.json();
if (data.reply) {
return {
reply: data.reply.trim(),
source: data.source || "api",
model: data.model || "unknown",
};
}
} catch (e) {
console.log("Mom API error, using local fallback", e);
}
return {
reply: getMomFallback(userMessage),
source: "local",
model: "local-mom-fallback",
};
}
function makeAudio() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
return AudioContext ? new AudioContext() : null;
}
function playDoorSound() {
const audio = makeAudio();
if (!audio) return;
const gain = audio.createGain();
const low = audio.createOscillator();
const high = audio.createOscillator();
low.type = "sawtooth";
high.type = "triangle";
low.frequency.setValueAtTime(95, audio.currentTime);
low.frequency.exponentialRampToValueAtTime(48, audio.currentTime + 0.62);
high.frequency.setValueAtTime(380, audio.currentTime);
high.frequency.exponentialRampToValueAtTime(150, audio.currentTime + 0.38);
gain.gain.setValueAtTime(0.0001, audio.currentTime);
gain.gain.exponentialRampToValueAtTime(0.045, audio.currentTime + 0.04);
gain.gain.exponentialRampToValueAtTime(0.0001, audio.currentTime + 0.68);
low.connect(gain);
high.connect(gain);
gain.connect(audio.destination);
low.start();
high.start();
low.stop(audio.currentTime + 0.7);
high.stop(audio.currentTime + 0.5);
}
function playMeow() {
const audio = makeAudio();
if (!audio) return;
const osc = audio.createOscillator();
const gain = audio.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(640, audio.currentTime);
osc.frequency.exponentialRampToValueAtTime(330, audio.currentTime + 0.22);
gain.gain.setValueAtTime(0.0001, audio.currentTime);
gain.gain.exponentialRampToValueAtTime(0.075, audio.currentTime + 0.03);
gain.gain.exponentialRampToValueAtTime(0.0001, audio.currentTime + 0.34);
osc.connect(gain);
gain.connect(audio.destination);
osc.start();
osc.stop(audio.currentTime + 0.36);
}
const radioEngineRef = { current: null };
function noiseBuffer(context, seconds = 2) {
const buffer = context.createBuffer(1, context.sampleRate * seconds, context.sampleRate);
const data = buffer.getChannelData(0);
let pink = 0;
for (let i = 0; i < data.length; i += 1) {
const white = Math.random() * 2 - 1;
pink = 0.96 * pink + 0.04 * white;
data[i] = pink * 0.9;
}
return buffer;
}
function clearRadioNodes(engine) {
engine.timers.forEach((timer) => window.clearInterval(timer));
engine.timers = [];
engine.nodes.forEach((node) => {
try {
if (node.stop) node.stop();
} catch {
// Already stopped.
}
try {
node.disconnect();
} catch {
// Already disconnected.
}
});
engine.nodes = [];
}
function getRadioEngine() {
if (radioEngineRef.current) return radioEngineRef.current;
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return null;
const context = new AudioContext();
const master = context.createGain();
master.gain.value = 0.7;
master.connect(context.destination);
radioEngineRef.current = { context, master, nodes: [], timers: [], station: null };
return radioEngineRef.current;
}
function connectLoopingNoise(engine, filterType, frequency, q = 0.7, gainValue = 0.18) {
const { context, master } = engine;
const source = context.createBufferSource();
source.buffer = noiseBuffer(context, 3);
source.loop = true;
const filter = context.createBiquadFilter();
filter.type = filterType;
filter.frequency.value = frequency;
filter.Q.value = q;
const gain = context.createGain();
gain.gain.value = gainValue;
source.connect(filter);
filter.connect(gain);
gain.connect(master);
source.start();
engine.nodes.push(source, filter, gain);
return { source, filter, gain };
}
function playTone(engine, frequency, start, duration, gainValue = 0.06, type = "sine") {
const { context, master } = engine;
const osc = context.createOscillator();
const gain = context.createGain();
osc.type = type;
osc.frequency.setValueAtTime(frequency, start);
gain.gain.setValueAtTime(0.0001, start);
gain.gain.linearRampToValueAtTime(gainValue, start + 0.04);
gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
osc.connect(gain);
gain.connect(master);
osc.start(start);
osc.stop(start + duration + 0.04);
engine.nodes.push(osc, gain);
}
async function startAmbientStation(stationId, volume) {
const engine = getRadioEngine();
if (!engine) throw new Error("Web Audio API unavailable");
if (engine.context.state === "suspended") await engine.context.resume();
clearRadioNodes(engine);
engine.station = stationId;
engine.master.gain.value = volume;
if (stationId === "rain") {
connectLoopingNoise(engine, "lowpass", 1400, 0.45, 0.4);
connectLoopingNoise(engine, "highpass", 1800, 0.2, 0.08);
}
if (stationId === "fireplace") {
connectLoopingNoise(engine, "lowpass", 520, 0.9, 0.35);
const crackle = () => {
for (let i = 0; i < 4; i += 1) {
const when = engine.context.currentTime + Math.random() * 0.8;
playTone(engine, 140 + Math.random() * 900, when, 0.035 + Math.random() * 0.06, 0.08, "triangle");
}
};
crackle();
engine.timers.push(window.setInterval(crackle, 720));
}
if (stationId === "piano") {
const notes = [261.63, 293.66, 329.63, 392.0, 440.0, 392.0, 329.63, 293.66];
let index = 0;
const melody = () => {
playTone(engine, notes[index % notes.length], engine.context.currentTime, 1.15, 0.3, "sine");
playTone(engine, notes[(index + 2) % notes.length] / 2, engine.context.currentTime, 1.6, 0.12, "sine");
index += 1;
};
melody();
engine.timers.push(window.setInterval(melody, 1450));
}
if (stationId === "forest") {
connectLoopingNoise(engine, "bandpass", 760, 0.65, 0.35);
const chirp = () => {
const now = engine.context.currentTime;
playTone(engine, 1600 + Math.random() * 1200, now, 0.12, 0.07, "sine");
playTone(engine, 2100 + Math.random() * 900, now + 0.14, 0.1, 0.05, "sine");
};
engine.timers.push(window.setInterval(chirp, 2600 + Math.random() * 1800));
}
if (stationId === "lofi") {
const bed = connectLoopingNoise(engine, "bandpass", 620, 0.5, 0.3);
const pulse = () => {
const now = engine.context.currentTime;
bed.gain.gain.cancelScheduledValues(now);
bed.gain.gain.setValueAtTime(0.18, now);
bed.gain.gain.linearRampToValueAtTime(0.35, now + 0.08);
bed.gain.gain.exponentialRampToValueAtTime(0.18, now + 0.45);
playTone(engine, 98, now, 0.18, 0.12, "sine");
};
pulse();
engine.timers.push(window.setInterval(pulse, 850));
}
}
function stopAmbientRadio() {
const engine = radioEngineRef.current;
if (!engine) return;
clearRadioNodes(engine);
engine.station = null;
}
function setAmbientRadioVolume(volume) {
const engine = radioEngineRef.current;
if (engine) engine.master.gain.value = volume;
}
function App() {
const [scene, setScene] = React.useState("arrival");
const [transitioning, setTransitioning] = React.useState(false);
const [notes, setNotes] = React.useState(fallbackNotes);
const [selectedMemory, setSelectedMemory] = React.useState(null);
const [selectedNote, setSelectedNote] = React.useState(fallbackNotes[0]);
const [noteDraft, setNoteDraft] = React.useState("");
const [artifacts, setArtifacts] = React.useState(() => readArtifacts());
const [collectibles, setCollectibles] = React.useState(() => readStorage(STORAGE_KEYS.collectibles, []));
const [plants, setPlants] = React.useState(() => readStorage(STORAGE_KEYS.plants, []).map(normalizePlant));
const [progression, setProgression] = React.useState(() => readStorage(STORAGE_KEYS.unlocks, {
trunkOpen: false,
atticLetterRevealed: false,
kitchenRecipeUnlocked: false,
hiddenMemoryUnlocked: false,
studyBookmarkUnlocked: false,
}));
const [visits, setVisits] = React.useState(() => Number(localStorage.getItem(STORAGE_KEYS.visits) || localStorage.getItem(LEGACY_STORAGE_KEYS.visits) || "0"));
const [tableArtifacts, setTableArtifacts] = React.useState(() => readArtifacts().slice(-5));
const [drawerItem, setDrawerItem] = React.useState(null);
const [catState, setCatState] = React.useState("sleeping");
const [catBubble, setCatBubble] = React.useState("");
const [momInput, setMomInput] = React.useState("");
const [messages, setMessages] = React.useState([
{ role: "mom", text: momWelcomeMessage },
]);
const [momDraft, setMomDraft] = React.useState("");
const [momThinking, setMomThinking] = React.useState(false);
const [chatOpen, setChatOpen] = React.useState(true);
const [apiStatus, setApiStatus] = React.useState({ state: "checking", label: "Checking Mom API..." });
const [radioStation, setRadioStation] = React.useState(0);
const [radioPlaying, setRadioPlaying] = React.useState(false);
const [radioVolume, setRadioVolume] = React.useState(0.7);
const [backyardVideo, setBackyardVideo] = React.useState(sceneVideos.backyardA);
const [benchOpen, setBenchOpen] = React.useState(true);
const [reflection, setReflection] = React.useState("");
const [questionIndex] = React.useState(() => dayOfYear() % reflectionQuestions.length);
const [sceneNote, setSceneNote] = React.useState("");
const [parallax, setParallax] = React.useState({ x: 0, y: 0 });
const streamRef = React.useRef(null);
const momThreadRef = React.useRef(null);
const unlocks = React.useMemo(
() => ({
attic: progression.trunkOpen || collectibles.some((item) => item.title === "Small Key") || visits >= 3,
study: progression.studyBookmarkUnlocked || artifacts.length >= 3 || collectibles.some((item) => item.unlock === "study"),
greenhouse: plants.length >= 3 || collectibles.some((item) => item.unlock === "greenhouse") || progression.hiddenMemoryUnlocked,
}),
[artifacts.length, collectibles, plants.length, progression, visits]
);
React.useEffect(() => {
const nextVisits = Number(localStorage.getItem(STORAGE_KEYS.visits) || localStorage.getItem(LEGACY_STORAGE_KEYS.visits) || visits || "0") + 1;
setVisits(nextVisits);
localStorage.setItem(STORAGE_KEYS.visits, String(nextVisits));
const todayKey = String(dayOfYear());
const lastGrowthDay = localStorage.getItem("home_plants_last_growth_day");
if (lastGrowthDay !== todayKey) {
const storedPlants = readStorage(STORAGE_KEYS.plants, []).map(normalizePlant);
if (storedPlants.length) {
const grownPlants = storedPlants.map((plant) => growPlant(plant, "A new day opened Home."));
setPlants(grownPlants);
writeStorage(STORAGE_KEYS.plants, grownPlants);
}
localStorage.setItem("home_plants_last_growth_day", todayKey);
}
async function loadApiStatus() {
try {
const response = await fetch("/api/status");
if (!response.ok) throw new Error("status unavailable");
const data = await response.json();
setApiStatus({
state: data.groq_ready ? "online" : "local",
label: data.groq_ready ? "Mom is listening" : "Mom is nearby",
});
} catch {
setApiStatus({ state: "offline", label: "Mom is nearby" });
}
}
async function loadNotes() {
const stored = readStorage(STORAGE_KEYS.notes, []);
try {
const response = await fetch("/api/notes");
if (!response.ok) throw new Error("No notes endpoint");
const data = await response.json();
const loaded = data.notes?.length ? data.notes : stored;
if (loaded.length) {
const next = [...fallbackNotes, ...loaded].slice(-12);
setNotes(next);
setSelectedNote(next[0]);
}
} catch {
if (stored.length) {
const next = [...fallbackNotes, ...stored].slice(-12);
setNotes(next);
setSelectedNote(next[0]);
}
}
}
loadApiStatus();
loadNotes();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (momThreadRef.current && chatOpen) {
momThreadRef.current.scrollTop = momThreadRef.current.scrollHeight;
}
}, [messages, momDraft, momThinking, chatOpen]);
React.useEffect(() => {
if (scene !== "memory") return;
const cleaned = dedupeArtifactsByTitleDate(artifacts);
if (cleaned.length !== artifacts.length) {
persistArtifacts(cleaned);
setSelectedMemory(cleaned.at(-1) || null);
}
}, [scene, artifacts]);
React.useEffect(() => {
let timeout;
function scheduleCat() {
timeout = window.setTimeout(() => {
const states = ["sleeping", "walking", "sitting", "looking"];
setCatState(states[Math.floor(Math.random() * states.length)]);
scheduleCat();
}, 30000 + Math.random() * 30000);
}
scheduleCat();
return () => window.clearTimeout(timeout);
}, []);
React.useEffect(() => () => {
window.clearInterval(streamRef.current);
stopRadio();
}, []);
React.useEffect(() => {
setAmbientRadioVolume(radioVolume);
}, [radioVolume]);
function persistArtifacts(next) {
const normalized = dedupeArtifactsByTitleDate(next.map(normalizeArtifact));
setArtifacts(normalized);
setTableArtifacts(normalized.slice(-5));
writeStorage(STORAGE_KEYS.artifacts, normalized);
}
function persistCollectibles(next) {
setCollectibles(next);
writeStorage(STORAGE_KEYS.collectibles, next);
}
function persistPlants(next) {
const normalized = next.map(normalizePlant);
setPlants(normalized);
writeStorage(STORAGE_KEYS.plants, normalized);
}
function persistProgression(next) {
setProgression(next);
writeStorage(STORAGE_KEYS.unlocks, next);
}
function rememberArtifact(artifact, toast = "The house placed something on the table.") {
const nextArtifacts = [...artifacts, artifact].slice(-30);
persistArtifacts(nextArtifacts);
setSelectedMemory(artifact);
setSceneNote(toast);
}
function buildHouseState() {
const examMemory = [...artifacts].reverse().find((item) => /exam|fail|recovery/i.test(`${item.title} ${item.source} ${item.description}`));
const confidencePlant = [...plants].reverse().find((plant) => /confidence|courage|presentation|proud/i.test(`${plant.title} ${plant.body}`));
const lastLesson = [...artifacts].reverse().find((item) => item.kind === "lesson");
return {
memory_count: artifacts.length,
plant_count: plants.length,
collectible_count: collectibles.length,
last_memory: artifacts.at(-1)?.title || "",
last_collectible: collectibles.at(-1)?.title || "",
last_plant: plants.at(-1)?.title || "",
lastExamMemory: examMemory,
confidencePlant,
lastCollectible: collectibles.at(-1),
lastLesson,
progression,
};
}
function createHiddenMemory(title, description, source, kind = "attic") {
const artifact = makeArtifact(kind, source, {
icon: "π",
tableIcon: "π",
title,
tableTitle: title,
description,
tableLine: description,
});
rememberArtifact(artifact, `${title} joined the hallway.`);
return artifact;
}
function unlockKitchenRecipe(source = "The recipe page clicked into the counter book.") {
persistProgression({ ...progression, kitchenRecipeUnlocked: true });
const artifact = makeArtifact("meal", source, {
icon: "π",
tableIcon: "π",
title: "Unlocked Kitchen Recipe",
tableTitle: "Warm Rice Recipe",
description: "The recipe book now knows: rice, water, salt, patience, and one small kindness.",
tableLine: "Warm Rice Recipe unlocked from an attic page.",
});
rememberArtifact(artifact, "The recipe page unlocked a kitchen recipe.");
travel("kitchen", "The recipe page fluttered down to the kitchen.");
}
function revealOldLetter(source = "Opened the attic trunk with the small key.") {
const alreadyHasLetter = collectibles.some((item) => item.title === "Old Letter");
const nextProgression = { ...progression, trunkOpen: true, atticLetterRevealed: true };
persistProgression(nextProgression);
if (!alreadyHasLetter) {
const letter = {
...catCollectibles.find((item) => item.title === "Old Letter"),
id: `${Date.now()}-attic-letter`,
date: new Date().toLocaleDateString(undefined, { month: "short", day: "numeric" }),
};
persistCollectibles([...collectibles, letter].slice(-24));
}
createHiddenMemory(
"Old Letter Memory",
"The trunk revealed a letter about being loved before you had proof you would succeed.",
source
);
}
function inspectCollectible(item, location = "shelf") {
if (!item) return;
setSelectedMemory(null);
const title = item.title;
if (location === "hallway") {
setSelectedMemory({
id: item.id || `${title}-collectible`,
kind: "collectible",
icon: item.icon,
title,
date: item.date || "Found by the cat",
description: itemStories[title] || item.body || "The cat brought this home because it matters.",
source: "Cat discovery",
relatedRoom: title === "Recipe Page" ? "Kitchen" : title === "Pressed Flower" ? "Backyard garden" : "House Memory Hallway",
});
return;
}
if (title === "Small Key") {
if (!progression.trunkOpen) {
revealOldLetter("Small Key opened the locked attic trunk.");
setSceneNote("The Small Key opened the locked trunk and revealed an Old Letter.");
} else {
setSceneNote("The Small Key already opened the trunk. The attic remembers the turn of the lock.");
}
travel("attic", "The key pulls your attention upstairs.");
return;
}
if (title === "Old Letter") {
persistProgression({ ...progression, hiddenMemoryUnlocked: true, atticLetterRevealed: true });
createHiddenMemory(
"Letter From Before",
"The old letter became a hallway memory: someone believed in you before the result arrived.",
item.body
);
travel("memory", "The Old Letter pinned itself to the hallway.");
return;
}
if (title === "Recipe Page") {
unlockKitchenRecipe(item.body);
return;
}
if (title === "Pressed Flower" || title === "Dried Flower") {
const flower = normalizePlant({
id: `${Date.now()}-pressed-flower`,
title: "Pressed Flower",
body: "Planted from the cat's discovery.",
growth: 2,
date: new Date().toLocaleDateString(undefined, { month: "short", day: "numeric" }),
history: ["Found by the cat.", "Flower: planted in the greenhouse."],
});
persistPlants([...plants, flower].slice(-24));
persistProgression({ ...progression, hiddenMemoryUnlocked: true });
setSceneNote("The pressed flower became a greenhouse bloom.");
travel("greenhouse", "The pressed flower takes root.");
return;
}
if (title === "Lost Ribbon") {
persistProgression({ ...progression, studyBookmarkUnlocked: true });
const artifact = makeArtifact("lesson", item.body, {
icon: "π",
tableIcon: "π",
title: "Ribbon Bookmark",
tableTitle: "Saved Study Page",
description: "The ribbon marked a study page so the lesson can be found again.",
});
rememberArtifact(artifact, "The lost ribbon became a study bookmark.");
travel("study", "The ribbon saves a page in the study.");
return;
}
if (title === "Feather") {
persistProgression({ ...progression, hiddenMemoryUnlocked: true });
setSceneNote(`${title}: ${itemStories[title] || item.body}`);
travel("greenhouse", "The feather points toward the greenhouse.");
return;
}
setSceneNote(`${title}: ${itemStories[title] || item.body || "The house has not decoded this yet."}`);
if (location === "attic") {
createHiddenMemory(`${title} Story`, itemStories[title] || item.body, `Inspected ${title} in the attic.`);
}
}
function inspectMemory(artifact) {
setSelectedMemory(artifact);
if (!artifact || artifact.id === "empty") return;
const sourceText = artifact.source ? ` Created from: "${artifact.source}"` : "";
const action = artifact.kind === "meal"
? " The recipe can return to the kitchen table."
: artifact.kind === "lesson"
? " This can be opened in the study as a learning note."
: artifact.kind === "candle"
? " Mom can turn this into a recovery plan."
: " It can be returned to the kitchen table or discussed with Mom.";
setSceneNote(`${artifact.title}: ${artifact.description}${sourceText}${action}`);
}
function tendPlant(plantId) {
const target = plants.find((plant) => plant.id === plantId);
if (!target) return;
const nextPlants = plants.map((plant) =>
plant.id === plantId ? growPlant(plant, "Tended by hand.") : plant
);
persistPlants(nextPlants);
const updated = nextPlants.find((plant) => plant.id === plantId);
setSceneNote(`${updated.title} is now a ${updated.stageName}.`);
}
function restoreArtifactToTable(artifact = selectedMemory) {
if (!artifact?.id) return;
const withoutDuplicate = tableArtifacts.filter((item) => item.id !== artifact.id);
setTableArtifacts([...withoutDuplicate, artifact].slice(-5));
setSceneNote(`${artifact.tableTitle || artifact.title} returned to the kitchen table.`);
}
function removeSelectedMemory() {
if (!selectedMemory?.id || selectedMemory.id === "empty") return;
const nextArtifacts = artifacts.filter((item) => item.id !== selectedMemory.id);
persistArtifacts(nextArtifacts);
setSelectedMemory(nextArtifacts.at(-1) || null);
setSceneNote("The hallway took down one old note.");
}
function tidyHallway() {
const nextArtifacts = dedupeArtifactsByTitleDate(artifacts);
persistArtifacts(nextArtifacts);
setSelectedMemory(nextArtifacts.at(-1) || null);
setSceneNote("The hallway kept the first copy of each memory and cleared duplicate clutter.");
}
function askMomAboutMemory() {
if (!selectedMemory?.id || selectedMemory.id === "empty") {
setSceneNote("Choose a hallway memory first, then Mom can help turn it into a plan.");
return;
}
setChatOpen(true);
setMomInput(`Help me understand this memory: ${selectedMemory.title}. ${selectedMemory.description}`);
travel("kitchen", "Mom pulls that memory back to the kitchen table.");
}
function fillMomPrompt(text) {
setMomInput(text);
setChatOpen(true);
window.setTimeout(() => {
document.getElementById("mom-message")?.focus();
}, 30);
}
function travel(nextScene, note = "") {
const safeScene = ["attic", "study", "greenhouse"].includes(nextScene) ? "kitchen" : nextScene;
if (safeScene === scene) return;
setTransitioning(true);
setSceneNote(safeScene === nextScene ? note : "That room is resting for the demo, so Mom brings you back to the kitchen.");
window.setTimeout(() => {
setScene(safeScene);
if (safeScene === "backyard") {
setBenchOpen(true);
setBackyardVideo((current) =>
current === sceneVideos.backyardA ? sceneVideos.backyardB : sceneVideos.backyardA
);
}
}, 360);
window.setTimeout(() => {
setTransitioning(false);
setSceneNote("");
}, 920);
}
function enterHome() {
playDoorSound();
travel("kitchen", "The door opens. The kitchen is awake.");
}
function handleMouseMove(event) {
const rect = event.currentTarget.getBoundingClientRect();
setParallax({
x: ((event.clientX - rect.left) / rect.width - 0.5) * 18,
y: ((event.clientY - rect.top) / rect.height - 0.5) * 18,
});
}
function streamReply(reply, nextMessages, onComplete) {
window.clearInterval(streamRef.current);
setMomThinking(true);
setMomDraft("Mom is writing");
window.setTimeout(() => {
setMomThinking(false);
const words = reply.split(" ");
let index = 0;
setMomDraft("");
streamRef.current = window.setInterval(() => {
index += 1;
setMomDraft(words.slice(0, index).join(" "));
if (index >= words.length) {
window.clearInterval(streamRef.current);
setMomDraft("");
setMessages([...nextMessages, { role: "mom", text: reply }]);
if (onComplete) onComplete();
setCatState("walking");
window.setTimeout(() => setCatState("sleeping"), 4300);
}
}, 72);
}, 720);
}
async function sendToMom(event) {
event.preventDefault();
const clean = momInput.trim();
if (!clean) return;
const kind = classifyMessage(clean);
const nextMessages = [...messages, { role: "you", text: clean }];
setMessages(nextMessages);
setMomInput("");
setApiStatus({ state: "online", label: "Mom is listening" });
const momResult = await askMom(clean, nextMessages, buildHouseState());
setApiStatus({
state: momResult.source === "local" ? "local" : "online",
label: momResult.source === "local" ? "Mom is using house memory" : "Mom is listening",
});
streamReply(momResult.reply, nextMessages, () => {
const artifact = requestedArtifactFromMessage(clean);
rememberArtifact(artifact, `${artifact.title} appeared on the kitchen table.`);
});
if (kind === "tea") {
setSceneNote("Mom recommends the Fireplace Ambience station.");
setRadioStation(1);
}
if (kind === "meal") {
setSceneNote("The kitchen table makes room for a warm meal.");
}
if (kind === "lesson") {
persistProgression({ ...progression, studyBookmarkUnlocked: true });
}
}
function openDrawer() {
const slip = drawerSlips[Math.floor(Math.random() * drawerSlips.length)];
const [title, ...bodyParts] = slip.split(":");
const item = {
icon: title.includes("Recipe") ? "π" : title.includes("Letter") ? "π" : title.includes("Study") ? "ποΈ" : "π",
title: title.trim(),
body: bodyParts.join(":").trim(),
};
const artifact = {
id: Date.now(),
type: "artifact",
kind: "drawer",
icon: item.icon,
tableIcon: item.icon,
title: item.title,
tableTitle: item.title,
description: item.body,
tableLine: item.body,
source: "Found in Mom's drawer.",
date: new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }),
};
setDrawerItem(item);
rememberArtifact(artifact, `${item.title} found in Mom's drawer.`);
}
function openRecipeBook() {
const artifact = makeArtifact("lesson", "Opened the recipe book on the kitchen counter.", {
icon: "π",
title: "Counter Recipe Book",
description: "A page marked: study gently, revise twice, ask one clear question.",
});
rememberArtifact(artifact, "The recipe book opened to a useful page.");
}
function pourTea() {
const artifact = makeArtifact("tea", "Poured tea before saying anything.", {
icon: "π΅",
title: "Steaming Tea Note",
description: "The cup says: you can pause before you solve everything.",
});
rememberArtifact(artifact, "The tea cup left a warm note on the table.");
}
function clickCat() {
playMeow();
const item = catCollectibles[Math.floor(Math.random() * catCollectibles.length)];
const collectible = {
...item,
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
date: new Date().toLocaleDateString(undefined, { month: "short", day: "numeric" }),
};
const next = [...collectibles, collectible].slice(-24);
persistCollectibles(next);
setCatState("looking");
setCatBubble(`${item.icon} ${item.title}`);
setSceneNote(`The cat brought you: ${item.title}. Click it in the shelf or attic to use it.`);
window.setTimeout(() => setCatBubble(""), 2200);
}
async function startRadio(stationIndex = radioStation) {
const resolvedStationIndex = Number.isInteger(stationIndex) ? stationIndex : radioStation;
const station = radioStations[resolvedStationIndex] || radioStations[0];
setRadioPlaying(true);
setSceneNote("");
try {
await startAmbientStation(station.id, radioVolume);
} catch (error) {
setRadioPlaying(false);
setSceneNote(`Browser blocked the radio for a moment (${error?.name || "audio policy"}). Click Play again after entering Home.`);
}
}
function stopRadio() {
stopAmbientRadio();
setRadioPlaying(false);
}
function changeStation(index) {
setRadioStation(index);
if (radioPlaying) {
window.setTimeout(() => startRadio(index), 40);
}
}
async function saveBackyardNote(event) {
event.preventDefault();
const clean = reflection.trim();
if (!clean) return;
const plant = normalizePlant({
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
...plantFromReflection(clean),
text: clean,
stage: 0,
date: new Date().toLocaleDateString(undefined, { month: "short", day: "numeric" }),
history: [`Seed: ${clean}`],
});
const nextPlants = [...plants, plant].slice(-24);
persistPlants(nextPlants);
setReflection("");
const note = { title: plant.title, body: clean };
const stored = readStorage(STORAGE_KEYS.notes, []);
const nextStored = [...stored, note].slice(-10);
writeStorage(STORAGE_KEYS.notes, nextStored);
setNotes([...fallbackNotes, ...nextStored].slice(-12));
setSelectedNote(note);
setSceneNote(`${plant.title} was planted as a seed.`);
try {
await fetch("/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(note),
});
} catch {
// Local storage keeps the garden alive during frontend-only previews.
}
}
async function saveHallwayNote(event) {
event.preventDefault();
const clean = noteDraft.trim();
if (!clean) return;
const note = {
title: clean.split(/\s+/).slice(0, 3).join(" ") || "Visitor",
body: clean,
};
const stored = readStorage(STORAGE_KEYS.notes, []);
const nextStored = [...stored, note].slice(-10);
writeStorage(STORAGE_KEYS.notes, nextStored);
const nextNotes = [...fallbackNotes, ...nextStored].slice(-12);
setNotes(nextNotes);
setSelectedNote(note);
setNoteDraft("");
setSceneNote("Your note joined the hallway.");
try {
const response = await fetch("/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(note),
});
if (response.ok) {
const data = await response.json();
if (data.notes?.length) setNotes([...fallbackNotes, ...data.notes].slice(-12));
}
} catch {
// Local storage keeps notes working when the Python backend is not running.
}
}
const station = radioStations[radioStation];
if (scene === "memory") {
console.log("Home hallway artifacts", artifacts);
}
return (
{scene === "memory" && }
travel("arrival", "You step back onto the wet porch.")}>Porch
travel("kitchen", "The kitchen gathers around you.")}>Kitchen
travel("memory", "The hallway remembers what happened here.")}>Hallway
travel("backyard", "The garden air is cool and kind.")}>Backyard
{scene === "arrival" && (
rain on the roof Β· light in the windows
π‘ Home
A place that is always glad you came back.
Enter Home
)}
{scene === "kitchen" && (
{chatOpen ? (
) : (
setChatOpen(true)}>
π©
Talk to Mom
)}
π
Recipe Book
π΅
Tea Cup
π
Mom's Drawer
Kitchen Table
{tableArtifacts.length ? "Artifacts from your day" : "Waiting for its first memory"}
{tableArtifacts.length ? tableArtifacts.map((artifact, index) => (
{
setSelectedMemory(artifact);
travel("memory", "The hallway opens to the artifact you made.");
}}
>
{artifact.tableIcon}
{artifact.tableTitle}
)) :
Say βI failed my examβ, βI got an internshipβ, or βExplain recursion.β
}
{drawerItem && (
setDrawerItem(null)}>Close
{drawerItem.icon}
{drawerItem.title}
{drawerItem.body}
)}
(radioPlaying ? stopRadio() : startRadio())}>{radioPlaying ? "Pause" : "Play"}
setRadioVolume(Number(event.target.value))} />
{radioStations.map((item, index) => (
changeStation(index)}>
{item.icon} {item.name}
))}
π
{collectibles.length > 0 && {collectibles.length} }
{catBubble && {catBubble}
}
Cat's Finds
{collectibles.slice(-6).map((item) => (
inspectCollectible(item)}>
{item.icon}
{item.title}
))}
{!collectibles.length && Click the cat. It brings discoveries. }
travel("memory", "The hallway opens to the house's memory.")}>
House Memory Hallway β
)}
{scene === "memory" && (
{(artifacts.length ? artifacts.slice(-12).reverse() : [{
id: "empty",
icon: "π",
title: "The first memory is waiting",
date: "Today",
description: "The wall is waiting for your first memory.",
}]).map((artifact, index) => (
inspectMemory(artifact)}
>
{artifact.icon || "π"}
{artifact.title || "House Memory"}
{artifact.date || "Today"}
{artifact.description || "The house kept this memory."}
))}
Recent Visitor Notes
{notes.slice(-5).map((note, index) => (
setSelectedNote(note)}>
{note.title}
))}
{selectedNote.body}
setNoteDraft(event.target.value)} placeholder="Leave a note for the hallway..." />
Pin note
Cat Discoveries
{collectibles.length ? (
{collectibles.slice(-8).map((item) => (
inspectCollectible(item, "hallway")}>
{item.icon} {item.title}
))}
) : Click the cat in any room to start the shelf.
}
Garden Progress
{plants.length ? (
{plants.slice(-6).map((plant) => (
tendPlant(plant.id)}>
{plant.icon} {plant.stageName || "Seed"} Β· {plant.title}
))}
) : Plant a reflection in the backyard.
}
Tidy duplicate memories
{selectedMemory && selectedMemory.id !== "empty" && (
setSelectedMemory(null)}>Close
{selectedMemory.icon || "π"}
{selectedMemory.relatedRoom || selectedMemory.kind || "House Memory"}
{selectedMemory.title || "This house remembers you."}
{selectedMemory.date || "Today"}
{selectedMemory.description || "Artifacts from your milestones, hard days, lessons, and discoveries will pin themselves here."}
{selectedMemory.source && "{selectedMemory.source}" }
{selectedMemory?.kind !== "collectible" && restoreArtifactToTable()}>Return to table }
{selectedMemory?.kind !== "collectible" && Ask Mom }
{selectedMemory?.kind === "meal" && unlockKitchenRecipe(selectedMemory.source || selectedMemory.description)}>Unlock recipe }
{selectedMemory?.kind !== "collectible" && Take down }
)}
travel("backyard", "The hallway door opens into the reflection garden.")}>
To the reflection garden β
)}
{scene === "backyard" && (
π
{collectibles.length > 0 && {collectibles.length} }
{benchOpen ? (
setBenchOpen(false)}>
Close
Reflection Bench
{reflectionQuestions[questionIndex]}
{plants.length ? plants.slice(-6).map((plant) => (
tendPlant(plant.id)} title={plant.text || plant.body}>
{plant.icon}
{plant.stageName}
)) : No plants yet. }
setReflection(event.target.value)} placeholder="Write a reflection. The garden will grow something from it." />
Plant this π±
) : (
setBenchOpen(true)}>
Open Reflection Bench π±
)}
{plants.slice(-6).map((plant, index) => (
tendPlant(plant.id)}
title={`${plant.stageName}: ${plant.body}`}
>
{plant.icon}
{plant.stageName}
))}
Garden Progress
{plants.length} plants grown
{collectibles.length} cat finds
{artifacts.length} hallway memories
travel("kitchen", "You return to the warm kitchen.")}>
Back to Mom
)}
{scene === "attic" && (
Attic
{unlocks.attic ? "The old trunk is open." : "The attic is still locked."}
{unlocks.attic
? "Your key, old letters, and repeated visits have made this room part of Home."
: "Find the Small Key from the cat or visit Home three times to open this room."}
{progression.trunkOpen ? "Unlocked Trunk" : "Locked Trunk"}
{progression.trunkOpen
? "The trunk has revealed the old letter. Inspect the letter to pin its memory downstairs."
: "Needs the Small Key. When it opens, it should change the hallway."}
{
const key = collectibles.find((item) => item.title === "Small Key");
if (key) inspectCollectible(key, "attic");
else setSceneNote("The trunk is locked. Ask the cat to look around for a Small Key.");
}}>
{progression.trunkOpen ? "Inspect trunk" : "Try the lock"}
Collectibles with consequences
{collectibles.map((item) => (
inspectCollectible(item, "attic")}>
{item.icon}
{item.title}
))}
{!collectibles.length && No discoveries yet. Click the cat when you see it. }
travel("kitchen", "You come down from the attic stairs.")}>Back to Mom
)}
{scene === "study" && (
Study Room
Mini Library
Lessons, papers, and quiet courage live here.
{["Algorithms", "Python", "Math", "Design", "Dreams", "Letters", "Notes", "Maps", "Stories", "Rest"].map((book, index) => (
setSceneNote(`${book}: Mom marked one page for later.`)}
>
{book}
))}
Home Notes
{selectedMemory?.title || "Choose a memory from the hallway."}
openRecipeBook()}>
Lesson Page
Open recipe book
askMomAboutMemory()}>
Ask Mom
Turn memory into a plan
setSceneNote("The laptop saved a small study plan: 25 minutes, tea, then one example.")}>
Study Plan
25 min focus
On the desk
{artifacts.some((item) => item.kind === "lesson") ? `${artifacts.filter((item) => item.kind === "lesson").length} study notes saved from Mom's teaching.` : "Ask Mom to explain recursion, operating systems, scheduling, or data structures."}
{artifacts.filter((item) => item.kind === "lesson").slice(-6).map((artifact) => (
{
setSelectedMemory(artifact);
setSceneNote(`${artifact.title}: ${artifact.source || artifact.description}`);
}}>
{artifact.icon}
{artifact.title}
))}
{!artifacts.some((item) => item.kind === "lesson") && No study notes yet. }
travel("kitchen", "You carry a page back to the kitchen.")}>
Back to Mom
)}
{scene === "greenhouse" && (
Greenhouse
{unlocks.greenhouse ? "The garden has moved indoors." : "The greenhouse is waiting."}
{unlocks.greenhouse
? "Your reflections have grown enough roots to live here."
: "Grow three backyard plants or let the cat bring a greenhouse discovery."}
Plant evolution timeline
{plants.slice(-8).map((plant) => (
setSceneNote(`${plant.title}: ${plant.history?.join(" β ") || plant.body}`)}>
{plant.icon}
{plant.stageName} Β· {plant.title}
))}
{!plants.length && No plants yet. Answer the backyard bench question to grow one. }
Oldest: {plants[0]?.title || "none yet"}
Newest: {plants.at(-1)?.title || "none yet"}
Completed trees: {plants.filter((plant) => plant.growth >= 3).length}
travel("backyard", "You step back into the reflection garden.")}>Grow something
)}
{sceneNote && {sceneNote}
}
);
}
function SceneVideo({ src, active, label }) {
return (
);
}
function MemoryBackdrop() {
return (
);
}
function StudyBackdrop() {
return (
);
}
const rootElement = document.getElementById("root");
globalThis.__homeRoot = globalThis.__homeRoot || createRoot(rootElement);
globalThis.__homeRoot.render( );