ai-travel-companion-platform, the AI trip planner that refuses to trust its own output
Gemini drafts the itinerary, OpenStreetMap grounds it in reality, and the app lets you regenerate one day or one activity without blowing up the whole trip.
- The repo’s real innovation is not trip generation, but trust management: AI proposes the itinerary, then geocoding checks whether the places are real.
- Its best product decision is surgical regeneration, which lets a user replace one day or one activity instead of restarting the whole plan.
- The backend is tuned for messy model output, with JSON cleaning, concurrent location enrichment, and update logic that accepts flexible AI-shaped data.
- The frontend’s job is to absorb latency and keep the editing loop responsive while the backend does the slow work.
AI travel planning is easy to demo and hard to trust. This repo treats that as the main problem, not a side effect. Gemini writes the first draft, but OpenStreetMap and Nominatim decide whether the draft deserves to survive.
The app does not stop at a pretty itinerary
Most travel bots are good at producing something that reads like confidence. They are worse at making sure the places exist, the order makes sense, and one bad activity does not force a full restart. This project is built around that gap.
The result is less a chatbot than a correction engine. It accepts that the model is fast, useful, and occasionally messy, then spends the engineering effort where users feel the difference: grounding, repair, and selective edits.
The real product is a trust pipeline
The backend flow is straightforward once you strip away the AI mystique. User preferences go into a Gemini prompt, Gemini returns itinerary JSON, the service strips markdown wrappers, parses the object, enriches each location through Nominatim, and persists the result in MongoDB. The important detail is not that each step exists. It is that the code expects the model to be imperfect.
const cleaned = rawResponse
.replace(/^```json\s*/i, '')
.replace(/```$/i, '')
.trim();
const itinerary = JSON.parse(cleaned);
const enriched = await Promise.all(
itinerary.days.flatMap(day =>
day.activities.map(activity => enrichLocation(activity.location))
)
);
That cleaning step is a tell. The app does not pretend the LLM will always emit pristine machine-ready output. It defends against markdown fences, parse failures, and brittle place strings, then keeps moving.
Why the app feels more editable than most AI planners
The product choice that stands out is granularity. Users can regenerate an entire day or just one activity. That sounds small, but it changes the whole mental model. The user is not starting over. They are editing.
| Approach | Strength | Weakness | What this repo does differently |
|---|---|---|---|
| Generic LLM travel chat | Fast suggestions and conversational tone | No grounding, no durable structure | Uses the model for draft generation, then verifies the output against real geography |
| Traditional itinerary planner | Good organization and predictable layout | Weak personalization and poor iterative editing | Lets the user regenerate only the broken part of the trip |
| This repo | Generates, validates, and repairs in one loop | Requires more backend logic | Treats AI as a draft engine, not the source of truth |
That difference matters because travel intent changes locally. A bad lunch stop should not destroy the rest of the day. This app behaves like an editor because that is the only way AI output stays usable after the first draft.
The backend chooses flexibility over schema purity
The itinerary lives in a flexible Mongoose field rather than a rigid nested schema. That is a deliberate tradeoff. AI-shaped data changes too much for a hard schema to feel comfortable, but once you accept flexibility, you have to tell MongoDB when deep nested edits happen.
trip.itinerary.days[dayIndex].activities[activityIndex] = updatedActivity;
trip.markModified('itinerary');
await trip.save();
That `markModified` call is the price of admission. It says the author chose practical AI integration over schema purity, then handled the consequences explicitly instead of hoping the ODM would infer them.
The performance trick hiding in plain sight
The geocoding step could easily become the slowest part of the pipeline if each place were resolved one by one. Instead, the code fans out lookups with `Promise.all`, then recombines them. That is a small implementation choice with an outsized effect on perceived speed.
const resolvedPlaces = await Promise.all(
places.flatMap(place => place.names.map(name => enrichLocation(name)))
);
This is the kind of detail that makes an AI app feel sharp instead of sticky. The model may be the dramatic part, but concurrency is what keeps the user from staring at a spinner while the system checks dozens of locations.
Frontend polish matters because the backend is slow by nature
The frontend is doing more than rendering results. React 19, TanStack Query, and auth bootstrapping give the app a resilient shell around expensive operations. Query caching and loading states absorb some of the mess that comes with AI latency and network calls.
| Layer | What it handles | Why it matters |
|---|---|---|
| React 19 + Vite | Fast rendering and component state | Keeps the interface lightweight while the backend works |
| TanStack Query | Caching and async server state | Prevents every edit from feeling like a cold start |
| AuthContext bootstrap | JWT validation on mount | Makes the app feel ready before the user begins planning |
| Framer Motion and Leaflet | Motion and map interaction | Adds clarity without turning the UI into a novelty |
The frontend is not trying to hide latency completely. It is trying to make latency legible and manageable, which is the right goal for a tool that depends on external APIs and model calls.
What this project is really competing with
The nearest competitors are not just other AI trip planners. They are every workflow that leaves the user to clean up after the model. Generic chat tools can suggest destinations. Traditional planners can organize bookings. Few systems do both generation and grounding while preserving editability.
| Category | Examples | What they optimize for | What they usually miss |
|---|---|---|---|
| AI travel chat | Generic LLM assistants | Speed and conversational breadth | Verified locations and stable structure |
| Travel organizers | TripIt-style tools | Email parsing and itinerary storage | Personalized draft generation |
| AI itinerary builders | Inspirock, Utrip | Prebuilt trip creation | Surgical repair of a single day or activity |
| This repo | ai-travel-companion-platform | Draft, verify, and edit in one loop | It accepts that the first answer is not the final answer |
That last line is the point. The repo is not trying to make AI magical. It is trying to make AI accountable.
Why this repo feels production-minded
The codebase reads like something built for real failure modes. There is retry logic for flaky AI responses, defensive parsing, explicit state updates, security middleware, and clean service boundaries. None of that is flashy. All of it matters.
- The Gemini service retries transient failures instead of giving up on the first bad response.
- The backend cleans model output before parsing, which reduces brittle failures at the first boundary.
- The itinerary update path explicitly marks deep changes so nested edits actually persist.
- The frontend uses query caching and auth bootstrapping to keep the app responsive across slow calls.
This is the kind of repo that understands where AI systems fail in practice. Not in the demo. In the seams between generation, verification, storage, and repair.