This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
I did not want another app that talks about going outside. I wanted one that shuts up and starts a timer.
Sproutpatch is a local-first daily check-in. You type a few lines about your day. A Gemma model that you installed writes one short, warm reply. Then TypeScript looks at the real weather and daylight where you are, picks a walk length from your mood, and a dim page counts the minutes. When you press I'm back, you rate how you feel, and a sprout shows up in your history.
The model never decides the walk. It is not allowed to. Gemma may talk. Code decides everything that could hurt you if the model is wrong: safety, weather, and how long you should be outside.
It is for anyone who will write a note on their phone and then actually leave the room. It is not a therapist.
The loop is short on purpose:
risk_flag. The model can raise the alarm. It can never lower it.
I ran a real check-in on my own phone after I shipped. I wrote that I was tired. gemma3:1b on Ollama gave me one short sentence. The rules, not the model, set the walk: weather and daylight from Open-Meteo, length from mood. Then the timer went dim and told me to put the phone away. That is the whole product. Gemma talks. The screen is supposed to stop there.
Live: https://sahilwmi.github.io/sproutpatch/
On a phone: open that URL → Add to Home Screen. Location, weather, the timer, history, and music all work without a model. In Settings you can load Gemma 3 270M on the device (about 250 MB from litert-community/gemma-3-270m-it), or point at Ollama on the same Wi-Fi with npm run dev -- --host.
On a computer:
ollama pull gemma3:1b
npm install
npm run dev
The setup wizard lists your local models. Continue without one and the app still nudges you outside.
Look and sound sit at the top of Settings. Theme follows the clock. Meadow and Night Grass are generated piano. My music plays files you add, tap to play, saved only on that device.
A local-first daily check-in that nudges you outside. Bring your own Gemma (or any open model).
You type a few lines about your day. A Gemma model that you installed (through Ollama) writes a short, warm reply. Plain TypeScript looks at the real weather and daylight where you are and suggests one concrete thing to do outside. A dim, nearly empty timer page counts the walk. When you press "I'm back" you rate how you feel, and a sprout appears in your history for every walk you finish.
The screen is meant to be the shortest part of the experience. Nudges are concrete (real weather, real sunset time, never a model's guess). And Sproutpatch is not a therapist.
ollama pull gemma3:1b
npm install
npm run dev
Open the URL Vite prints (usually http://localhost:5173), and pick your model in the setup wizard. That's it.
MIT. New repo, first commit 12 Oct 2026 02:59 +0530 (still 11 Oct PDT). React 19, Vite, Tailwind 4, zod, idb, Vitest. No backend. Entries live in IndexedDB. Export JSON / delete everything is in History.
Open-source AI at the core is Gemma, two ways:
gemma3:1b. Vite proxies /ollama so localhost just works..task file once. It lives in OPFS and runs on the phone GPU through MediaPipe LLM Inference and WebGPU. The WASM is served from the site, not a CDN.
The hosted GitHub Pages copy runs without a model (keyword mood + template replies + the same weather rules). A public HTTPS page cannot reach Ollama on your PC; that is an origin allowlist, not a missing fetch.
Gemma is allowed to return exactly this:
{ "mood_score": -1, "themes": ["work"], "risk_flag": false, "reply": "That sounds like a long one." }
Everything after that is boring on purpose.
Safety is not a plugin. checkSafety() scans the raw note first. Tests prove a provider that returns risk_flag: false cannot suppress keyword risk mode, and that neither the model, the nudge, nor the weather lookup runs once risk mode starts. I do not trust a 1B model with crisis language. In testing, gemma3:1b also over-flagged ordinary bad work days. That is the cautious direction.
Validation sits in one function. Every provider — Ollama, OpenAI-compatible, on-device, or a file you drop in src/agents/ — goes through runCheckIn. It strips code fences, parses JSON, validates with zod, clamps mood to -3..3, cuts the reply to 300 characters, drops therapy-shaped sentences, retries once, then falls back to templates.
Walk length is a ladder, not a vibe. Mood sets minutes: 10 / 15 / 30 / 45 / 60. Rain is always five. Heat uses a shorter ladder that still follows mood. Night used to cap at 15. I hated that. If I am in a good mood I will walk an hour after dark, on a lit street. So night no longer shortens the walk. It only changes the where.
export function minutesForRule(rule: NudgeRuleId, moodScore: number): number {
if (rule === 'rain') return RAIN_MINUTES;
if (rule === 'heat') return shortWalkMinutesForMood(moodScore);
return walkMinutesForMood(moodScore); // night included
}
The 270M IT model has no system role, so the prompt folds system text into the first user turn.
A closed chat API would have been easier. It would also have been the wrong tool.
The words never leave the machine. A daily check-in is as personal as text gets. With Gemma on Ollama or in OPFS there is no account, no vendor log, no per-token bill. The only network calls are your local model (or none) and Open-Meteo.
I can distrust the model in code. A hosted API lets you ask it not to invent weather or play therapist. Open weights plus a local runtime let me refuse to give it those jobs. Weather is a forecast. Walk length is a function. Safety is a regex that runs first. Gemma writes a sentence. That split is the product.
It still works when Gemma is wrong, missing, or too small. 270M on a phone is short and sometimes misses the JSON shape. 1B over-flags a rough day. The fallback is not an error screen. You still get a concrete nudge from real weather.
You can swap it. Any Ollama tag. Any OpenAI-compatible server (LM Studio, llama.cpp, vLLM). Or drop a Provider / NudgeStrategy in src/agents/ and it shows up in Settings.
It costs nothing to run, so it has no reason to keep you scrolling. The live site is static GitHub Pages. No API keys in the frontend.