""" 🌍 AI Travel Concierge - MCP 1st Birthday Hackathon Premium Chat Interface with Gradio 6 Best Practices Award-winning UX/UI with: - Modern messages format (OpenAI-style) - Smooth animations and transitions - Real-time agent status updates - Beautiful booking cards with links - Optional AI-generated travel poster """ import gradio as gr import asyncio import json import os import httpx from datetime import datetime, timedelta from dotenv import load_dotenv load_dotenv() # ============== CONFIGURATION ============== # Try multiple possible environment variable names for HF Spaces compatibility NEBIUS_API_KEY = os.getenv("OPENAI_API_KEY") or os.getenv("NEBIUS_API_KEY") or os.getenv("HF_TOKEN") NEBIUS_BASE_URL = os.getenv("OPENAI_BASE_URL") or os.getenv("NEBIUS_BASE_URL") or "https://api.studio.nebius.ai/v1/" # Fallback for HF Spaces if secrets aren't loading if not NEBIUS_API_KEY: # Temporary fallback - will be removed after testing NEBIUS_API_KEY = "v1.CmQKHHN0YXRpY2tleS1lMDBxZHo3Nzdzcnl5YWI2aGMSIXNlcnZpY2VhY2NvdW50LWUwMHFlNjY1a214YXJ5bTZnYTIMCIGJ88gGEKfcq5UBOgwIgIyLlAcQgO6m7AJAAloDZTAw.AAAAAAAAAAFpaIe7TQXIIO9jnQJWji15jqL-5Gts-kuJHPIqA8JxedCZSxHmDYTmU6QnsUXQHLlitYOwId8GjSGCdT1JHJ0K" print("⚠️ Using fallback API key") # Debug: Print all environment variables that might be relevant print("=" * 50) print("πŸ” DEBUG: Checking environment variables...") all_keys = [k for k in os.environ.keys()] api_related = [k for k in all_keys if any(x in k.upper() for x in ['API', 'KEY', 'TOKEN', 'OPENAI', 'NEBIUS'])] print(f"API-related env vars: {api_related}") # Validate API key on startup if NEBIUS_API_KEY: print(f"βœ“ API key found (length: {len(NEBIUS_API_KEY)})") print(f"βœ“ Base URL: {NEBIUS_BASE_URL}") else: print("❌ No API key available!") print("=" * 50) MODEL = "Qwen/Qwen3-235B-A22B-Instruct-2507" # MCP Server configurations MCP_SERVERS = { "flights": {"command": "python", "args": ["flights_server.py"]}, "hotels": {"command": "python", "args": ["hotels_server.py"]}, "activities": {"command": "python", "args": ["activities_server.py"]}, "dining": {"command": "python", "args": ["dining_server.py"]}, "transport": {"command": "python", "args": ["transport_server.py"]}, "weather": {"command": "python", "args": ["weather_server.py"]}, "poster": {"command": "python", "args": ["poster_server.py"]}, "recommendations": {"command": "python", "args": ["recommendations_server.py"]}, } # ============== MCP CLIENT ============== class MCPClient: def __init__(self): self.processes = {} self.tools_cache = {} async def connect_server(self, name: str, config: dict): import subprocess process = subprocess.Popen( [config["command"]] + config["args"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=os.path.dirname(os.path.abspath(__file__)) ) self.processes[name] = process init_request = { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "travel-concierge", "version": "1.0.0"} } } response = await self._send_request(name, init_request) init_notification = {"jsonrpc": "2.0", "method": "notifications/initialized"} process.stdin.write((json.dumps(init_notification) + "\n").encode()) process.stdin.flush() tools_request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} tools_response = await self._send_request(name, tools_request) if tools_response and "result" in tools_response: self.tools_cache[name] = tools_response["result"].get("tools", []) return response async def _send_request(self, server_name: str, request: dict) -> dict: process = self.processes.get(server_name) if not process: return {"error": f"Server {server_name} not connected"} try: process.stdin.write((json.dumps(request) + "\n").encode()) process.stdin.flush() response_line = process.stdout.readline().decode().strip() if response_line: return json.loads(response_line) except Exception as e: return {"error": str(e)} return {} async def call_tool(self, server_name: str, tool_name: str, arguments: dict) -> dict: request = { "jsonrpc": "2.0", "id": 100, "method": "tools/call", "params": {"name": tool_name, "arguments": arguments} } response = await self._send_request(server_name, request) if "result" in response: content = response["result"].get("content", []) if content and isinstance(content, list): return {"success": True, "data": content[0].get("text", "")} return {"success": False, "error": response.get("error", "Unknown error")} async def close_all(self): for name, process in self.processes.items(): try: process.terminate() process.wait(timeout=2) except: process.kill() self.processes.clear() # ============== AGENT CONFIG ============== AGENTS = { "weather": {"name": "Weather Agent", "icon": "🌀️", "color": "#4FC3F7", "tool": "get_weather_forecast"}, "flights": {"name": "Flight Agent", "icon": "✈️", "color": "#7C4DFF", "tool": "search_flights"}, "hotels": {"name": "Hotel Agent", "icon": "🏨", "color": "#FF7043", "tool": "search_hotels"}, "activities": {"name": "Activities Agent", "icon": "🎯", "color": "#66BB6A", "tool": "search_activities"}, "dining": {"name": "Dining Agent", "icon": "🍽️", "color": "#FFA726", "tool": "search_restaurants"}, "transport": {"name": "Transport Agent", "icon": "πŸš—", "color": "#42A5F5", "tool": "search_transport"}, } AGENT_SEQUENCE = ["weather", "flights", "hotels", "activities", "dining", "transport"] BOOKING_LINKS = { "flights": [ # Tested and working flight booking URLs - with return dates ("Google Flights", "https://www.google.com/travel/flights?q=flights%20from%20{origin_encoded}%20to%20{dest_encoded}%20{date}%20to%20{checkout}", "#4285F4"), ("Skyscanner", "https://www.skyscanner.com/transport/flights/{origin_lower}/{dest_lower}/{date}/{checkout}/?adults={travelers}&cabinclass=economy", "#1DB4A4"), ("Kayak", "https://www.kayak.com/flights/{origin}-{dest}/{date}/{checkout}/{travelers}adults?sort=bestflight_a", "#FF690F"), ("Momondo", "https://www.momondo.com/flight-search/{origin}-{dest}/{date}/{checkout}/{travelers}adults", "#E91E63"), ], "hotels": [ ("Booking.com", "https://www.booking.com/searchresults.html?ss={dest_encoded}&checkin={checkin}&checkout={checkout}&group_adults={travelers}&no_rooms=1", "#003580"), ("Hotels.com", "https://www.hotels.com/Hotel-Search?destination={dest_encoded}&startDate={checkin}&endDate={checkout}&rooms=1&adults={travelers}", "#D32F2F"), ("Airbnb", "https://www.airbnb.com/s/{dest_encoded}/homes?checkin={checkin}&checkout={checkout}&adults={travelers}", "#FF5A5F"), ("Agoda", "https://www.agoda.com/search?city={dest_encoded}&checkIn={checkin}&checkOut={checkout}&rooms=1&adults={travelers}", "#5E2CA5"), ], "activities": [ ("Viator", "https://www.viator.com/searchResults/all?text={dest_encoded}", "#1A1A1A"), ("GetYourGuide", "https://www.getyourguide.com/s/?q={dest_encoded}&date_from={checkin}&date_to={checkout}", "#FF5533"), ("TripAdvisor", "https://www.tripadvisor.com/Search?q={dest_encoded}%20things%20to%20do", "#00AF87"), ("Klook", "https://www.klook.com/search/?keyword={dest_encoded}", "#FF5722"), ], "dining": [ ("TripAdvisor", "https://www.tripadvisor.com/Search?q={dest_encoded}%20restaurants", "#00AF87"), ("Yelp", "https://www.yelp.com/search?find_desc=restaurants&find_loc={dest_encoded}", "#D32323"), ("OpenTable", "https://www.opentable.com/s?term={dest_encoded}", "#DA3743"), ("TheFork", "https://www.thefork.com/search/?cityId={dest_encoded}", "#00665E"), ], "transport": [ ("Rome2Rio", "https://www.rome2rio.com/s/{origin_encoded}/{dest_encoded}", "#00BCD4"), ("Uber", "https://m.uber.com/looking", "#000000"), ("GetTransfer", "https://gettransfer.com/en?utm_source=widget&from={dest_encoded}%20Airport&to={dest_encoded}%20City%20Center", "#4CAF50"), ("Bolt", "https://bolt.eu/", "#34D186"), ], } # ============== HELPER FUNCTIONS ============== def make_booking_links_html(agent_key: str, trip_data: dict) -> str: """Generate beautiful booking link buttons in HTML""" links = BOOKING_LINKS.get(agent_key, []) if not links: return "" # Get trip data destination = trip_data.get("destination", "Paris") origin = trip_data.get("origin", "New York") start_date = trip_data.get("start_date", "") end_date = trip_data.get("end_date", "") travelers = trip_data.get("travelers", 2) # Calculate nights nights = 7 try: from datetime import datetime d1 = datetime.strptime(start_date, "%Y-%m-%d") d2 = datetime.strptime(end_date, "%Y-%m-%d") nights = (d2 - d1).days except: pass # Clean destination and origin (remove country suffix like ", France") dest_clean = destination.split(",")[0].strip() if destination else "Paris" origin_clean = origin.split(",")[0].strip() if origin else "New York" # URL encoding formats import urllib.parse dest_encoded = urllib.parse.quote(dest_clean) # URL encoded: Dubai origin_encoded = urllib.parse.quote(origin_clean) # URL encoded: New%20York dest_lower = dest_clean.lower().replace(" ", "-") # lowercase with dashes: dubai origin_lower = origin_clean.lower().replace(" ", "-") # lowercase with dashes: new-york buttons = [] for name, url_template, color in links: try: url = url_template.format( dest=dest_clean, origin=origin_clean, dest_encoded=dest_encoded, origin_encoded=origin_encoded, dest_lower=dest_lower, origin_lower=origin_lower, date=start_date, checkin=start_date, checkout=end_date, travelers=travelers, nights=nights ) buttons.append(f'{name}') except Exception as e: pass if not buttons: return "" return f'''
πŸ”— Book Now:
{"".join(buttons)}
''' def make_booking_links_markdown(agent_key: str, trip_data: dict) -> str: """Generate booking links in Markdown format for chat messages""" links = BOOKING_LINKS.get(agent_key, []) if not links: return "" # Get trip data destination = trip_data.get("destination", "Paris") origin = trip_data.get("origin", "New York") start_date = trip_data.get("start_date", "") end_date = trip_data.get("end_date", "") travelers = trip_data.get("travelers", 2) # Calculate nights nights = 7 try: from datetime import datetime d1 = datetime.strptime(start_date, "%Y-%m-%d") d2 = datetime.strptime(end_date, "%Y-%m-%d") nights = (d2 - d1).days except: pass # Clean destination and origin dest_clean = destination.split(",")[0].strip() if destination else "Paris" origin_clean = origin.split(",")[0].strip() if origin else "New York" # URL encoding formats import urllib.parse dest_encoded = urllib.parse.quote(dest_clean) origin_encoded = urllib.parse.quote(origin_clean) dest_lower = dest_clean.lower().replace(" ", "-") origin_lower = origin_clean.lower().replace(" ", "-") link_strs = [] for name, url_template, color in links: try: url = url_template.format( dest=dest_clean, origin=origin_clean, dest_encoded=dest_encoded, origin_encoded=origin_encoded, dest_lower=dest_lower, origin_lower=origin_lower, date=start_date, checkin=start_date, checkout=end_date, travelers=travelers, nights=nights ) link_strs.append(f"[{name}]({url})") except: pass if not link_strs: return "" return f"\n\nπŸ”— **Book Now:** {' | '.join(link_strs)}" async def run_agent(mcp_client: MCPClient, agent_key: str, trip_data: dict) -> str: """Run an agent and return results""" origin = trip_data.get("origin", "") destination = trip_data.get("destination", "") start_date = trip_data.get("start_date", "") end_date = trip_data.get("end_date", "") travelers = trip_data.get("travelers", 1) budget = trip_data.get("budget", "moderate") interests = trip_data.get("interests", []) # Map interests to activity types interest_map = {"culture": "history", "food": "food", "art": "art", "adventure": "adventure", "nature": "adventure"} interest_type = "general" for i in interests: if i.lower() in interest_map: interest_type = interest_map[i.lower()] break try: if agent_key == "weather": # Tool: get_forecast(city, dates) resp = await mcp_client.call_tool("weather", "get_forecast", { "city": destination, "dates": f"{start_date} to {end_date}" }) elif agent_key == "flights": # Tool: search_flights(origin, destination, date, passengers, return_date) resp = await mcp_client.call_tool("flights", "search_flights", { "origin": origin, "destination": destination, "date": start_date, "passengers": travelers, "return_date": end_date }) elif agent_key == "hotels": # Tool: search_hotels(location, check_in, check_out, stars) stars = 5 if budget == "luxury" else 4 if budget == "moderate" else 3 resp = await mcp_client.call_tool("hotels", "search_hotels", { "location": destination, "check_in": start_date, "check_out": end_date, "stars": stars }) elif agent_key == "activities": # Tool: search_activities(city, interest) resp = await mcp_client.call_tool("activities", "search_activities", { "city": destination, "interest": interest_type }) elif agent_key == "dining": # Tool: find_restaurants(city, cuisine, buffet) resp = await mcp_client.call_tool("dining", "find_restaurants", { "city": destination, "cuisine": "local", "buffet": False }) elif agent_key == "transport": # Tool: book_transfer(pickup, dropoff, passengers) resp = await mcp_client.call_tool("transport", "book_transfer", { "pickup": f"{destination} Airport", "dropoff": f"{destination} City Center", "passengers": travelers }) else: return "Agent not found." return resp.get("data", "No data") if resp.get("success") else f"Error: {resp.get('error', 'Unknown')}" except Exception as e: return f"Error: {str(e)}" def generate_final_report_html(trip_data: dict, agent_results: dict) -> str: """Generate beautiful final report with all booking links""" dest = trip_data.get("destination", "Destination") origin = trip_data.get("origin", "Origin") start = trip_data.get("start_date", "") end = trip_data.get("end_date", "") travelers = trip_data.get("travelers", 2) budget = trip_data.get("budget", "moderate").title() dest_url = dest.replace(" ", "+") o = origin[:3].upper() if len(origin) >= 3 else origin.upper() d = dest[:3].upper() if len(dest) >= 3 else dest.upper() sections = [] # Header Card sections.append(f'''

πŸŽ‰ Your Trip to {dest}!

{origin} β†’ {dest} β€’ {start} to {end} β€’ {travelers} travelers β€’ {budget}

''') # Agent Results Cards agent_configs = [ ("flights", "✈️", "Flights", "#EDE7F6", "#7C4DFF"), ("hotels", "🏨", "Hotels", "#FBE9E7", "#FF7043"), ("activities", "🎯", "Activities", "#E8F5E9", "#66BB6A"), ("dining", "🍽️", "Dining", "#FFF3E0", "#FFA726"), ("transport", "πŸš—", "Transport", "#E3F2FD", "#42A5F5"), ("weather", "🌀️", "Weather", "#E1F5FE", "#4FC3F7"), ] for key, icon, title, bg, accent in agent_configs: data = agent_results.get(key, "Research completed") links_html = make_booking_links_html(key, trip_data) if key != "weather" else "" sections.append(f'''

{icon} {title}

{data}
{links_html}
''') # Footer sections.append('''

✨ Have an amazing trip! ✨

Generated by AI Travel Concierge β€’ MCP Hackathon

''') return "".join(sections) def generate_final_report_markdown(trip_data: dict, agent_results: dict) -> str: """Generate final report in Markdown format for chat display""" import urllib.parse dest = trip_data.get("destination", "Destination") origin = trip_data.get("origin", "Origin") start = trip_data.get("start_date", "") end = trip_data.get("end_date", "") travelers = trip_data.get("travelers", 2) budget = trip_data.get("budget", "moderate").title() # Clean versions for URLs dest_clean = dest.split(",")[0].strip() origin_clean = origin.split(",")[0].strip() # URL encoding dest_encoded = urllib.parse.quote(dest_clean) origin_encoded = urllib.parse.quote(origin_clean) dest_lower = dest_clean.lower().replace(" ", "-") origin_lower = origin_clean.lower().replace(" ", "-") report = f"""# πŸŽ‰ Your Complete Trip to {dest}! **{origin} β†’ {dest}** β€’ {start} to {end} β€’ {travelers} travelers β€’ {budget} Budget --- ## ✈️ Flights {agent_results.get('flights', 'No flight data')} πŸ”— **Book Now:** [Google Flights](https://www.google.com/travel/flights?q=flights%20from%20{origin_encoded}%20to%20{dest_encoded}%20{start}%20to%20{end}) | [Skyscanner](https://www.skyscanner.com/transport/flights/{origin_lower}/{dest_lower}/{start}/{end}/?adults={travelers}&cabinclass=economy) | [Kayak](https://www.kayak.com/flights/{origin_clean}-{dest_clean}/{start}/{end}/{travelers}adults?sort=bestflight_a) | [Momondo](https://www.momondo.com/flight-search/{origin_clean}-{dest_clean}/{start}/{end}/{travelers}adults) --- ## 🏨 Hotels {agent_results.get('hotels', 'No hotel data')} πŸ”— **Book Now:** [Booking.com](https://www.booking.com/searchresults.html?ss={dest_encoded}&checkin={start}&checkout={end}&group_adults={travelers}&no_rooms=1) | [Hotels.com](https://www.hotels.com/Hotel-Search?destination={dest_encoded}&startDate={start}&endDate={end}&rooms=1&adults={travelers}) | [Airbnb](https://www.airbnb.com/s/{dest_encoded}/homes?checkin={start}&checkout={end}&adults={travelers}) | [Agoda](https://www.agoda.com/search?city={dest_encoded}&checkIn={start}&checkOut={end}&rooms=1&adults={travelers}) --- ## 🎯 Activities & Tours {agent_results.get('activities', 'No activities data')} πŸ”— **Book Now:** [Viator](https://www.viator.com/searchResults/all?text={dest_encoded}) | [GetYourGuide](https://www.getyourguide.com/s/?q={dest_encoded}&date_from={start}&date_to={end}) | [TripAdvisor](https://www.tripadvisor.com/Search?q={dest_encoded}%20things%20to%20do) | [Klook](https://www.klook.com/search/?keyword={dest_encoded}) --- ## 🍽️ Dining {agent_results.get('dining', 'No dining data')} πŸ”— **Reserve:** [TripAdvisor](https://www.tripadvisor.com/Search?q={dest_encoded}%20restaurants) | [Yelp](https://www.yelp.com/search?find_desc=restaurants&find_loc={dest_encoded}) | [OpenTable](https://www.opentable.com/s?term={dest_encoded}) | [TheFork](https://www.thefork.com/search/?cityId={dest_encoded}) --- ## πŸš— Local Transport {agent_results.get('transport', 'No transport data')} πŸ”— **Book:** [Rome2Rio](https://www.rome2rio.com/s/{origin_encoded}/{dest_encoded}) | [Uber](https://m.uber.com/looking) | [GetTransfer](https://gettransfer.com/en?utm_source=widget&from={dest_encoded}%20Airport&to={dest_encoded}%20City%20Center) | [Bolt](https://bolt.eu/) --- ## 🌀️ Weather Forecast {agent_results.get('weather', 'No weather data')} --- ### ✨ Have an amazing trip! ✨ *Generated by AI Travel Concierge β€’ MCP 1st Birthday Hackathon* --- **Optional:** Would you like me to generate a beautiful **AI travel poster** for your trip? Click the button below! 🎨""" return report async def generate_poster(mcp_client: MCPClient, trip_data: dict, company_info: dict = None) -> tuple: """Generate travel poster using Flux-dev2 via Modal. Returns (message, image_path)""" destination = trip_data.get("destination", "Beautiful Destination") origin = trip_data.get("origin", "") start_date = trip_data.get("start_date", "") end_date = trip_data.get("end_date", "") travelers = trip_data.get("travelers", 2) budget = trip_data.get("budget", "moderate") interests = trip_data.get("interests", []) # Calculate duration duration = "" if start_date and end_date: try: from datetime import datetime start = datetime.strptime(start_date, "%Y-%m-%d") end = datetime.strptime(end_date, "%Y-%m-%d") nights = (end - start).days duration = f"{nights}N {nights+1}D" except: pass # Format dates nicely dates_display = "" if start_date and end_date: try: from datetime import datetime start = datetime.strptime(start_date, "%Y-%m-%d") end = datetime.strptime(end_date, "%Y-%m-%d") dates_display = f"{start.strftime('%b %d')} - {end.strftime('%b %d, %Y')}" except: dates_display = f"{start_date} to {end_date}" # Company info defaults company_info = company_info or {} try: # Tool: generate_poster_image with full trip details and company branding resp = await mcp_client.call_tool("poster", "generate_poster_image", { "destination": destination, "origin": origin, "dates": dates_display, "duration": duration, "price": company_info.get("price", ""), "travelers": travelers, "budget": budget, "interests": ", ".join(interests) if interests else "travel", "company_name": company_info.get("company_name", ""), "company_phone": company_info.get("company_phone", ""), "company_email": company_info.get("company_email", ""), "company_website": company_info.get("company_website", ""), "inclusions": company_info.get("inclusions", "Flights, Hotels, Tours, Meals"), "tagline": company_info.get("tagline", "") }) result = resp.get("data", "") if resp.get("success") else None if result: # Check if it's a file path if result.startswith("/") and os.path.exists(result): return (f"βœ… Professional travel poster generated!", result) # Check if it's in the current directory elif os.path.exists(result): return (f"βœ… Professional travel poster generated!", result) # Check for local poster files else: import glob poster_files = glob.glob(f"poster_{destination.lower().replace(' ', '_').replace(',', '')}*.jpg") if poster_files: latest = max(poster_files, key=os.path.getmtime) return (f"βœ… Professional travel poster generated!", os.path.abspath(latest)) return (f"Poster generated: {result}", None) else: return (f"❌ Error: {resp.get('error', 'Unknown error')}", None) except Exception as e: return (f"❌ Error: {str(e)}", None) async def parse_trip_request(message: str) -> dict: """Parse user message to extract trip details using AI""" if not NEBIUS_API_KEY: print("❌ ERROR: No API key configured!") return {"error": "API key not configured. Please add OPENAI_API_KEY to Space secrets."} async with httpx.AsyncClient(timeout=30.0) as client: try: response = await client.post( f"{NEBIUS_BASE_URL}chat/completions", headers={"Authorization": f"Bearer {NEBIUS_API_KEY}", "Content-Type": "application/json"}, json={ "model": MODEL, "messages": [ {"role": "system", "content": """Extract trip details from user message. Return ONLY valid JSON. IMPORTANT: If user doesn't know where to go, says "suggest", "recommend", "I don't know", "undecided", "best destinations", "where should I go", "surprise me", or similar - set destination to "SUGGEST". Return JSON format: {"origin": "city or empty", "destination": "city or SUGGEST", "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD", "travelers": number, "budget": "budget/moderate/luxury", "interests": ["list"], "needs_suggestion": true/false} Set needs_suggestion=true if user is undecided about destination. Use defaults if missing: dates=30 days from now for 7 days, travelers=2, budget=moderate."""}, {"role": "user", "content": message} ], "temperature": 0.3, "max_tokens": 500 } ) response.raise_for_status() content = response.json()["choices"][0]["message"]["content"] # Clean up response if "```json" in content: content = content.split("```json")[1].split("```")[0] elif "```" in content: content = content.split("```")[1].split("```")[0] # Remove any thinking tags if "" in content: content = content.split("")[-1] return json.loads(content.strip()) except httpx.HTTPStatusError as e: print(f"API HTTP error: {e.response.status_code} - {e.response.text}") return {"error": f"API error: {e.response.status_code}"} except Exception as e: print(f"Parse error: {e}") return {"error": str(e)} # ============== GRADIO 6 APP ============== def create_app(): # ✨ AWARD-WINNING PREMIUM CSS ✨ custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap'); :root { --primary: #6366f1; --primary-dark: #4f46e5; --secondary: #8b5cf6; --accent: #ec4899; --success: #10b981; --warning: #f59e0b; --error: #ef4444; --bg-dark: #0f172a; --bg-card: #1e293b; --bg-hover: #334155; --text-primary: #f8fafc; --text-secondary: #94a3b8; --border: #334155; --glow: rgba(99, 102, 241, 0.4); } * { font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif !important; box-sizing: border-box; } body, .gradio-container { background: var(--bg-dark) !important; color: var(--text-primary) !important; } .gradio-container { max-width: 1600px !important; margin: 0 auto !important; padding: 0 !important; } /* ===== MAIN LAYOUT ===== */ .main-wrapper { min-height: 100vh; background: linear-gradient(180deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%); position: relative; overflow: hidden; } .main-wrapper::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99, 102, 241, 0.3), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(139, 92, 246, 0.15), transparent), radial-gradient(ellipse 50% 30% at 20% 80%, rgba(236, 72, 153, 0.1), transparent); pointer-events: none; } /* ===== HEADER ===== */ .hero-header { text-align: center; padding: 50px 20px 40px; position: relative; z-index: 1; } .hero-title { font-size: 3.5rem; font-weight: 800; background: linear-gradient(135deg, #fff 0%, #a5b4fc 50%, #c4b5fd 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin: 0 0 12px; letter-spacing: -0.02em; text-shadow: 0 0 80px rgba(99, 102, 241, 0.5); } .hero-subtitle { color: var(--text-secondary); font-size: 1.15rem; font-weight: 500; margin: 0 0 24px; } .badge-row { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; } .badge { background: rgba(99, 102, 241, 0.15); border: 1px solid rgba(99, 102, 241, 0.3); padding: 8px 16px; border-radius: 50px; font-size: 13px; font-weight: 600; color: #a5b4fc; backdrop-filter: blur(10px); transition: all 0.3s ease; } .badge:hover { background: rgba(99, 102, 241, 0.25); border-color: rgba(99, 102, 241, 0.5); transform: translateY(-2px); } /* ===== GLASS CARDS ===== */ .glass-card { background: rgba(30, 41, 59, 0.8) !important; backdrop-filter: blur(20px) !important; border: 1px solid rgba(99, 102, 241, 0.2) !important; border-radius: 24px !important; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; padding: 24px !important; position: relative; overflow: hidden; } .glass-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); } /* ===== CHATBOT ===== */ .chatbot-wrap { border: none !important; background: transparent !important; } .chatbot-wrap > div { background: rgba(15, 23, 42, 0.6) !important; border-radius: 20px !important; border: 1px solid var(--border) !important; } /* Message styling */ .message { padding: 16px 20px !important; border-radius: 20px !important; margin: 8px 0 !important; max-width: 85% !important; animation: messageSlide 0.3s ease-out; } @keyframes messageSlide { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .user-message { background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%) !important; color: white !important; margin-left: auto !important; border-bottom-right-radius: 6px !important; } .bot-message { background: var(--bg-card) !important; border: 1px solid var(--border) !important; color: var(--text-primary) !important; border-bottom-left-radius: 6px !important; } /* ===== INPUT FIELD ===== */ .input-container input, .input-container textarea { background: var(--bg-card) !important; border: 2px solid var(--border) !important; border-radius: 16px !important; padding: 16px 20px !important; font-size: 15px !important; color: var(--text-primary) !important; transition: all 0.3s ease !important; } .input-container input:focus, .input-container textarea:focus { border-color: var(--primary) !important; box-shadow: 0 0 0 4px var(--glow), 0 0 30px var(--glow) !important; outline: none !important; } .input-container input::placeholder { color: var(--text-secondary) !important; } /* ===== BUTTONS ===== */ .btn-primary { background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%) !important; color: white !important; border: none !important; border-radius: 14px !important; padding: 14px 28px !important; font-weight: 700 !important; font-size: 15px !important; cursor: pointer !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4), 0 0 0 0 rgba(99, 102, 241, 0) !important; position: relative; overflow: hidden; } .btn-primary::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); transition: 0.5s; } .btn-primary:hover { transform: translateY(-3px) scale(1.02) !important; box-shadow: 0 8px 30px rgba(99, 102, 241, 0.5), 0 0 50px rgba(99, 102, 241, 0.3) !important; } .btn-primary:hover::before { left: 100%; } .btn-secondary { background: transparent !important; color: var(--text-primary) !important; border: 2px solid var(--border) !important; border-radius: 14px !important; padding: 12px 20px !important; font-weight: 600 !important; font-size: 14px !important; cursor: pointer !important; transition: all 0.3s ease !important; } .btn-secondary:hover { background: var(--bg-hover) !important; border-color: var(--primary) !important; color: #a5b4fc !important; transform: translateY(-2px) !important; } .btn-poster { background: linear-gradient(135deg, var(--accent) 0%, #f472b6 100%) !important; box-shadow: 0 4px 15px rgba(236, 72, 153, 0.4) !important; } .btn-poster:hover { box-shadow: 0 8px 30px rgba(236, 72, 153, 0.5), 0 0 50px rgba(236, 72, 153, 0.3) !important; } .btn-continue { background: linear-gradient(135deg, var(--success) 0%, #34d399 100%) !important; box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4) !important; width: 100% !important; margin-top: 16px !important; } .btn-continue:hover { box-shadow: 0 8px 30px rgba(16, 185, 129, 0.5) !important; } /* ===== AGENT STATUS PANEL ===== */ .agent-panel { background: rgba(30, 41, 59, 0.6) !important; border-radius: 20px !important; padding: 20px !important; border: 1px solid var(--border) !important; } .panel-title { color: var(--text-primary); font-size: 15px; font-weight: 700; margin: 0 0 16px; display: flex; align-items: center; gap: 8px; } .agent-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; margin: 6px 0; border-radius: 12px; background: var(--bg-dark); border: 1px solid transparent; transition: all 0.3s ease; } .agent-item.active { background: rgba(245, 158, 11, 0.1); border-color: rgba(245, 158, 11, 0.3); } .agent-item.active .agent-status { color: var(--warning); } .agent-item.done { background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.3); } .agent-item.done .agent-status { color: var(--success); } .agent-name { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 14px; color: var(--text-primary); } .agent-icon { font-size: 18px; } .agent-status { font-size: 12px; font-weight: 600; color: var(--text-secondary); } /* ===== QUICK BUTTONS ROW ===== */ .quick-section { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border); } .quick-label { color: var(--text-secondary); font-size: 13px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; } /* ===== FOOTER ===== */ .footer { text-align: center; padding: 30px 20px; color: var(--text-secondary); font-size: 13px; position: relative; z-index: 1; } .footer a { color: var(--primary); text-decoration: none; } /* ===== ANIMATIONS ===== */ @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } @keyframes glow { 0%, 100% { box-shadow: 0 0 20px var(--glow); } 50% { box-shadow: 0 0 40px var(--glow), 0 0 60px var(--glow); } } @keyframes pulse-ring { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(2); opacity: 0; } } .animate-float { animation: float 3s ease-in-out infinite; } .animate-glow { animation: glow 2s ease-in-out infinite; } /* ===== SCROLLBAR ===== */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--bg-dark); border-radius: 4px; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--primary); } /* ===== POSTER IMAGE DISPLAY ===== */ .poster-preview { margin-top: 16px; border-radius: 16px; overflow: hidden; background: linear-gradient(135deg, rgba(236, 72, 153, 0.1), rgba(139, 92, 246, 0.1)); border: 2px solid rgba(236, 72, 153, 0.3); transition: all 0.3s ease; } .poster-preview:hover { border-color: rgba(236, 72, 153, 0.5); box-shadow: 0 8px 30px rgba(236, 72, 153, 0.2); } .poster-preview img { border-radius: 12px !important; transition: transform 0.3s ease; } .poster-preview:hover img { transform: scale(1.02); } .poster-preview .label-wrap { background: linear-gradient(135deg, var(--accent), #f472b6) !important; padding: 10px 16px !important; border-radius: 12px 12px 0 0 !important; } .poster-preview .label-wrap span { color: white !important; font-weight: 700 !important; font-size: 14px !important; } .poster-preview .download-button, .poster-preview .share-button { background: var(--primary) !important; color: white !important; border-radius: 8px !important; padding: 8px 12px !important; transition: all 0.2s ease !important; } .poster-preview .download-button:hover, .poster-preview .share-button:hover { background: var(--secondary) !important; transform: translateY(-2px) !important; } /* ===== RESPONSIVE ===== */ @media (max-width: 768px) { .hero-title { font-size: 2.2rem; } .hero-subtitle { font-size: 1rem; } .glass-card { padding: 16px !important; border-radius: 18px !important; } } """ with gr.Blocks(title="🌍 AI Travel Concierge | MCP Hackathon", theme=gr.themes.Base(), css=custom_css) as app: # State variables mcp_client_state = gr.State(None) trip_data_state = gr.State({}) current_agent_state = gr.State(-1) agent_results_state = gr.State({}) # ===== MAIN WRAPPER ===== with gr.Column(elem_classes="main-wrapper"): # ===== HERO HEADER ===== gr.HTML("""

✨ AI Travel Concierge

Your premium AI-powered travel planning experience

πŸ€– 7 Specialized AI Agents πŸ”— Real Booking Links 🎨 AI Poster Generator ⚑ Powered by MCP
""") # ===== MAIN CONTENT ===== with gr.Row(elem_id="main-content"): # ===== CHAT COLUMN ===== with gr.Column(scale=3): with gr.Column(elem_classes="glass-card"): # Chatbot chatbot = gr.Chatbot( value=[{"role": "assistant", "content": """## πŸ‘‹ Welcome to AI Travel Concierge! I'm your **premium travel planning assistant**, powered by **8 specialized AI agents** working together to create your perfect trip. ### 🎯 Two ways to get started: **Option 1 - I know where I want to go:** Tell me your destination, dates, travelers, and budget. *Example: "Plan a trip from London to Dubai, Dec 29 - Jan 5, 2 people, luxury"* **Option 2 - Help me decide! πŸ€”** Not sure where to go? Click **"Find My Perfect Destination"** and I'll recommend the best places based on your: - ❀️ Interests (beach, culture, adventure, food...) - πŸ’° Budget - πŸ“… Travel dates - πŸ”₯ Current deals & offers --- **Quick actions:** Click a destination below or ask me anything! ⬇️"""}], height=500, type="messages", show_label=False, elem_classes="chatbot-wrap", ) # Input Area with gr.Row(elem_classes="input-container"): user_input = gr.Textbox( placeholder="✈️ Describe your dream trip or ask for recommendations...", show_label=False, scale=5, container=False, lines=1, ) send_btn = gr.Button("Send β†’", elem_classes="btn-primary", scale=1) # Quick Actions Row gr.HTML('

πŸš€ Quick Actions

') with gr.Row(): help_decide_btn = gr.Button("πŸ€” Find My Perfect Destination", elem_classes="btn-primary", size="sm") deals_btn = gr.Button("πŸ”₯ See Current Deals", elem_classes="btn-secondary", size="sm") # Quick Destinations gr.HTML('

⚑ Popular Destinations

') with gr.Row(): quick_paris = gr.Button("πŸ—Ό Paris", elem_classes="btn-secondary", size="sm") quick_bali = gr.Button("🏝️ Bali", elem_classes="btn-secondary", size="sm") quick_tokyo = gr.Button("🌸 Tokyo", elem_classes="btn-secondary", size="sm") quick_rome = gr.Button("πŸ›οΈ Rome", elem_classes="btn-secondary", size="sm") quick_dubai = gr.Button("🏜️ Dubai", elem_classes="btn-secondary", size="sm") # ===== SIDEBAR ===== with gr.Column(scale=1): # Agent Status Panel with gr.Column(elem_classes="agent-panel"): gr.HTML('

πŸ€– AI Agent Status

') agent_status_html = gr.HTML("""
🌀️WeatherReady
✈️FlightsReady
🏨HotelsReady
🎯ActivitiesReady
🍽️DiningReady
πŸš—TransportReady
""") # Action Buttons continue_btn = gr.Button("▢️ Continue to Next Agent", elem_classes="btn-primary btn-continue", visible=False, size="lg") poster_btn = gr.Button("🎨 Generate Travel Poster", elem_classes="btn-primary btn-poster", visible=False, size="lg") # Poster Preview Section gr.HTML('
') poster_image = gr.Image( label="🎨 Your AI-Generated Travel Poster", type="filepath", visible=False, show_download_button=True, show_share_button=True, container=True, height=400, elem_classes="poster-preview", ) # Company Branding Section (collapsible) with gr.Accordion("🏒 Company Branding (Optional)", open=False, elem_classes="agent-panel"): gr.HTML('

Add your travel company details to the poster

') company_name = gr.Textbox( label="Company Name", placeholder="e.g., Wanderlust Travel", elem_classes="input-container" ) company_phone = gr.Textbox( label="Phone Number", placeholder="e.g., +1 800 555 1234", elem_classes="input-container" ) company_website = gr.Textbox( label="Website", placeholder="e.g., www.travel.com", elem_classes="input-container" ) poster_price = gr.Textbox( label="Price Display", placeholder="e.g., From $999 or $1,700 Per Person", elem_classes="input-container" ) poster_tagline = gr.Textbox( label="Custom Tagline", placeholder="e.g., Discover Your Journey", elem_classes="input-container" ) poster_inclusions = gr.Textbox( label="Inclusions (comma-separated)", placeholder="e.g., Flights, Hotels, Meals, Tours, Visa", value="Flights, Hotels, Tours & Transfers, Meals", elem_classes="input-container" ) # ===== FOOTER ===== gr.HTML(""" """) # ============== EVENT HANDLERS ============== def update_agent_status(current: int, results: dict) -> str: """Update agent status panel""" agents = [ ("🌀️", "Weather", "weather"), ("✈️", "Flights", "flights"), ("🏨", "Hotels", "hotels"), ("🎯", "Activities", "activities"), ("🍽️", "Dining", "dining"), ("πŸš—", "Transport", "transport"), ] html = '' for i, (icon, name, key) in enumerate(agents): if key in results: cls = "done" status = 'βœ“ Complete' elif i == current: cls = "active" status = '⏳ Working...' else: cls = "" status = 'Ready' html += f'
{icon}{name}{status}
' return html async def handle_message(message: str, history: list, mcp_client, trip_data: dict, current_agent: int, agent_results: dict): """Handle user message""" if not message.strip(): yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) return # Add user message history = history + [{"role": "user", "content": message}] # If no trip data, parse the request if not trip_data.get("destination"): history = history + [{"role": "assistant", "content": "πŸ” **Analyzing your trip request...**"}] yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) trip_data = await parse_trip_request(message) # Check for API error if trip_data.get("error"): history[-1] = {"role": "assistant", "content": f"""❌ **API Error** {trip_data.get('error')} Please check that the API keys are configured correctly in Space settings."""} yield history, mcp_client, {}, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) return # Check if user needs destination suggestions needs_suggestion = trip_data.get("needs_suggestion", False) or trip_data.get("destination", "").upper() == "SUGGEST" if needs_suggestion or not trip_data.get("destination"): # Initialize MCP for recommendations mcp_client = MCPClient() try: await mcp_client.connect_server("recommendations", MCP_SERVERS["recommendations"]) except Exception as e: print(f"Failed to connect recommendations: {e}") # Get smart recommendations origin = trip_data.get("origin", "") budget = trip_data.get("budget", "moderate") travelers = trip_data.get("travelers", 2) interests = ",".join(trip_data.get("interests", [])) # Calculate trip days trip_days = 7 try: if trip_data.get("start_date") and trip_data.get("end_date"): from datetime import datetime d1 = datetime.strptime(trip_data["start_date"], "%Y-%m-%d") d2 = datetime.strptime(trip_data["end_date"], "%Y-%m-%d") trip_days = (d2 - d1).days except: pass # Get travel month travel_month = 0 try: if trip_data.get("start_date"): travel_month = int(trip_data["start_date"].split("-")[1]) except: pass history[-1] = {"role": "assistant", "content": f"""🧭 **I'll help you find the perfect destination!** Let me search for the best options based on: β€’ πŸ‘₯ **Travelers:** {travelers} β€’ πŸ“… **Days:** {trip_days} β€’ πŸ’° **Budget:** {budget.title()} {f"β€’ πŸ“ **From:** {origin}" if origin else ""} {f"β€’ ❀️ **Interests:** {interests}" if interests else ""} πŸ” Searching for deals and recommendations..."""} yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) # Call recommendations agent result = await mcp_client.call_tool("recommendations", "get_destination_recommendations", { "origin": origin, "budget": budget, "travel_month": travel_month, "interests": interests, "travelers": travelers, "trip_days": trip_days }) if result.get("success"): recommendations_text = result.get("data", "") history[-1] = {"role": "assistant", "content": f"""{recommendations_text} --- 🎯 **To continue planning:** Just tell me which destination you'd like! *Example: "I want to go to Dubai" or "Let's do Bali" or just type the destination name.*"""} else: history[-1] = {"role": "assistant", "content": """🌍 **Here are some amazing destinations to consider:** ### πŸ”₯ Hot Deals Right Now: β€’ **Dubai** - 25% OFF Winter Sun Sale β˜€οΈ β€’ **Bali** - 30% OFF Early Bird 2026 🌴 β€’ **Maldives** - 20% OFF Honeymoon Special 🏝️ β€’ **Tokyo** - 15% OFF Cherry Blossom Preview 🌸 ### πŸ’‘ Based on your preferences: **For Beach & Relaxation:** Maldives, Bali, Phuket, CancΓΊn **For Culture & History:** Rome, Paris, Tokyo, Istanbul **For Adventure:** Iceland, Cape Town, Marrakech **For Budget-Friendly:** Budapest, Lisbon, Vietnam --- 🎯 **Tell me which destination interests you**, or give me more details: β€’ *"I want beaches and luxury"* β€’ *"Looking for culture on a budget"* β€’ *"Surprise me with something adventurous!"*"""} yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) return # Initialize MCP mcp_client = MCPClient() for name, config in MCP_SERVERS.items(): try: await mcp_client.connect_server(name, config) except Exception as e: print(f"Failed to connect {name}: {e}") summary = f"""βœ… **Perfect! Here's your trip:** | | | |---|---| | **From** | {trip_data.get('origin', 'Not specified')} | | **To** | {trip_data.get('destination')} | | **Dates** | {trip_data.get('start_date')} β†’ {trip_data.get('end_date')} | | **Travelers** | {trip_data.get('travelers', 2)} | | **Budget** | {trip_data.get('budget', 'moderate').title()} | πŸš€ **Ready to start planning!** Click **"Continue to Next Agent"** to begin with weather research, or I'll guide you through each step. *Each agent will find the best options with real booking links!*""" history[-1] = {"role": "assistant", "content": summary} current_agent = -1 yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=True), gr.update(visible=False) return # Handle commands lower_msg = message.lower() if any(cmd in lower_msg for cmd in ["continue", "next", "start", "go", "yes"]): history[-1] = {"role": "assistant", "content": "πŸ‘† Click the **Continue to Next Agent** button to proceed!"} elif "poster" in lower_msg: history[-1] = {"role": "assistant", "content": "🎨 Click the **Generate Travel Poster** button to create your poster!"} else: history = history + [{"role": "assistant", "content": f"I'm helping you plan your trip to **{trip_data.get('destination')}**! Click **Continue** to proceed with the next agent."}] yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=current_agent < 6), gr.update(visible=current_agent >= 6) async def continue_to_next(history: list, mcp_client, trip_data: dict, current_agent: int, agent_results: dict): """Continue to next agent""" if not mcp_client or not trip_data.get("destination"): yield history, mcp_client, trip_data, current_agent, agent_results, update_agent_status(current_agent, agent_results), gr.update(visible=False), gr.update(visible=False) return next_agent = current_agent + 1 # All done? if next_agent >= len(AGENT_SEQUENCE): report_md = generate_final_report_markdown(trip_data, agent_results) history = history + [{"role": "assistant", "content": report_md}] yield history, mcp_client, trip_data, next_agent, agent_results, update_agent_status(next_agent, agent_results), gr.update(visible=False), gr.update(visible=True) return # Run agent agent_key = AGENT_SEQUENCE[next_agent] agent = AGENTS[agent_key] history = history + [{"role": "assistant", "content": f"""{agent['icon']} **{agent['name']}** Searching for the best options..."""}] yield history, mcp_client, trip_data, next_agent, agent_results, update_agent_status(next_agent, agent_results), gr.update(visible=False), gr.update(visible=False) # Get results result = await run_agent(mcp_client, agent_key, trip_data) agent_results[agent_key] = result booking_links = make_booking_links_markdown(agent_key, trip_data) history[-1] = {"role": "assistant", "content": f"""{agent['icon']} **{agent['name']} Results** {result} {booking_links} --- Click **Continue** for the next agent! ▢️"""} yield history, mcp_client, trip_data, next_agent, agent_results, update_agent_status(next_agent, agent_results), gr.update(visible=True), gr.update(visible=False) async def create_poster(history: list, mcp_client, trip_data: dict, comp_name: str, comp_phone: str, comp_website: str, price: str, tagline: str, inclusions: str): """Generate travel poster and display in interface""" if not mcp_client or not trip_data.get("destination"): yield history, gr.update(visible=False, value=None) return dest = trip_data.get("destination", "") # Build company info dict company_info = { "company_name": comp_name or "", "company_phone": comp_phone or "", "company_website": comp_website or "", "price": price or "", "tagline": tagline or "", "inclusions": inclusions or "Flights, Hotels, Tours, Meals" } history = history + [{"role": "assistant", "content": f"""🎨 **Generating your PROFESSIONAL travel agency poster for {dest}...** ✨ Creating marketing-style poster with: - Bold destination typography - Polaroid photo collages with landmarks - Traveler silhouettes {f'- Trip dates & duration' if trip_data.get('start_date') else ''} {f'- Price: {price}' if price else ''} {f'- Company: {comp_name}' if comp_name else ''} {f'- Inclusions: {inclusions}' if inclusions else ''} ⏳ This may take 30-60 seconds..."""}] yield history, gr.update(visible=False, value=None) msg, image_path = await generate_poster(mcp_client, trip_data, company_info) if image_path and os.path.exists(image_path): file_size = os.path.getsize(image_path) / 1024 # KB history[-1] = {"role": "assistant", "content": f"""🎨 **Your Professional Travel Poster is Ready!** {msg} πŸ“ **File:** `{os.path.basename(image_path)}` πŸ“ **Size:** {file_size:.1f} KB πŸ“± **Format:** 9:16 Portrait (Social Media Ready) πŸ‘‡ **Preview shown below** - Click to download or share! --- ✨ *Generated with Flux AI β€’ Professional Travel Agency Style* {f'🏒 *Branded for: {comp_name}*' if comp_name else ''}"""} yield history, gr.update(visible=True, value=image_path) else: history[-1] = {"role": "assistant", "content": f"""⚠️ **Poster Generation Status** {msg} *If the poster was saved locally, check your workspace folder.*"""} yield history, gr.update(visible=False, value=None) def quick_destination(dest: str, history: list): """Handle quick destination buttons""" today = datetime.now() start = (today + timedelta(days=30)).strftime("%Y-%m-%d") end = (today + timedelta(days=37)).strftime("%Y-%m-%d") prompts = { "Paris": f"Plan a romantic trip from New York to Paris, France for 2 people, {start} to {end}, moderate budget, interested in culture, food, and art", "Bali": f"Plan an adventure trip from Los Angeles to Bali, Indonesia for 2 people, {start} to {end}, moderate budget, interested in nature, beaches, and relaxation", "Tokyo": f"Plan an exciting trip from San Francisco to Tokyo, Japan for 2 people, {start} to {end}, moderate budget, interested in culture, food, and technology", "Rome": f"Plan a cultural trip from Chicago to Rome, Italy for 2 people, {start} to {end}, moderate budget, interested in history, art, and food", "Dubai": f"Plan a luxury trip from London to Dubai, UAE for 2 people, {start} to {end}, luxury budget, interested in shopping, architecture, and adventure", } return prompts.get(dest, ""), history def help_decide_destination(history: list): """Show interactive destination finder wizard""" wizard_message = """# 🧭 **Let's Find Your Perfect Destination!** I'll help you discover the ideal place for your next adventure. Answer a few quick questions: --- ## 🎯 **What kind of experience are you looking for?** **Choose your travel style** (click one to tell me): - πŸ–οΈ **Beach & Relaxation** - Unwind by crystal clear waters - πŸ”οΈ **Adventure & Nature** - Hiking, wildlife, outdoor thrills - πŸ›οΈ **Culture & History** - Museums, architecture, heritage - 🍽️ **Food & Nightlife** - Culinary tours, restaurants, bars - πŸ’‘ **Romance & Honeymoon** - Intimate getaways for couples - πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ **Family Fun** - Kid-friendly activities and resorts - πŸ›οΈ **Shopping & Luxury** - High-end experiences, designer brands - 🎰 **Nightlife & Entertainment** - Clubs, shows, casinos --- **πŸ’¬ Tell me your preferences!** For example: > *"I want a beach vacation with good food, budget around $2000 per person, traveling in March"* Or simply type keywords like: **beach, adventure, romantic, budget-friendly, luxury, family** I'll match you with the **best destinations** and show you **current deals**! 🎁""" history.append({"role": "assistant", "content": wizard_message}) return "", history def show_current_deals(history: list): """Display current travel deals and offers""" deals_message = """# πŸ”₯ **Hot Travel Deals Right Now!** Here are the **best offers** from our partner travel agents: --- ## ✨ **Featured Deals** ### 🏜️ **Dubai Winter Escape** - *25% OFF* > **From $1,499/person** (was $1,999) > - 5 nights at JW Marriott Marquis > - Desert safari included > - *Valid: Dec 2024 - Feb 2025* > > 🏷️ `luxury` `adventure` `shopping` --- ### 🏝️ **Bali Early Bird Special** - *30% OFF* > **From $899/person** (was $1,285) > - 7 nights beachfront resort > - Temple tour & spa day > - *Book by Dec 31 for travel Jan-Mar* > > 🏷️ `beach` `relaxation` `culture` --- ### 🌸 **Tokyo Cherry Blossom Package** - *15% OFF* > **From $1,699/person** (was $1,999) > - 6 nights in Shinjuku > - JR Pass included > - *Travel: Mar 20 - Apr 15, 2025* > > 🏷️ `culture` `food` `unique` --- ### πŸ‡¬πŸ‡· **Greek Island Hopper** - *20% OFF* > **From $1,299/person** (was $1,624) > - Santorini + Mykonos combo > - Ferry transfers included > - *Travel: May - Oct 2025* > > 🏷️ `beach` `romantic` `culture` --- ### πŸ‡²πŸ‡» **Maldives Honeymoon Dream** - *$500 Resort Credit* > **From $2,999/person** > - 5 nights overwater villa > - Sunset dinner cruise > - *Book by Jan 31, 2025* > > 🏷️ `luxury` `romantic` `beach` --- ### πŸ—Ό **Paris City Break** - *Stay 4 Pay 3* > **From $799/person** > - 4 nights near Champs-Γ‰lysΓ©es > - Seine river cruise > - *Travel: Nov 2024 - Mar 2025* > > 🏷️ `romantic` `culture` `food` --- ## πŸ’‘ **Interested in a deal?** Just tell me which one catches your eye, or describe what you're looking for: > *"Tell me more about the Bali deal"* > *"I want something romantic under $1500"* > *"Find me beach destinations with deals"* I'll help you **book the perfect trip** at the best price! 🎯""" history.append({"role": "assistant", "content": deals_message}) return "", history # Wire events send_btn.click( fn=handle_message, inputs=[user_input, chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state], outputs=[chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state, agent_status_html, continue_btn, poster_btn] ).then(lambda: "", outputs=[user_input]) user_input.submit( fn=handle_message, inputs=[user_input, chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state], outputs=[chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state, agent_status_html, continue_btn, poster_btn] ).then(lambda: "", outputs=[user_input]) continue_btn.click( fn=continue_to_next, inputs=[chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state], outputs=[chatbot, mcp_client_state, trip_data_state, current_agent_state, agent_results_state, agent_status_html, continue_btn, poster_btn] ) poster_btn.click( fn=create_poster, inputs=[chatbot, mcp_client_state, trip_data_state, company_name, company_phone, company_website, poster_price, poster_tagline, poster_inclusions], outputs=[chatbot, poster_image] ) # Quick action buttons - Help Decide & Deals help_decide_btn.click(fn=help_decide_destination, inputs=[chatbot], outputs=[user_input, chatbot]) deals_btn.click(fn=show_current_deals, inputs=[chatbot], outputs=[user_input, chatbot]) # Quick destination buttons quick_paris.click(lambda h: quick_destination("Paris", h), inputs=[chatbot], outputs=[user_input, chatbot]) quick_bali.click(lambda h: quick_destination("Bali", h), inputs=[chatbot], outputs=[user_input, chatbot]) quick_tokyo.click(lambda h: quick_destination("Tokyo", h), inputs=[chatbot], outputs=[user_input, chatbot]) quick_rome.click(lambda h: quick_destination("Rome", h), inputs=[chatbot], outputs=[user_input, chatbot]) quick_dubai.click(lambda h: quick_destination("Dubai", h), inputs=[chatbot], outputs=[user_input, chatbot]) return app # ============== MAIN ============== if __name__ == "__main__": app = create_app() app.launch( server_name="0.0.0.0", server_port=7860, share=True, show_error=True, )