This report summarizes the implementation of the Montreal Travel Companion web application and explains how it satisfies the functional, usability, and LLM-related requirements of the course project. The app is a mobile-first React + TypeScript single-page application that uses a Leaflet map, a centralized state store (Zustand), OpenAI’s web-search capable models, and a Firebase Realtime Database–backed login system.
The application tracks two kinds of location:
location in
ContextState, initialized to downtown Montreal).
pinned location (set either by tapping on the map or
using GPS).
In MapPane, the ClickToPin component listens for map click
events via useMapEvents and updates the pinned coordinates; a
notification (“Pinned location set”) is also triggered. The bottom-right
floating “📍” button in geolocate() uses the browser’s
navigator.geolocation.getCurrentPosition API to set the pin to the user’s actual
GPS location and pushes a toast with the fixed coordinates.
In the main App component, the “active” context location is always
activeLoc = pinned ?? location, i.e., the pin overrides GPS until cleared; this
active location is passed into the recommender. The recommendation
engine (recommend()) computes distances from this location using a Haversine
helper distanceKm() and filters POIs within a configurable radius (3 km by
default). The final recommendations are then shown both in
the side “Top Picks Nearby” panel and as map markers.
Weather is detected via an LLM-powered helper fetchWeatherFromLLM(), which calls
OpenAI’s Responses API with the web_search tool to fetch current conditions for
the user’s coordinates. The model is instructed to respond with strict JSON including a short
condition string and temperatures in °C and °F. The function parses the
JSON, normalizes the condition, infers missing values (e.g., converting °F to °C if needed),
and maps it to the internal Weather enum via mapToWeatherEnum().
The App component calls fetchWeatherFromLLM() on mount and then
every 30 minutes in a useEffect, updating the global weather and
tempC fields in the store. If the call fails, a
toast “Weather refresh failed, using last known data” is shown to the user.
The recommendation pipeline enforces weather-aware behavior: outdoor POIs are filtered out in
weatherOK() whenever the weather is bad for being outside (rain, snow, or extreme
heat). Thus, on a rainy evening, the app will bias toward indoor
museums, cinemas, cafés, and restaurants instead of parks or viewpoints. The current weather
and temperature are also summarized in the mobile “Dynamic Island” top bar, showing an emoji
glyph (☀️, 🌧️, ❄️, etc.) plus the mapped condition and temperature.
Time is stored globally as an ISO timestamp (timeISO) in the Zustand store and is
initialized to the current datetime. The “Wizard-of-Oz” controls expose
a datetime-local input for the instructor account, allowing simulated time-of-day
testing. Because native datetime inputs expect local time strings, a helper
toLocalDateTimeInput() converts the stored ISO UTC time into the correct
YYYY-MM-DDTHH:mm format, and on change, the value is converted back to a canonical
ISO string.
Time directly affects recommendations in two ways:
isOpen() helper uses dayjs to check the current
weekday and current time against each POI’s opening hours table; closed venues are filtered
out.
dayOfWeek and formatted localTime,
which allows the model to understand whether it is breakfast, lunch, dinner, late night,
weekday vs. weekend, etc., when proposing additional activities.
User preferences are represented by a structured Preferences object in the global
store, including:
indoorOutdoor (indoor / outdoor / either)activityTypes (e.g., restaurant, park, museum, café, cinema, viewpoint, shopping)cuisines (smoked-meat, pizza, italian, poutine, burgers, asian, coffee, dessert)mealTimes (preferred breakfast, lunch, dinner times)
Preferences are stored in and restored from localStorage, so they persist between
sessions. The onboarding screen (OnboardPreferences) is
shown after login to encourage users to configure their preferences using the shared
Controls component; “Skip” and “Done” both set an onboarded flag and
advance to the main map.
The Controls panel lets users:
The recommend() function filters POIs by:
activityTypes
This ensures that adjusting preferences (e.g., switching from “parks” to “museums”, or from
“pizza” to “asian”) immediately changes the set of recommended POIs.
Preference changes also trigger a notification “Preferences updated” through the store’s
setPrefs method.
Notifications are implemented centrally in the store as a list of
{ id, msg } items. The pushNotify() method appends a new item and
automatically schedules its removal after a timeout. The main
App component renders a “toast stack” in the bottom-right corner, with the newest
notification at the bottom; clicking a toast dismisses it.
Recommendation updates trigger notifications in several ways:
fetchPOIsFromLLM(). When this
completes, the store updates both static POIs and dynamic AI suggestions and pushes the
message “Recommendations updated”.
runFreeformAsk(). After AI suggestions
are appended, the UI shows “AI answer ready” as a toast; if the LLM call fails, the app shows
“AI search failed”.
The interface has been designed with Nielsen’s 10 Usability Heuristics in mind. Key mappings are summarized below (0.5 points per heuristic).
Controls or via the “❌”
floating button, returning control to GPS.
index.html.
Login.
maxBounds, preventing users from
getting “lost” on the map.
index.html uses a light, minimal palette, rounded cards, and soft
shadows. On phones, the sidebar becomes a slide-in sheet and the map occupies the full
viewport.
The project uses OpenAI models in two main runtime features:
The module useLLM.tsx defines askLLMJSON() and
fetchPOIsFromLLM(). These functions:
tools: [{ type: "web_search" }] and a prompt
that forces the model to return strict JSON with summary, an array of
items, and optional sources.
POI objects
(itemsToPOIs()), and return them along with the summary text.
MapPane uses fetchPOIsFromLLM() in two ways:
pois set and appended to dynamicPois, and the summary
text is shown in the recommendation panel.
userPrompt,
switches the panel into “AI only” mode, and appends fresh dynamic POIs.
Dynamic POIs are deduplicated by ID so that static and AI-suggested markers do not clash, and the recommendation panel also deduplicates AI results by name.
fetchWeatherFromLLM() similarly uses web_search to query a reliable
weather provider and returns structured JSON with condition, temperatures, and an optional
natural-language summary. This summary can be logged for debugging and is used to validate that
the model’s mapping makes sense.
Beyond runtime features, an LLM (ChatGPT) was used extensively as an assistive tool during development in the following ways:
App, Login, OnboardPreferences,
Controls, MapPane, and RecPanel, and defining a POI
schema with opening hours and tags.
Rec objects with human-readable reasons.
data.output[0].content[0].text, choices[0].message.content, etc.).
A fallback also strips Markdown code fences when necessary.
<input type="datetime-local"> simply sliced the
ISO string, which led to confusing off-by-timezone errors. With LLM help, a dedicated helper
toLocalDateTimeInput() was introduced to convert between ISO UTC and the local
datetime format expected by the browser, and to safely handle invalid values.
webkitSpeechRecognition), interim results, and TypeScript typing issues (using
any for the recognition instance). ChatGPT helped design the
toggleVoice() helper, the ensureRecognition() factory, and error
handling logic for cases where the browser does not support speech recognition.
Key difficulties and the role of LLMs in overcoming them include:
App.
The implemented Montreal Travel Companion meets the functional requirements by automatically detecting location, weather, and time; incorporating detailed user preferences; and updating recommendations with clear notifications. A combination of a rule-based recommender and LLM-powered discovery allows the system to provide both reliable, locally constrained suggestions and flexible, web-enhanced ideas. The interface follows the 10 usability heuristics through clear feedback, real-world mapping, strong user control, and a mobile-first, minimalist design. Throughout the project, large language models were used not only at runtime (for weather and POI search) but also as a development partner to refine architecture, fix bugs, and iterate on the user experience.