DEV Community

Harsh Patel
Harsh Patel

Posted on AI-assisted

MoodTrail: pick a place to go outside together, with a model that never leaves your phone

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

What I Built

My partner and I like hikes, trails and parks. What stops us is not the walk, it's the deciding. We stand in the living room, scroll for somewhere to go, disagree about how far, and by the time we agree it's getting dark.

MoodTrail takes that conversation down to about thirty seconds. Each of us taps three cards: energy (chill, moderate, push it), vibe (quiet, scenic, social) and time (1h, 2h, half day). It looks up named parks and nature reserves near us, checks today's weather and sunset, and shows three places that fit both of us. Each card has a one-sentence note from a small language model and an "Open in Maps" button. Then the phone goes in the pocket.

There is also an optional movie night box. Add a few titles you would both watch, and it picks one for when you're back, so the evening doesn't start with forty minutes of scrolling.

I haven't taken it on a real outing yet. That's the plan for this weekend, and I'll update this post with how it went.

Demo

Try it: harsh.zip/moodtrail

Use a browser with WebGPU, such as recent Chrome, Edge or Arc on desktop. The first tap starts a one-time model download of about 880 MB, with a progress label at the top of the page. Without WebGPU the picks still work, you just don't get the model's notes.

Code

GitHub logo harsh8398 / moodtrail

Pick a place to go outside together. On-device open model (WebLLM), no backend.

MoodTrail

Two people tap their moods, get three places to go, and put the phone away.

Live at harsh.zip/moodtrail. Built for the DEV Hacktoberfest "Touch Grass" challenge.

How it works

  1. Each of you picks an energy, vibe and time budget.
  2. The app finds named parks and nature reserves near you (OpenStreetMap, through Nominatim) and today's weather and sunset (Open-Meteo).
  3. A scoring function in src/shortlist.ts picks three places. The same moods and places always give the same picks.
  4. An open-weight model running in your browser (Qwen2.5-1.5B through WebLLM, on WebGPU) writes a one-sentence note for each pick, streamed into the card.
  5. Optional: add a movie night list and it picks one title for when you're back.

Your moods, location and watchlist stay in the browser. Only your coordinates go to the place and weather lookups. There is no backend and no API key.

Without WebGPU the picks still work…

It is a small Vite and TypeScript app with no backend and no API key. MIT licensed.

How I Built It

The open pieces

  • Model: Qwen2.5-1.5B-Instruct, an open-weight model, quantized to 4 bits.
  • Inference: WebLLM, which runs the model in the browser on WebGPU. Nothing is sent to a model server.
  • Data: OpenStreetMap places through Nominatim, and forecast and sunset from Open-Meteo. Both are open and need no key.

The part I got wrong first

My first version asked the model to choose the three best places and return JSON. On my laptop that took 78 to 100 seconds, and the answers changed between identical runs. Sometimes it returned two places instead of three, and once it wrote "Both people like nature reserves" as the reason for a park.

A 1.5B model is not a good judge of a list. So I split the job:

  1. Code picks the places. A scoring function in shortlist.ts weighs the shorter of your two time budgets, your average energy and both vibes. Same moods and same places always give the same three picks, and the cards appear within a second or two.
  2. The model writes the note. Your two moods are merged into a single plain plan ("a 2h outing at a relaxed pace with a scenic feel"), and the model writes one friendly sentence about why the place suits it. The sentence streams into the card as it is written, so you never wait on it.

Giving the model one merged plan instead of two moods also fixed its habit of garbling them ("both your 1h and 2h preferences").

Other things that bit me

  • WebLLM's JSON mode needs the schema passed as a string. Without one it fails inside the grammar matcher with a cryptic Cannot pass non-string to std::string.
  • The public Overpass servers returned 504 and 500 errors in every test, so I moved to Nominatim and cached results for six hours per location. The cost is that named hiking routes are gone, since only Overpass had them.
  • My first service worker did not cache the page on a first visit, so reloading offline showed a browser error. It now saves the page on install. I only caught this because I tested airplane mode on the production build.

Why Does Open Innovation Matter?

  • Your location and moods stay on the device. The model runs in your browser. The only things that leave are the coordinates sent to the place and weather lookups. No account, no server of mine in the middle.
  • It keeps working when the signal doesn't. After the first visit the app, the model and your last places are cached. In an offline test of the production build, the page reloaded, the model loaded from cache in about 1.5 seconds, and the cards and notes generated with no network.
  • It costs nothing to run. No API key, no per-request bill, no rate limit to hit on a Saturday morning. That matters for something you open every weekend.
  • I could change the model with one line. MODEL_ID in src/llm.ts takes any model in WebLLM's prebuilt list. I started with a 1.5B model because it fits on a phone, and a bigger one is a one-line change.

Where it was weaker than a closed API: a small local model is much worse at judgment than a large hosted one. The honest lesson of this project is that open and local works best when you give the model a small, well-defined job, and keep the decisions in plain code you can test.

I used AI assistance to write code and to draft this post, and I tested the app and edited the post myself.

Top comments (0)