import streamlit as st
from datetime import date
from streamlit.components.v1 import html as st_html
from planmate import utils
from planmate.config import APP_TITLE, APP_TAGLINE, THEME, CURRENCY
from planmate.flights import search_flights
from planmate.weather import summarize_forecast_for_range, geocode_city
from planmate.attractions import get_attractions_and_stays
from planmate.llm import rank_accommodations, resolve_city_to_iata_ai
from planmate.itinerary import build_itinerary
st.set_page_config(page_title=APP_TITLE, page_icon="✈️", layout="wide")
# ---------- Modern Green Theme Styles ----------
st.markdown(
"""
""",
unsafe_allow_html=True,
)
# ---------- Header ----------
colH1, colH2 = st.columns([0.8, 0.2])
with colH1:
# Option 1: Replace with logo image
try:
st.image("planmate-logo.png", width=250) # Adjust width as needed
st.caption(APP_TAGLINE)
except:
# Fallback if logo not found
st.title(APP_TITLE)
st.caption(APP_TAGLINE)
# Option 2: Logo with text combination (uncomment to use instead)
# col_logo, col_text = st.columns([0.3, 0.7])
# with col_logo:
# try:
# st.image("logo.png", width=80)
# except:
# st.write("🚀") # Fallback emoji
# with col_text:
# st.title(APP_TITLE)
# st.caption(APP_TAGLINE)
with colH2:
if st.button("🔄 Start Over", use_container_width=True, help="Reset and start planning a new trip"):
for k in list(st.session_state.keys()):
del st.session_state[k]
st.rerun()
# ---------- Sidebar Inputs ----------
with st.sidebar:
st.markdown("### 🧳 Trip Planning")
with st.container():
src_city = st.text_input("🛫 From", value="Lahore", help="Enter your departure city")
dst_city = st.text_input("🛬 To", value="Dubai", help="Enter your destination city")
col1, col2 = st.columns(2)
with col1:
start_date = st.date_input("📅 Start Date", value=date.today())
with col2:
num_days = st.number_input("📆 Days", min_value=1, max_value=30, value=5, step=1)
col3, col4 = st.columns(2)
with col3:
adults = st.number_input("👥 Adults", min_value=1, max_value=9, value=1, step=1)
with col4:
non_stop = st.checkbox("✈️ Non-stop", value=False, help="Direct flights only")
with st.expander("⚙️ Advanced Options", expanded=False):
st.markdown("*Override AI-suggested airport codes*")
override_origin = st.text_input("Origin IATA Code", placeholder="e.g., LHE", help="Optional: Force specific origin airport")
override_dest = st.text_input("Destination IATA Code", placeholder="e.g., DXB", help="Optional: Force specific destination airport")
st.markdown("---")
go = st.button("🚀 Plan My Trip", type="primary", use_container_width=True)
# ---------- Helpers ----------
def set_planned(**kwargs):
"""Persist a 'planned' flag and any key/value to session."""
st.session_state.planned = True
for k, v in kwargs.items():
st.session_state[k] = v
def not_planned():
return not st.session_state.get("planned", False)
def scroll_to(target_id: str):
"""Smooth-scroll to a section with the given DOM id."""
st_html(
f"""
""",
height=0,
)
# flight panel visibility toggle
if "show_flights" not in st.session_state:
st.session_state.show_flights = False
# ---------- First click: plan & fetch flights ----------
if go:
# Country hints (optional) to improve AI IATA mapping
src_country = None
dst_country = None
try:
src_country = geocode_city(src_city).get("country")
except Exception:
pass
try:
dst_country = geocode_city(dst_city).get("country")
except Exception:
pass
# Resolve to IATA via AI, with optional overrides
try:
if override_origin.strip():
origin_code, origin_label, origin_kind = (
override_origin.strip().upper(),
f"(override) {override_origin.strip().upper()}",
"OVERRIDE",
)
else:
code, name, kind = resolve_city_to_iata_ai(src_city, src_country)
origin_code, origin_label, origin_kind = code, f"{name} ({code})", kind
if override_dest.strip():
dest_code, dest_label, dest_kind = (
override_dest.strip().upper(),
f"(override) {override_dest.strip().upper()}",
"OVERRIDE",
)
else:
code, name, kind = resolve_city_to_iata_ai(dst_city, dst_country)
dest_code, dest_label, dest_kind = code, f"{name} ({code})", kind
except Exception as e:
st.markdown(
f"""
Either city was not found or it does not have an airport: {str(e)}
""",
unsafe_allow_html=True
)
st.stop()
# Compute return date
ret_date = utils.compute_return_date(start_date, int(num_days))
# Fetch flights once and store
try:
with st.spinner("🔍 Searching for the best flights..."):
flights = search_flights(
origin_code,
dest_code,
utils.to_iso(start_date),
utils.to_iso(ret_date),
int(adults),
CURRENCY,
non_stop=non_stop,
)
except Exception as e:
st.markdown(
f"""
Flight search failed: {str(e)}
""",
unsafe_allow_html=True
)
st.stop()
set_planned(
src_city=src_city,
dst_city=dst_city,
start_date=start_date,
num_days=int(num_days),
adults=int(adults),
non_stop=bool(non_stop),
origin_code=origin_code,
origin_label=origin_label,
origin_kind=origin_kind,
dest_code=dest_code,
dest_label=dest_label,
dest_kind=dest_kind,
ret_date=ret_date,
flights=flights,
flight_idx=None,
show_flights=True,
weather=None,
pois=None,
selected_stay=None,
itinerary_md=None,
scroll_target=None,
)
# ---------- Render results if we have a planned trip ----------
if not_planned():
st.markdown(
"""
Fill in your trip details in the sidebar and click Plan My Trip to get started with your adventure!
""",
unsafe_allow_html=True
)
st.stop()
# Banner for resolved IATA
st.markdown(
f"""
Routes Resolved: {st.session_state.src_city} → {st.session_state.origin_label} [{st.session_state.origin_kind}] •
{st.session_state.dst_city} → {st.session_state.dest_label} [{st.session_state.dest_kind}]
""",
unsafe_allow_html=True
)
# ---------- Flights ----------
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.subheader("✈️ Flight Options")
fl = st.session_state.get("flights", {"flights": []})
# If user has selected a flight, show a compact summary
if st.session_state.get("flight_idx") is not None:
sel_offer = fl["flights"][st.session_state.flight_idx]
st.markdown(
f"""
🎯 Selected Flight — {sel_offer['price_label']}
""",
unsafe_allow_html=True
)
for leg_i, segs in enumerate(sel_offer["legs"]):
st.write(f"**✈️ Leg {leg_i+1}**")
for s in segs:
st.write(f"• {s['from']} → {s['to']} | {s['dep']} → {s['arr']} | {s['carrier']} {s['number']}")
if st.button("🔄 Change Flight", help="Select a different flight option"):
st.session_state.flight_idx = None
st.session_state.show_flights = True
st.session_state.weather = None
st.session_state.pois = None
st.session_state.itinerary_md = None
st.session_state.scroll_target = "section-flights"
st.rerun()
else:
if not fl.get("flights"):
st.markdown(
"""
No flight offers found for the selected dates/route. Try different dates or cities.
""",
unsafe_allow_html=True
)
else:
if st.session_state.get("show_flights", True):
for idx, offer in enumerate(fl["flights"][:10]):
st.markdown(
f"""
🛫 Option {idx+1} — {offer['price_label']}
""",
unsafe_allow_html=True
)
for leg_i, segs in enumerate(offer["legs"]):
st.write(f"**Leg {leg_i+1}**")
for s in segs:
st.write(f"• {s['from']} → {s['to']} | {s['dep']} → {s['arr']} | {s['carrier']} {s['number']}")
if st.button("✅ Select This Flight", key=f"select-flight-{idx}"):
st.session_state.flight_idx = idx
st.session_state.show_flights = False
st.session_state.weather = None
st.session_state.pois = None
st.session_state.itinerary_md = None
st.session_state.scroll_target = "section-weather"
st.rerun()
st.markdown("", unsafe_allow_html=True)
else:
if st.button("👀 Show Flight Options"):
st.session_state.show_flights = True
st.rerun()
# Gate further steps until a flight is selected
if st.session_state.get("flight_idx") is None:
st.markdown(
"""
Select a flight to proceed to weather, attractions, and itinerary planning.
""",
unsafe_allow_html=True
)
if st.session_state.get("scroll_target"):
scroll_to(st.session_state["scroll_target"])
st.session_state["scroll_target"] = None
st.stop()
# ---------- Weather ----------
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.subheader("🌦️ Weather Forecast")
if st.session_state.weather is None:
try:
with st.spinner("🌤️ Fetching weather forecast..."):
st.session_state.weather = summarize_forecast_for_range(
st.session_state.dst_city, st.session_state.start_date, st.session_state.num_days
)
except Exception as e:
st.markdown(
f"""
Weather data unavailable: {str(e)}
""",
unsafe_allow_html=True
)
st.session_state.weather = {"summary_text": "Weather data not available", "daily": []}
weather = st.session_state.weather
if "location" in weather:
st.markdown(
f"""
📍 {weather['location'].get('name')}, {weather['location'].get('country')}
""",
unsafe_allow_html=True
)
for r in weather.get("daily", []):
st.markdown(
f"""
{r['date']}: {r['desc']} • {r['temp_avg']}°C • Wind {r['wind']} m/s
""",
unsafe_allow_html=True
)
# ---------- Attractions & Stays ----------
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.subheader("📍 Attractions & Accommodations")
if st.session_state.pois is None:
try:
with st.spinner("🗺️ Discovering attractions and accommodations..."):
loc = geocode_city(st.session_state.dst_city)
st.session_state.pois = get_attractions_and_stays(lat=loc["lat"], lon=loc["lon"], radius=8000)
except Exception as e:
st.markdown(
f"""
Attractions lookup failed: {str(e)}
""",
unsafe_allow_html=True
)
st.session_state.pois = {"attractions": [], "stays": []}
pois = st.session_state.pois
attractions = pois.get("attractions", [])[:30]
stays = pois.get("stays", [])[:30]
# Select attractions
st.markdown("#### 🎯 **Top Attractions**")
selected_attractions = []
for i, a in enumerate(attractions[:12]):
col1, col2 = st.columns([0.85, 0.15])
with col1:
st.markdown(
f"""
📌 {a['name']} ({a.get('kinds','')})
""",
unsafe_allow_html=True
)
with col2:
add = st.checkbox("Add", key=f"attr-{i}", help=f"Include {a['name']} in your itinerary")
if add:
selected_attractions.append(a)
if not selected_attractions:
st.markdown(
"""
💡 Select attractions you'd like to visit for a personalized itinerary.
""",
unsafe_allow_html=True
)
# Stays with LLM ranking
st.markdown("#### 🏨 **Accommodations**")
st.caption("*Powered by OpenTripMap - for reference only, no live booking*")
try:
ranked_stays = rank_accommodations(stays, prefs="central, well-reviewed, convenient")
except Exception:
ranked_stays = stays
stay_names = [s["name"] for s in ranked_stays[:15]]
chosen_stay_name = st.selectbox(
"Choose accommodation",
options=["(None selected)"] + stay_names,
help="Select a place to stay for your trip"
)
selected_stay = None if chosen_stay_name == "(None selected)" else next(
(s for s in ranked_stays if s["name"] == chosen_stay_name), None
)
st.session_state.selected_stay = selected_stay
# ---------- Review & Booking (Mock) ----------
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.subheader("🎫 Review & Booking")
st.markdown("#### **Flight Summary**")
sel_offer = fl["flights"][st.session_state.flight_idx]
st.markdown(
f"""
💰 Total Cost: {sel_offer['price_label']}
⚠️ This is a demo environment - no actual booking or payment processed
""",
unsafe_allow_html=True
)
col1, col2 = st.columns(2)
with col1:
passenger_name = st.text_input("👤 Full Name", value="Test User", help="Passenger name for booking")
with col2:
passenger_email = st.text_input("📧 Email", value="test@example.com", help="Contact email")
if st.button("🎫 Reserve Flight (Demo)", help="Simulate flight booking"):
import uuid
pnr = str(uuid.uuid4())[:8].upper()
st.markdown(
f"""
🎉 Flight reserved! Reference: {pnr}
""",
unsafe_allow_html=True
)
st.session_state.flight_pnr = pnr
if st.session_state.selected_stay:
st.markdown(f"#### **Hotel Selected:** {st.session_state.selected_stay['name']}")
if st.button("🏨 Reserve Hotel (Demo)", help="Simulate hotel booking"):
import uuid
hid = str(uuid.uuid4())[:10].upper()
st.markdown(
f"""
🏨 Hotel reserved! Reference: {hid}
""",
unsafe_allow_html=True
)
st.session_state.hotel_ref = hid
# ---------- Itinerary ----------
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.subheader("🗓️ AI Itinerary")
if st.button("🤖 Generate Itinerary", help="Create a personalized day-by-day itinerary using AI"):
with st.spinner("🧠 AI planning your perfect itinerary..."):
try:
st.session_state.itinerary_md = build_itinerary(
st.session_state.dst_city,
utils.to_iso(st.session_state.start_date),
int(st.session_state.num_days),
selected_attractions,
st.session_state.selected_stay,
weather.get("summary_text", ""),
)
# Optionally scroll to itinerary when generated
st.session_state.scroll_target = "section-itinerary"
except Exception as e:
st.markdown(
f"""
Itinerary generation failed: {str(e)}
""",
unsafe_allow_html=True
)
if st.session_state.get("itinerary_md"):
st.markdown(
"""
📋 Your Personalized Itinerary
""",
unsafe_allow_html=True
)
st.markdown(st.session_state.itinerary_md)
# ---------- Final: perform any pending scroll ----------
if st.session_state.get("scroll_target"):
scroll_to(st.session_state["scroll_target"])