Inspiration

I started cycling in LA last year, and the learning curve was steep — not because riding a bike is hard, but because LA streets are unforgiving if you don't know them. Every ride felt like a test I hadn't studied for. Which lanes are safe? When do I take the full lane? What do I do when a car cuts me off on a busy intersection?

I looked for tools that could actually help, and found nothing that spoke to new cyclists the way a knowledgeable friend would. Google Maps gives you a route but no context. YouTube videos are generic. What I wanted was someone riding next to me — someone who knew the city, knew my comfort level, and could talk me through it in real time.

That's what Wheeler is and the tagline says it all: your sous-chef that takes you from the kitchen to the streets.


What it does

Wheeler is an AI-powered cycling companion built for Los Angeles riders. It combines route planning, real-time voice navigation, and adaptive coaching into one experience.

  • Route planning — Enter a start and destination and get bike-friendly route options. Before you leave, you can add stops, use the "suggest a stop" feature to find coffee shops, water, or rest spots along your actual route, and preview every turn on a full Google Maps navigation view.

  • Pre-ride check-in — Before each ride, Wheeler asks how confident you feel, what makes you nervous about this route, and what you're excited about. These answers shape everything that follows.

  • Live voice companion (Jamie) — Once you start riding, an always-on AI mentor named Jamie greets you by name, references your check-in answers, and stays with you the whole ride. No tapping a button — just talk. Jamie proactively updates you as you move through neighborhoods, gives heads-ups before tricky turns, and responds to anything you say naturally.

  • Ride simulation — Practice a route before you ride it. Wheeler generates realistic LA scenarios based on your route and nervousness so you can build mental muscle memory without being on the road.

  • Learn & practice — Lessons and quizzes covering LA bike laws, the door zone, intersection rules, and night riding — with badge tracking as you complete them.

  • Ride history dashboard — Completed rides are logged with distance, duration, and places visited. A weekly graph tracks miles ridden, new places explored, and your confidence trend over time.


How we built it

Wheeler is a React + Vite single-page app. The full stack in plain terms:

Mapping and navigation We use the Google Maps JavaScript API with four libraries loaded simultaneously: Directions (route calculation), Places (stop suggestions and address autocomplete), Geometry (distance math for off-route detection and path sampling), and the Geocoder (reverse-geocoding live GPS into street names and neighborhoods).

The stop suggestion feature was particularly interesting to build. Rather than searching at a single point, we sample up to 6 evenly-spaced positions along the decoded route polyline and run a PlacesService.textSearch at each — then deduplicate by place_id and sort results by their true distance to the route path using the Haversine formula:

$$ d = 2R \cdot \arctan\left(\sqrt{\frac{\sin^2!\left(\frac{\Delta\phi}{2}\right) + \cos\phi_1 \cos\phi_2 \sin^2!\left(\frac{\Delta\lambda}{2}\right)}{1 - \sin^2!\left(\frac{\Delta\phi}{2}\right) - \cos\phi_1 \cos\phi_2 \sin^2!\left(\frac{\Delta\lambda}{2}\right)}}\right) $$

This ensures results are actually on your path — not just near your destination.

AI (Claude) We call the Claude API directly from the client for four distinct tasks: rider profile generation, Jamie's conversational responses, simulation scenario generation, and intent detection (deciding whether "find me coffee" is a place search or a conversation).

Jamie's system prompt is rebuilt on every turn with live context — current street, neighborhood, next turn instruction, pre-ride check-in answers, and the rider's full ride history — so responses feel specific to the moment, not generic.

Voice Everything runs on the browser-native Web Speech API. SpeechRecognition with auto-restart handles always-on listening. SpeechSynthesis handles playback. The key engineering challenge: the microphone must pause while the speaker is active, then resume exactly 500ms after speech ends — otherwise the assistant picks up its own voice as input. We built a custom useVoice hook that manages this pause/resume cycle around a shouldResume ref, ensuring no feedback loop regardless of how fast responses arrive.

Data No backend — everything lives in localStorage. Ride history, profile, badges, and active route are all persisted locally, keeping the architecture simple and the app fast.


Challenges we ran into

The Google Maps API has sharp edges. Getting the Places API to return results was harder than expected. nearbySearch with a keyword was unreliable — results were sparse or irrelevant. We switched to textSearch, increased the radius, moved to sequential requests with delays to avoid rate limits, and switched from decoding the overview polyline (which required the Geometry library to be loaded at the right time) to sampling directly from DirectionsRoute.legs[].steps[].start_location objects — which are always available and don't depend on any additional library. That last change was the fix.

Off-route detection had its own complexity. Checking distance from a single GPS point to a single step endpoint was too noisy. We ended up checking a sliding window of ±4 steps around the current position, taking the minimum distance to any start or end location in that window, and only triggering a reroute after 3 consecutive GPS readings above the 50m threshold — filtering out GPS jitter without adding lag.

Getting the voice assistant to feel consistent. The Web Speech API is not designed for always-on use. SpeechRecognition stops after silence, stops when the page loses focus, stops mid-word on slow connections, and on some browsers stops after about 60 seconds regardless. We solved this with an auto-restart loop: every onend and non-fatal onerror event schedules a restart with a short backoff — 300ms on clean stop, 800ms on error. The loop is gated by a shouldResume ref so it doesn't restart during TTS playback or while Claude is generating a response.

Making Jamie sound like a person rather than a chatbot required as much prompt engineering as it did code. Generic system prompts produced generic responses. The breakthrough was treating the system prompt as a live briefing — rebuilt on every call with the rider's exact street, next turn, check-in answers, and past ride history — and giving Jamie a clear personality directive: "Use real street names. Sound like a friend who bikes everywhere. No em-dashes."


Accomplishments that we're proud of

  • The always-on voice loop that seamlessly pauses for TTS and resumes after — it genuinely feels like talking to someone, not pressing a button
  • Stop suggestions that actually land on your route rather than near your destination
  • The pre-ride check-in feeding directly into both the simulation and Jamie's real-time responses — the system remembers what you said you were nervous about and addresses it proactively when you approach that part of the route
  • Building the entire thing without a backend — zero infrastructure, zero deployment cost, fully functional from a single npm run dev

What we learned

  • The gap between "working" and "feeling right" in voice UI is enormous. A response that takes 2.5 seconds feels fine on a desktop app and broken on a bike. Every latency optimization mattered.
  • Prompt engineering is real engineering. The difference between a helpful AI companion and a generic chatbot is almost entirely in how you structure context, not in which model you use.
  • The Google Maps API documentation describes what functions exist, not how they actually behave under real conditions. Reading the source issues and forums taught us more than the docs did.
  • localStorage is underrated for MVPs. No auth, no migrations, no infra — just build the product.

What's next for Wheeler

  • Visual identity — Lean into the bicycle + kitchen theme: a custom illustrated aesthetic that makes the brand feel as distinct as the name. Think chopping boards, bike chains, mise en place — cycling prep as cooking prep.

  • Backend + authentication — Move ride history, profiles, and progress to a real database so riders keep their data across devices and browsers. Supabase is the likely choice — Postgres, auth, and real-time in one.

  • Deployment — Put Wheeler on a real URL so anyone in LA can use it on their phone during an actual ride. This means a lightweight proxy server to keep API keys server-side and a PWA manifest so it installs like a native app.

  • Community layer — Riders sharing routes, tips for specific streets, and neighborhood guides. The data Wheeler collects across users would make the AI's local knowledge genuinely crowd-sourced.

Built With

Share this project:

Updates

Submission history