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"])