Skip to content

feat(aviation): city name search + multi-option sorted price results in command bar#2527

Merged
koala73 merged 6 commits intomainfrom
feat/aviation-cmd-city-names-multi-prices
Mar 29, 2026
Merged

feat(aviation): city name search + multi-option sorted price results in command bar#2527
koala73 merged 6 commits intomainfrom
feat/aviation-cmd-city-names-multi-prices

Conversation

@koala73
Copy link
Copy Markdown
Owner

@koala73 koala73 commented Mar 29, 2026

Summary

  • City name support: the command bar now accepts city and airport names in addition to IATA codes. ops Lisbon, ops Dubai, ops Abu Dhabi, price London Dubai all resolve correctly via MONITORED_AIRPORTS. Two-word city names (Abu Dhabi, New York, etc.) are handled.
  • Price results show up to 5 options, sorted nonstop-first: previously the command bar returned only the single cheapest flight — which was often a 1-stop itinerary. Results are now sorted nonstop first, then by price, and up to 5 rows are shown with per-row: airline+flight, stop label (green for nonstop), dep/arr times, duration, and price.
  • Date shown: the search date is displayed next to the route header so it's clear what day is being quoted.
  • Suggestions and placeholder updated with natural-language examples (ops Dubai, price London Dubai, flight EK3).
  • Error hint updated to match new natural language style.

Test plan

  • ops Lisbon resolves to LIS and returns ops data
  • ops Abu Dhabi resolves to AUH
  • price London Dubai resolves LHR/DXB and shows multiple results
  • price BEY DXB returns nonstop options first, with $-sorted fallback
  • price LHR DXB 2026-04-15 uses explicit date
  • Results show dep–arr times and duration per row
  • Date label visible in result header

…onstop-first

Parser: resolve city/airport names to IATA so "ops Lisbon", "price London Dubai",
"ops Abu Dhabi" all work. extractAirports handles 1-word and 2-word city names.

Price results: show up to 5 options sorted nonstop-first then by price, with
dep/arr times and duration per row. TravelPayouts fallback also shows top 5 sorted.
Date label shown next to route header so users know what day is being quoted.

Suggestions and placeholder updated with natural language examples.
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Mar 29, 2026 7:27pm

Request Review

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 29, 2026

Greptile Summary

This PR extends AviationCommandBar with two main capabilities: city/airport-name resolution via MONITORED_AIRPORTS (so commands like ops Dubai or price London Dubai work in addition to raw IATA codes), and a richer price-results view that shows up to 5 itineraries sorted nonstop-first and includes airline, stop label, dep/arr times, duration, and price.

Key changes:

  • New resolveIata + extractAirports helpers perform case-insensitive city-name and partial-name lookup against MONITORED_AIRPORTS, with greedy two-word matching for names like "Abu Dhabi" or "New York".
  • PRICE_WATCH execution now sorts Google Flights results by (stops ASC, price ASC) before taking the top 5 — correctly implemented in the GF path.
  • The TravelPayouts/demo fallback path slices before sorting (quotes.slice(0, 5).sort(...)), meaning nonstop options priced outside the cheapest 5 are discarded before the nonstop-first sort runs — this is the opposite of the intended behaviour and the opposite of what the GF path does.
  • Date label added to result header using new Date(date + 'T12:00:00') (avoids midnight-UTC off-by-one day in western timezones).
  • Suggestions and placeholder updated to reflect natural-language examples.

Confidence Score: 4/5

Safe to merge after fixing the sort/slice ordering in the TravelPayouts fallback path; the city-name resolution and GF-path changes are correct.

One P1 logic defect: the TravelPayouts fallback sorts after slicing, silently hiding nonstop options priced outside the top-5 by price. This directly contradicts the PR's stated nonstop-first guarantee for that code path. The two P2 items (pluralisation, unescaped time strings) are cosmetic/low-risk.

src/components/AviationCommandBar.ts — specifically the quotes.slice(0, 5).sort(...) chain in the TravelPayouts fallback block (line 179).

Important Files Changed

Filename Overview
src/components/AviationCommandBar.ts Adds city-name airport resolution and multi-option price results; the fallback TravelPayouts path sorts after slicing (slice then sort instead of sort then slice), defeating the nonstop-first guarantee for that path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User input] --> B[parseIntent]
    B --> C{OPS / STATUS?}
    B --> D{PRICE/PRICES?}
    B --> E{FLIGHT/FLT?}
    B --> F{NEWS/BRIEF?}
    C --> G[extractAirports words.slice 1]
    D --> H[filter keywords + dates\nextractAirports nonKeywords]
    G --> I[resolveIata per token\ncity match via MONITORED_AIRPORTS]
    H --> I
    I --> J[OPS Intent]
    I --> K[PRICE_WATCH Intent]
    K --> L[executeIntent]
    L --> M{fetchGoogleFlights}
    M -- flights found --> N[sort by stops then price\nslice 0..5\nrender rows ✓]
    M -- no flights --> O[fetchFlightPrices fallback]
    O --> P[slice 0..5\nthen sort ⚠️ wrong order]
    P --> Q[render rows]
Loading

Reviews (1): Last reviewed commit: "feat(aviation): city name search, multi-..." | Re-trigger Greptile

Comment on lines +179 to +186
const rows = quotes.slice(0, 5).sort((a, b) => a.stops !== b.stops ? a.stops - b.stops : a.priceAmount - b.priceAmount).map(q => {
const stopColor = q.stops === 0 ? '#22c55e' : '#9ca3af';
const stopLabel = q.stops === 0 ? 'nonstop' : `${q.stops} stop`;
return `<div class="cmd-row" style="padding:5px 0;border-bottom:1px solid rgba(255,255,255,.05)">
<div style="flex:1">${escapeHtml(q.carrierName || q.carrierIata)}<span style="color:${stopColor};font-size:11px;margin-left:6px">${stopLabel}</span></div>
<div style="color:#60a5fa;font-weight:600">$${Math.round(q.priceAmount)}</div>
</div>`;
}).join('');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sort applied after slice in fallback path

.slice(0, 5) is called before .sort(...) in the TravelPayouts fallback path. The API returns quotes sorted cheapest-first, so you're first picking the 5 cheapest fares and then re-ordering them within that subset. This means a nonstop option priced outside the cheapest 5 is silently discarded before the nonstop-first sort ever runs — directly undermining the stated feature goal ("sorted nonstop-first").

The Google Flights path above does it correctly (sort → slice). The same pattern should be used here:

Suggested change
const rows = quotes.slice(0, 5).sort((a, b) => a.stops !== b.stops ? a.stops - b.stops : a.priceAmount - b.priceAmount).map(q => {
const stopColor = q.stops === 0 ? '#22c55e' : '#9ca3af';
const stopLabel = q.stops === 0 ? 'nonstop' : `${q.stops} stop`;
return `<div class="cmd-row" style="padding:5px 0;border-bottom:1px solid rgba(255,255,255,.05)">
<div style="flex:1">${escapeHtml(q.carrierName || q.carrierIata)}<span style="color:${stopColor};font-size:11px;margin-left:6px">${stopLabel}</span></div>
<div style="color:#60a5fa;font-weight:600">$${Math.round(q.priceAmount)}</div>
</div>`;
}).join('');
const rows = quotes.sort((a, b) => a.stops !== b.stops ? a.stops - b.stops : a.priceAmount - b.priceAmount).slice(0, 5).map(q => {

const depTime = leg?.departureDatetime?.slice(11, 16) ?? '';
const arrTime = f.legs[f.legs.length - 1]?.arrivalDatetime?.slice(11, 16) ?? '';
const stopColor = f.stops === 0 ? '#22c55e' : '#9ca3af';
const stopLabel = f.stops === 0 ? 'nonstop' : `${f.stops} stop`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing pluralization for stop count

${f.stops} stop renders "2 stop", "3 stop" for multi-stop itineraries. The same issue exists in the TravelPayouts path at line 181.

Suggested change
const stopLabel = f.stops === 0 ? 'nonstop' : `${f.stops} stop`;
const stopLabel = f.stops === 0 ? 'nonstop' : `${f.stops} stop${f.stops > 1 ? 's' : ''}`;

And line 181:

const stopLabel = q.stops === 0 ? 'nonstop' : `${q.stops} stop${q.stops > 1 ? 's' : ''}`;

Comment on lines +158 to +159
const depTime = leg?.departureDatetime?.slice(11, 16) ?? '';
const arrTime = f.legs[f.legs.length - 1]?.arrivalDatetime?.slice(11, 16) ?? '';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unescaped time strings rendered into HTML

depTime and arrTime are sliced directly from API-provided datetime strings and inserted into the HTML template without escapeHtml. In practice the slice always yields HH:MM (digits + colon), so the risk is very low, but it's inconsistent with how other API-sourced values are handled throughout this file.

Consider wrapping both in escapeHtml(...) when they are interpolated into the template:

${escapeHtml(depTime)}${arrTime ? `–${escapeHtml(arrTime)}` : ''} · ${escapeHtml(fmtDur(f.durationMinutes))}

@koala73 koala73 merged commit ec81d8c into main Mar 29, 2026
6 of 7 checks passed
@koala73 koala73 deleted the feat/aviation-cmd-city-names-multi-prices branch March 29, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant