NSamson1 commited on
Commit
285f5e6
Β·
verified Β·
1 Parent(s): 803333c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +195 -154
app.py CHANGED
@@ -1,15 +1,17 @@
1
- import os
2
  import json
3
  import requests
4
  from datetime import datetime, timedelta
 
5
  import gradio as gr
6
  import openai
7
- import random
8
  from typing import List, Dict, Any
9
 
10
  # -------------------- CONFIGURATION --------------------
11
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
12
  OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
 
13
 
14
  if not OPENAI_API_KEY:
15
  print("⚠️ OPENAI_API_KEY not set. AI itinerary generation will fall back to manual mode.")
@@ -20,41 +22,52 @@ else:
20
  # Global attractions cache
21
  ATTRACTIONS_CACHE = {}
22
 
23
- # Image cache to avoid repeated API calls
24
- IMAGE_CACHE = {}
25
-
26
- # -------------------- IMAGE FUNCTIONS --------------------
27
- def get_wikipedia_image(attraction_name: str) -> str:
28
- """Fetch an image for an attraction using Wikipedia API."""
29
- if attraction_name in IMAGE_CACHE:
30
- return IMAGE_CACHE[attraction_name]
31
- try:
32
- search_url = "https://en.wikipedia.org/w/api.php"
33
- params = {
34
- "action": "query",
35
- "format": "json",
36
- "prop": "pageimages",
37
- "piprop": "original",
38
- "titles": attraction_name
39
- }
40
- response = requests.get(search_url, params=params, timeout=5)
41
- data = response.json()
42
- pages = data.get("query", {}).get("pages", {})
43
- for page in pages.values():
44
- if "original" in page:
45
- img_url = page["original"]["source"]
46
- IMAGE_CACHE[attraction_name] = img_url
47
- return img_url
48
- except:
49
- pass
50
- # Fallback generic image if Wikipedia fails
51
- fallback = "https://upload.wikimedia.org/wikipedia/commons/6/65/No_image_available_600_x_450.svg"
52
- IMAGE_CACHE[attraction_name] = fallback
53
- return fallback
 
 
 
54
 
55
- def get_attraction_image(attraction_name: str, city: str) -> str:
56
- """Fetch an attraction image using Wikipedia only (no Unsplash)."""
57
- return get_wikipedia_image(attraction_name)
 
 
 
 
 
 
 
 
58
 
59
  # -------------------- WEATHER FUNCTION --------------------
60
  def get_weather(city: str) -> dict:
@@ -91,22 +104,31 @@ def get_weather(city: str) -> dict:
91
 
92
  # -------------------- ATTRACTIONS FUNCTIONS --------------------
93
  def fetch_attractions_via_openai(city: str) -> List[Dict]:
94
- """Fetch attractions for ANY city using OpenAI with rich descriptions."""
95
  if not client:
96
  return get_fallback_attractions(city)
97
 
98
  try:
99
  prompt = f"""
100
- You are a local expert in {city}. List the top 6-8 must-visit tourist attractions in {city}.
101
- For each attraction, provide:
102
- 1. Name (the actual famous attraction name, not generic)
103
  2. A vivid, engaging 2-sentence description
104
- 3. Entry fee (in USD, use numbers only - 0 for free)
105
  4. Visit duration in hours
106
- 5. Best time to visit (morning/afternoon/evening)
107
 
108
- Return ONLY a JSON array with these keys: name, description, entry_fee, duration_hours, best_time.
109
- Use REAL attractions specific to {city}.
 
 
 
 
 
 
 
 
 
 
 
110
  """
111
  response = client.chat.completions.create(
112
  model="gpt-3.5-turbo",
@@ -114,31 +136,46 @@ Use REAL attractions specific to {city}.
114
  temperature=0.7,
115
  max_tokens=1500
116
  )
117
- content = response.choices[0].message.content.strip()
 
 
 
118
  if content.startswith('```json'):
119
  content = content[7:]
120
  if content.startswith('```'):
121
  content = content[3:]
122
  if content.endswith('```'):
123
  content = content[:-3]
 
124
  attractions = json.loads(content)
 
125
  if isinstance(attractions, dict) and "attractions" in attractions:
126
  attractions = attractions["attractions"]
127
  elif not isinstance(attractions, list):
128
  attractions = [attractions]
 
129
  result = []
130
- for a in attractions[:8]:
131
  fee = a.get("entry_fee", 0)
132
- fee_display = "Free" if fee == 0 else f"${fee}" if isinstance(fee, (int, float)) else str(fee)
 
 
 
 
 
 
 
133
  result.append({
134
  "name": a.get("name", "Unknown"),
135
  "entry_fee": fee_display,
136
  "duration_hours": a.get("duration_hours", 2),
137
  "description": a.get("description", f"A must-visit attraction in {city}"),
138
- "best_time": a.get("best_time", "anytime")
139
  })
140
- ATTRACTIONS_CACHE[city.lower()] = result
 
 
141
  return result
 
142
  except Exception as e:
143
  print(f"OpenAI attractions error: {e}")
144
  return get_fallback_attractions(city)
@@ -146,33 +183,47 @@ Use REAL attractions specific to {city}.
146
  def get_fallback_attractions(city: str) -> List[Dict]:
147
  """Provide fallback attractions for any city."""
148
  city_lower = city.lower()
 
 
149
  real_attractions = {
150
- "paris": [
151
- {"name": "Eiffel Tower", "description": "Iconic iron lattice tower offering breathtaking panoramic views of Paris.", "entry_fee": "$25", "duration_hours": 2.5, "best_time": "evening"},
152
- {"name": "Louvre Museum", "description": "World's largest art museum and historic monument.", "entry_fee": "$20", "duration_hours": 4, "best_time": "morning"},
153
- {"name": "Notre-Dame Cathedral", "description": "Magnificent Gothic cathedral known for stunning architecture.", "entry_fee": "Free", "duration_hours": 1.5, "best_time": "morning"}
 
 
 
154
  ],
155
- "tokyo": [
156
- {"name": "Senso-ji Temple", "description": "Ancient Buddhist temple in Asakusa.", "entry_fee": "Free", "duration_hours": 2, "best_time": "morning"},
157
- {"name": "Shibuya Crossing", "description": "Famous pedestrian scramble crossing.", "entry_fee": "Free", "duration_hours": 1, "best_time": "evening"},
158
- {"name": "Tokyo Tower", "description": "Iconic red and white tower offering observation decks.", "entry_fee": "$12", "duration_hours": 1.5, "best_time": "evening"}
 
 
 
159
  ]
160
  }
 
161
  for key, attractions in real_attractions.items():
162
  if key in city_lower:
163
  return attractions
 
 
164
  return [
165
- {"name": f"{city} City Center", "description": f"The vibrant heart of {city}.", "entry_fee": "Free", "duration_hours": 2, "best_time": "daytime"},
166
- {"name": f"{city} Main Square", "description": f"The central gathering place in {city}.", "entry_fee": "Free", "duration_hours": 1.5, "best_time": "afternoon"}
 
 
167
  ]
168
 
169
  def get_attractions(city: str) -> Dict:
170
  """Get attractions from cache or fetch new ones."""
171
  city_clean = city.strip().lower()
172
  if city_clean in ATTRACTIONS_CACHE:
173
- return {"city": city, "attractions": ATTRACTIONS_CACHE[city_clean], "source": "cache"}
 
174
  attractions = fetch_attractions_via_openai(city)
175
- return {"city": city, "attractions": attractions, "source": "OpenAI"}
176
 
177
  # -------------------- BUDGET CALCULATION --------------------
178
  def calculate_budget(num_days: int, budget_amount: float = None) -> Dict:
@@ -194,11 +245,13 @@ def calculate_budget(num_days: int, budget_amount: float = None) -> Dict:
194
  else:
195
  level = "moderate"
196
  daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
 
197
  accommodation = daily_rates["accommodation"] * num_days
198
  food = daily_rates["food"] * num_days
199
  transport = daily_rates["transport"] * num_days
200
  activities = daily_rates["activities"] * num_days
201
  total = accommodation + food + transport + activities
 
202
  return {
203
  "level": level,
204
  "accommodation": accommodation,
@@ -208,10 +261,11 @@ def calculate_budget(num_days: int, budget_amount: float = None) -> Dict:
208
  "total": total,
209
  "daily": daily_rates
210
  }
 
211
  # -------------------- ITINERARY GENERATION --------------------
212
  def generate_itinerary(destination: str, start_date: str, num_days: int,
213
  budget_amount: float, budget_currency: str, departure_city: str = ""):
214
- """Main itinerary generation function with beautiful formatting."""
215
  try:
216
  # Validate inputs
217
  if not destination:
@@ -235,7 +289,7 @@ def generate_itinerary(destination: str, start_date: str, num_days: int,
235
  start = datetime.strptime(start_date, "%Y-%m-%d")
236
  end = start + timedelta(days=int(num_days) - 1)
237
 
238
- # Generate beautiful HTML itinerary with images
239
  html = generate_beautiful_itinerary(
240
  destination, weather, attractions, budget_data,
241
  num_days, start, end, budget_amount, budget_currency, departure_city
@@ -249,7 +303,7 @@ def generate_itinerary(destination: str, start_date: str, num_days: int,
249
  def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
250
  num_days, start_date, end_date, budget_amount,
251
  budget_currency, departure_city):
252
- """Create a stunning, visually appealing itinerary with images."""
253
 
254
  # Weather details
255
  weather_temp = f"{weather['temperature']:.1f}Β°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
@@ -261,41 +315,48 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
261
  budget_warning = f"""
262
  <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; border-radius: 8px; margin: 15px 0;">
263
  <strong>⚠️ Budget Alert:</strong> Estimated costs (${budget_data['total']:.0f}) exceed your budget (${budget_amount:.0f}).
264
- Consider reducing days or choosing budget-friendly options like street food and free attractions.
265
  </div>
266
  """
267
 
268
- # Attractions list with images
269
  attractions_html = ""
270
  for attr in attractions[:6]:
271
- time_icon = "πŸŒ…" if attr.get('best_time') == "morning" else "β˜€οΈ" if attr.get('best_time') == "afternoon" else "πŸŒ™"
272
- # Get image for this attraction
273
- img_url = get_attraction_image(attr['name'], destination)
274
 
275
- attractions_html += f"""
276
- <div style="background: white; border-radius: 12px; margin-bottom: 20px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.3s;">
277
- <div style="display: flex; flex-wrap: wrap;">
278
- <div style="flex: 0 0 200px; overflow: hidden;">
279
- <img src="{img_url}" style="width: 100%; height: 150px; object-fit: cover;" alt="{attr['name']}">
280
- </div>
281
- <div style="flex: 1; padding: 15px;">
282
- <div style="display: flex; justify-content: space-between; align-items: start;">
283
- <div style="flex: 1;">
284
- <strong style="font-size: 1.1em; color: #333;">πŸ“ {attr['name']}</strong>
285
- <div style="color: #666; margin: 8px 0; font-size: 0.95em;">{attr['description']}</div>
286
- <div style="display: flex; gap: 15px; margin-top: 8px; font-size: 0.85em;">
287
- <span>⏱️ {attr['duration_hours']} hrs</span>
288
- <span>🎟️ {attr['entry_fee']}</span>
289
- <span>{time_icon} Best: {attr.get('best_time', 'anytime')}</span>
290
- </div>
291
  </div>
292
  </div>
293
  </div>
294
  </div>
295
- </div>
296
- """
 
 
 
 
 
 
 
 
 
 
 
297
 
298
- # Daily itinerary with images
299
  daily_html = ""
300
  per_day = max(1, len(attractions) // max(1, num_days))
301
 
@@ -317,8 +378,10 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
317
  """
318
 
319
  for attr in day_attractions:
320
- img_url = get_attraction_image(attr['name'], destination)
321
- daily_html += f"""
 
 
322
  <div style="display: flex; gap: 15px; margin-bottom: 20px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
323
  <img src="{img_url}" style="width: 80px; height: 80px; object-fit: cover; border-radius: 8px;" alt="{attr['name']}">
324
  <div style="flex: 1;">
@@ -329,16 +392,22 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
329
  </div>
330
  </div>
331
  </div>
332
- """
 
 
 
 
 
 
 
 
 
 
333
 
334
  daily_html += f"""
335
  <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
336
- <div style="margin-bottom: 10px;">
337
- <span style="font-size: 1.1em;">🍽️</span> <strong>Lunch Recommendation:</strong> Try authentic local cuisine at a nearby restaurant
338
- </div>
339
- <div>
340
- <span style="font-size: 1.1em;">πŸŒ™</span> <strong>Evening Activity:</strong> Explore local markets, enjoy a cultural show, or relax at a cafe
341
- </div>
342
  </div>
343
  </div>
344
  </div>
@@ -352,57 +421,38 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
352
  <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white; margin: 20px 0;">
353
  <h3 style="margin: 0 0 15px 0;">πŸ’° Budget Breakdown ({level_display} Travel Style)</h3>
354
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px;">
355
- <div>
356
- <strong>🏨 Accommodation</strong><br>
357
- ${budget_data['accommodation']:.0f}<br>
358
- <small>(${budget_data['daily']['accommodation']}/day)</small>
359
- </div>
360
- <div>
361
- <strong>🍽️ Food & Dining</strong><br>
362
- ${budget_data['food']:.0f}<br>
363
- <small>(${budget_data['daily']['food']}/day)</small>
364
- </div>
365
- <div>
366
- <strong>πŸš— Local Transport</strong><br>
367
- ${budget_data['transport']:.0f}<br>
368
- <small>(${budget_data['daily']['transport']}/day)</small>
369
- </div>
370
- <div>
371
- <strong>🎟️ Activities & Tours</strong><br>
372
- ${budget_data['activities']:.0f}<br>
373
- <small>(${budget_data['daily']['activities']}/day)</small>
374
- </div>
375
  <div style="border-top: 2px solid rgba(255,255,255,0.3); padding-top: 10px; grid-column: 1/-1;">
376
- <strong>πŸ’° Total Estimated Cost</strong><br>
377
- <span style="font-size: 1.2em;">${budget_data['total']:.0f}</span>
378
  {f" (Your budget: ${budget_amount:.0f})" if budget_amount else ""}
379
  </div>
380
  </div>
381
  </div>
382
  """
383
 
384
- # Travel tips with icons
385
  tips_html = """
386
  <div style="background: #f0f4ff; padding: 20px; border-radius: 12px; margin: 20px 0;">
387
  <h3 style="margin: 0 0 15px 0; color: #667eea;">πŸ’‘ Smart Travel Tips</h3>
388
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
389
- <div>🎫 <strong>Book in Advance</strong><br>Save time and money by booking popular attractions online</div>
390
- <div>πŸš‡ <strong>Public Transport</strong><br>Get a day pass for unlimited travel and better savings</div>
391
- <div>πŸ“± <strong>Offline Maps</strong><br>Download Google Maps offline to navigate without data</div>
392
- <div>πŸ’΅ <strong>Local Currency</strong><br>Carry cash for markets and small vendors</div>
393
- <div>🌍 <strong>Learn Basic Phrases</strong><br>A few local words go a long way with locals</div>
394
- <div>πŸ“Έ <strong>Early Bird</strong><br>Visit popular spots early morning to avoid crowds</div>
395
  </div>
396
  </div>
397
  """
398
 
399
  # Complete HTML
400
  full_html = f"""
401
- <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; max-width: 100%;">
402
  <!-- Hero Section -->
403
  <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 20px; color: white; margin-bottom: 30px; text-align: center;">
404
  <h1 style="margin: 0; font-size: 2.5em;">🌍 {destination}</h1>
405
- <p style="margin: 10px 0 0; opacity: 0.9; font-size: 1.1em;">
406
  {num_days} Days of Adventure β€’ {start_date.strftime('%B %d')} - {end_date.strftime('%B %d, %Y')}
407
  </p>
408
  {f"<p style='margin: 5px 0 0; opacity: 0.8;'>✈️ From: {departure_city}</p>" if departure_city else ""}
@@ -437,13 +487,13 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
437
  <!-- Budget Section -->
438
  {budget_html}
439
 
440
- <!-- Top Attractions with Images -->
441
  <div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
442
  <h2 style="margin: 0 0 15px 0; color: #667eea;">✨ Top Attractions in {destination}</h2>
443
  {attractions_html}
444
  </div>
445
 
446
- <!-- Daily Itinerary with Images -->
447
  <div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
448
  <h2 style="margin: 0 0 15px 0; color: #667eea;">πŸ“… Your {num_days}-Day Itinerary</h2>
449
  {daily_html}
@@ -456,16 +506,15 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
456
  <div style="text-align: center; padding: 20px; margin-top: 20px; background: #f8f9fa; border-radius: 12px;">
457
  <h3 style="margin: 0 0 15px 0;">Ready to Book Your Trip?</h3>
458
  <p>
459
- πŸ›οΈ <a href="https://www.booking.com/searchresults.html?ss={destination.replace(' ', '+')}" target="_blank" style="color: #667eea; text-decoration: none;">Search Hotels</a> |
460
- ✈️ <a href="https://www.skyscanner.net/transport/flights/{departure_city.lower() if departure_city else ''}/{destination.lower()}/" target="_blank" style="color: #667eea; text-decoration: none;">Search Flights</a> |
461
- 🎟️ <a href="https://www.tripadvisor.com/Search?q={destination}" target="_blank" style="color: #667eea; text-decoration: none;">Read Reviews</a>
462
  </p>
463
  </div>
464
 
465
  <!-- Footer -->
466
  <div style="text-align: center; padding: 20px; margin-top: 20px; font-size: 0.85em; color: #666;">
467
- <p>✨ TravelBuddy AI β€’ Powered by OpenAI β€’ Real-time Weather β€’ Beautiful Images β€’ Smart Recommendations</p>
468
- <p>🌍 Plan your perfect adventure with confidence</p>
469
  </div>
470
  </div>
471
  """
@@ -475,7 +524,7 @@ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
475
  # -------------------- GRADIO INTERFACE --------------------
476
  css = """
477
  .gradio-container {
478
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
479
  max-width: 1200px;
480
  margin: 0 auto;
481
  }
@@ -494,18 +543,14 @@ input, select, textarea {
494
  border-radius: 8px !important;
495
  border: 1px solid #e0e0e0 !important;
496
  }
497
- label {
498
- font-weight: 500 !important;
499
- color: #333 !important;
500
- }
501
  """
502
 
503
  with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as demo:
504
  gr.HTML("""
505
  <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 20px; margin-bottom: 30px;">
506
  <h1 style="color: white; margin: 0; font-size: 2.5em;">✨ TravelBuddy AI</h1>
507
- <p style="color: white; margin: 10px 0 0; opacity: 0.95; font-size: 1.1em;">
508
- Your Intelligent Travel Companion - Create Beautiful, Personalized Itineraries for Any Destination Worldwide
509
  </p>
510
  </div>
511
  """)
@@ -516,7 +561,7 @@ with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as de
516
  gr.Markdown("### 🎯 Where's Your Next Adventure?")
517
  destination = gr.Textbox(
518
  label="Destination",
519
- placeholder="e.g., Paris, Tokyo, New York, Bali, Cape Town...",
520
  lines=1,
521
  show_label=False
522
  )
@@ -547,13 +592,13 @@ with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as de
547
  value=None
548
  )
549
  budget_currency = gr.Dropdown(
550
- ["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF"],
551
  label="Currency",
552
  value="USD"
553
  )
554
  gr.HTML("""
555
  <div style="background: #e8f0fe; padding: 10px; border-radius: 8px; margin-top: 10px;">
556
- <small>πŸ’‘ <strong>Smart Tip:</strong> We'll automatically suggest the best travel style based on your budget and trip duration!</small>
557
  </div>
558
  """)
559
 
@@ -561,7 +606,7 @@ with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as de
561
  gr.Markdown("### ✈️ Departure Info")
562
  departure_city = gr.Textbox(
563
  label="Departure City (Optional)",
564
- placeholder="e.g., New York, London, Sydney",
565
  lines=1
566
  )
567
 
@@ -579,24 +624,20 @@ with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as de
579
  # Examples section
580
  gr.HTML("""
581
  <div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee;">
582
- <h3>🌟 Popular Destinations to Try</h3>
583
  <div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
584
  <button onclick="document.querySelector('#destination input').value='Paris';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πŸ—Ό Paris</button>
 
585
  <button onclick="document.querySelector('#destination input').value='Tokyo';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πŸ—Ύ Tokyo</button>
586
  <button onclick="document.querySelector('#destination input').value='New York';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πŸ—½ New York</button>
587
- <button onclick="document.querySelector('#destination input').value='Bali';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">🏝️ Bali</button>
588
  <button onclick="document.querySelector('#destination input').value='Rome';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πŸ›οΈ Rome</button>
589
- <button onclick="document.querySelector('#destination input').value='Bangkok';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">🍜 Bangkok</button>
590
  </div>
591
  </div>
592
 
593
  <div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee; color: #666;">
594
- <small>Powered by OpenAI, Weather API, Unsplash β€’ Smart travel planning with beautiful visuals ✨</small>
595
  </div>
596
  """)
597
 
598
  if __name__ == "__main__":
599
- demo.launch(share=False, server_name="0.0.0.0")
600
-
601
-
602
-
 
1
+ import os
2
  import json
3
  import requests
4
  from datetime import datetime, timedelta
5
+ from functools import lru_cache
6
  import gradio as gr
7
  import openai
8
+ import re
9
  from typing import List, Dict, Any
10
 
11
  # -------------------- CONFIGURATION --------------------
12
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
13
  OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
14
+ SERP_API_KEY = os.getenv("SERPAPI_API_KEY", "")
15
 
16
  if not OPENAI_API_KEY:
17
  print("⚠️ OPENAI_API_KEY not set. AI itinerary generation will fall back to manual mode.")
 
22
  # Global attractions cache
23
  ATTRACTIONS_CACHE = {}
24
 
25
+ # Real images for famous attractions (only where we have verified images)
26
+ REAL_IMAGES = {
27
+ # Paris
28
+ "eiffel tower": "https://images.unsplash.com/photo-1543349689-9a4d426bee8e?w=800&h=500&fit=crop",
29
+ "louvre museum": "https://images.unsplash.com/photo-1564910443436-deafeb5e7d6c?w=800&h=500&fit=crop",
30
+ "notre-dame cathedral": "https://images.unsplash.com/photo-1491336477066-31156b5e4f35?w=800&h=500&fit=crop",
31
+ "arc de triomphe": "https://images.unsplash.com/photo-1566566108883-476d3293f1d3?w=800&h=500&fit=crop",
32
+ "seine river": "https://images.unsplash.com/photo-1493707553966-283afac8c358?w=800&h=500&fit=crop",
33
+ "montmartre": "https://images.unsplash.com/photo-1522093007474-d86e9bf7ba6f?w=800&h=500&fit=crop",
34
+
35
+ # Kigali
36
+ "kigali genocide memorial": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Kigali_Genocide_Memorial_03.jpg/800px-Kigali_Genocide_Memorial_03.jpg",
37
+ "inema arts center": "https://media-cdn.tripadvisor.com/media/photo-s/0e/9a/3f/8c/inema-arts-center.jpg",
38
+
39
+ # Tokyo
40
+ "senso-ji temple": "https://images.unsplash.com/photo-1564507592333-c60657eea523?w=800&h=500&fit=crop",
41
+ "tokyo tower": "https://images.unsplash.com/photo-1542051841857-5f90071e7989?w=800&h=500&fit=crop",
42
+ "shibuya crossing": "https://images.unsplash.com/photo-1542051841857-5f90071e7989?w=800&h=500&fit=crop",
43
+
44
+ # New York
45
+ "statue of liberty": "https://images.unsplash.com/photo-1505765050516-f72a3e10db15?w=800&h=500&fit=crop",
46
+ "times square": "https://images.unsplash.com/photo-1485871981521-5b1fd3805eee?w=800&h=500&fit=crop",
47
+ "central park": "https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&h=500&fit=crop",
48
+
49
+ # London
50
+ "big ben": "https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?w=800&h=500&fit=crop",
51
+ "london eye": "https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?w=800&h=500&fit=crop",
52
+ "tower bridge": "https://images.unsplash.com/photo-1529655683826-aba9b3e77383?w=800&h=500&fit=crop",
53
+
54
+ # Rome
55
+ "colosseum": "https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=800&h=500&fit=crop",
56
+ "vatican": "https://images.unsplash.com/photo-1549294413-26f195200c16?w=800&h=500&fit=crop",
57
+ "trevi fountain": "https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=800&h=500&fit=crop",
58
+ }
59
 
60
+ def get_attraction_image(attraction_name: str) -> str:
61
+ """Get real image URL if available, otherwise return None."""
62
+ name_lower = attraction_name.lower()
63
+
64
+ # Check exact match or partial match
65
+ for key, url in REAL_IMAGES.items():
66
+ if key in name_lower:
67
+ return url
68
+
69
+ # No image found
70
+ return None
71
 
72
  # -------------------- WEATHER FUNCTION --------------------
73
  def get_weather(city: str) -> dict:
 
104
 
105
  # -------------------- ATTRACTIONS FUNCTIONS --------------------
106
  def fetch_attractions_via_openai(city: str) -> List[Dict]:
107
+ """Fetch attractions for ANY city using OpenAI."""
108
  if not client:
109
  return get_fallback_attractions(city)
110
 
111
  try:
112
  prompt = f"""
113
+ List the top 6 must-visit tourist attractions in {city}. For each attraction, provide:
114
+ 1. Name (real attraction name)
 
115
  2. A vivid, engaging 2-sentence description
116
+ 3. Entry fee (in USD, use 0 for free)
117
  4. Visit duration in hours
 
118
 
119
+ Return ONLY a JSON array with keys: name, description, entry_fee, duration_hours.
120
+
121
+ Example:
122
+ [
123
+ {{
124
+ "name": "Eiffel Tower",
125
+ "description": "Iconic iron lattice tower offering breathtaking panoramic views of Paris.",
126
+ "entry_fee": 25,
127
+ "duration_hours": 2.5
128
+ }}
129
+ ]
130
+
131
+ Make sure to use REAL attractions specific to {city}.
132
  """
133
  response = client.chat.completions.create(
134
  model="gpt-3.5-turbo",
 
136
  temperature=0.7,
137
  max_tokens=1500
138
  )
139
+ content = response.choices[0].message.content
140
+
141
+ # Clean the response
142
+ content = content.strip()
143
  if content.startswith('```json'):
144
  content = content[7:]
145
  if content.startswith('```'):
146
  content = content[3:]
147
  if content.endswith('```'):
148
  content = content[:-3]
149
+
150
  attractions = json.loads(content)
151
+
152
  if isinstance(attractions, dict) and "attractions" in attractions:
153
  attractions = attractions["attractions"]
154
  elif not isinstance(attractions, list):
155
  attractions = [attractions]
156
+
157
  result = []
158
+ for a in attractions[:6]:
159
  fee = a.get("entry_fee", 0)
160
+ if isinstance(fee, (int, float)):
161
+ if fee == 0:
162
+ fee_display = "Free"
163
+ else:
164
+ fee_display = f"${fee}"
165
+ else:
166
+ fee_display = str(fee)
167
+
168
  result.append({
169
  "name": a.get("name", "Unknown"),
170
  "entry_fee": fee_display,
171
  "duration_hours": a.get("duration_hours", 2),
172
  "description": a.get("description", f"A must-visit attraction in {city}"),
 
173
  })
174
+
175
+ # Cache the results
176
+ ATTRACTIONS_CACHE[city] = result
177
  return result
178
+
179
  except Exception as e:
180
  print(f"OpenAI attractions error: {e}")
181
  return get_fallback_attractions(city)
 
183
  def get_fallback_attractions(city: str) -> List[Dict]:
184
  """Provide fallback attractions for any city."""
185
  city_lower = city.lower()
186
+
187
+ # Real attractions for major cities
188
  real_attractions = {
189
+ "kigali": [
190
+ {"name": "Kigali Genocide Memorial", "description": "A powerful memorial and museum honoring the victims of the 1994 genocide, with exhibits and gardens.", "entry_fee": "Free", "duration_hours": 3},
191
+ {"name": "Inema Arts Center", "description": "Vibrant contemporary art gallery showcasing Rwandan artists with colorful paintings and sculptures.", "entry_fee": "Free", "duration_hours": 2},
192
+ {"name": "Mount Kigali", "description": "The highest point in Kigali offering panoramic views of the city and surrounding hills.", "entry_fee": "Free", "duration_hours": 3},
193
+ {"name": "Kimironko Market", "description": "Kigali's largest local market where you can experience daily life and buy fresh produce.", "entry_fee": "Free", "duration_hours": 2},
194
+ {"name": "Nyamirambo Women's Center", "description": "Community center offering walking tours and cultural experiences supporting local women.", "entry_fee": "$20", "duration_hours": 3},
195
+ {"name": "Presidential Palace Museum", "description": "Former presidential residence with fascinating history and exhibits.", "entry_fee": "$5", "duration_hours": 2}
196
  ],
197
+ "paris": [
198
+ {"name": "Eiffel Tower", "description": "Iconic iron lattice tower offering breathtaking panoramic views of Paris.", "entry_fee": "$25", "duration_hours": 2.5},
199
+ {"name": "Louvre Museum", "description": "World's largest art museum, home to the Mona Lisa and thousands of artworks.", "entry_fee": "$20", "duration_hours": 4},
200
+ {"name": "Notre-Dame Cathedral", "description": "Magnificent Gothic cathedral with stunning architecture and history.", "entry_fee": "Free", "duration_hours": 1.5},
201
+ {"name": "Montmartre", "description": "Charming hilltop village with artists' square and SacrΓ©-CΕ“ur basilica.", "entry_fee": "Free", "duration_hours": 3},
202
+ {"name": "Seine River Cruise", "description": "Relaxing boat tour passing by Paris's most famous landmarks.", "entry_fee": "$15", "duration_hours": 1},
203
+ {"name": "Arc de Triomphe", "description": "Monumental arch honoring French soldiers with panoramic city views.", "entry_fee": "$13", "duration_hours": 2}
204
  ]
205
  }
206
+
207
  for key, attractions in real_attractions.items():
208
  if key in city_lower:
209
  return attractions
210
+
211
+ # Generic attractions
212
  return [
213
+ {"name": f"{city} City Center", "description": f"The vibrant heart of {city} with historic architecture and charming cafes.", "entry_fee": "Free", "duration_hours": 2},
214
+ {"name": f"{city} Main Market", "description": f"Experience local life at this vibrant market with fresh produce and crafts.", "entry_fee": "Free", "duration_hours": 1.5},
215
+ {"name": f"{city} Cathedral", "description": f"A magnificent religious site showcasing stunning architecture.", "entry_fee": "Free", "duration_hours": 1},
216
+ {"name": f"{city} Museum", "description": f"Discover the rich cultural heritage and artistic treasures of {city}.", "entry_fee": "$12", "duration_hours": 2}
217
  ]
218
 
219
  def get_attractions(city: str) -> Dict:
220
  """Get attractions from cache or fetch new ones."""
221
  city_clean = city.strip().lower()
222
  if city_clean in ATTRACTIONS_CACHE:
223
+ return {"city": city, "attractions": ATTRACTIONS_CACHE[city_clean]}
224
+
225
  attractions = fetch_attractions_via_openai(city)
226
+ return {"city": city, "attractions": attractions}
227
 
228
  # -------------------- BUDGET CALCULATION --------------------
229
  def calculate_budget(num_days: int, budget_amount: float = None) -> Dict:
 
245
  else:
246
  level = "moderate"
247
  daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
248
+
249
  accommodation = daily_rates["accommodation"] * num_days
250
  food = daily_rates["food"] * num_days
251
  transport = daily_rates["transport"] * num_days
252
  activities = daily_rates["activities"] * num_days
253
  total = accommodation + food + transport + activities
254
+
255
  return {
256
  "level": level,
257
  "accommodation": accommodation,
 
261
  "total": total,
262
  "daily": daily_rates
263
  }
264
+
265
  # -------------------- ITINERARY GENERATION --------------------
266
  def generate_itinerary(destination: str, start_date: str, num_days: int,
267
  budget_amount: float, budget_currency: str, departure_city: str = ""):
268
+ """Main itinerary generation function."""
269
  try:
270
  # Validate inputs
271
  if not destination:
 
289
  start = datetime.strptime(start_date, "%Y-%m-%d")
290
  end = start + timedelta(days=int(num_days) - 1)
291
 
292
+ # Generate beautiful HTML itinerary
293
  html = generate_beautiful_itinerary(
294
  destination, weather, attractions, budget_data,
295
  num_days, start, end, budget_amount, budget_currency, departure_city
 
303
  def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
304
  num_days, start_date, end_date, budget_amount,
305
  budget_currency, departure_city):
306
+ """Create a stunning itinerary with real images when available."""
307
 
308
  # Weather details
309
  weather_temp = f"{weather['temperature']:.1f}Β°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
 
315
  budget_warning = f"""
316
  <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; border-radius: 8px; margin: 15px 0;">
317
  <strong>⚠️ Budget Alert:</strong> Estimated costs (${budget_data['total']:.0f}) exceed your budget (${budget_amount:.0f}).
318
+ Consider reducing days or choosing budget-friendly options.
319
  </div>
320
  """
321
 
322
+ # Attractions list with images (only if real image exists)
323
  attractions_html = ""
324
  for attr in attractions[:6]:
325
+ img_url = get_attraction_image(attr['name'])
 
 
326
 
327
+ if img_url:
328
+ # With image
329
+ attractions_html += f"""
330
+ <div style="background: white; border-radius: 12px; margin-bottom: 20px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
331
+ <div style="display: flex; flex-wrap: wrap;">
332
+ <div style="flex: 0 0 200px;">
333
+ <img src="{img_url}" style="width: 100%; height: 150px; object-fit: cover;" alt="{attr['name']}">
334
+ </div>
335
+ <div style="flex: 1; padding: 15px;">
336
+ <strong style="font-size: 1.1em; color: #333;">πŸ“ {attr['name']}</strong>
337
+ <div style="color: #666; margin: 8px 0; font-size: 0.95em;">{attr['description']}</div>
338
+ <div style="display: flex; gap: 15px; margin-top: 8px; font-size: 0.85em;">
339
+ <span>⏱️ {attr['duration_hours']} hrs</span>
340
+ <span>🎟️ {attr['entry_fee']}</span>
 
 
341
  </div>
342
  </div>
343
  </div>
344
  </div>
345
+ """
346
+ else:
347
+ # Without image
348
+ attractions_html += f"""
349
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 12px; border-left: 3px solid #667eea;">
350
+ <strong style="font-size: 1.1em; color: #333;">πŸ“ {attr['name']}</strong>
351
+ <div style="color: #666; margin: 8px 0; font-size: 0.95em;">{attr['description']}</div>
352
+ <div style="display: flex; gap: 15px; margin-top: 8px; font-size: 0.85em;">
353
+ <span>⏱️ {attr['duration_hours']} hrs</span>
354
+ <span>🎟️ {attr['entry_fee']}</span>
355
+ </div>
356
+ </div>
357
+ """
358
 
359
+ # Daily itinerary with images (only if real image exists)
360
  daily_html = ""
361
  per_day = max(1, len(attractions) // max(1, num_days))
362
 
 
378
  """
379
 
380
  for attr in day_attractions:
381
+ img_url = get_attraction_image(attr['name'])
382
+
383
+ if img_url:
384
+ daily_html += f"""
385
  <div style="display: flex; gap: 15px; margin-bottom: 20px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
386
  <img src="{img_url}" style="width: 80px; height: 80px; object-fit: cover; border-radius: 8px;" alt="{attr['name']}">
387
  <div style="flex: 1;">
 
392
  </div>
393
  </div>
394
  </div>
395
+ """
396
+ else:
397
+ daily_html += f"""
398
+ <div style="margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
399
+ <strong>✨ {attr['name']}</strong><br>
400
+ <span style="font-size: 0.85em; color: #666;">{attr['description'][:100]}...</span>
401
+ <div style="font-size: 0.75em; color: #888; margin-top: 5px;">
402
+ ⏱️ {attr['duration_hours']} hrs | 🎟️ {attr['entry_fee']}
403
+ </div>
404
+ </div>
405
+ """
406
 
407
  daily_html += f"""
408
  <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
409
+ <div><span style="font-size: 1.1em;">🍽️</span> <strong>Lunch:</strong> Try authentic local cuisine</div>
410
+ <div style="margin-top: 8px;"><span style="font-size: 1.1em;">πŸŒ™</span> <strong>Evening:</strong> Explore local markets or enjoy cultural entertainment</div>
 
 
 
 
411
  </div>
412
  </div>
413
  </div>
 
421
  <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white; margin: 20px 0;">
422
  <h3 style="margin: 0 0 15px 0;">πŸ’° Budget Breakdown ({level_display} Travel Style)</h3>
423
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px;">
424
+ <div><strong>🏨 Accommodation</strong><br>${budget_data['accommodation']:.0f}<br><small>(${budget_data['daily']['accommodation']}/day)</small></div>
425
+ <div><strong>🍽️ Food & Dining</strong><br>${budget_data['food']:.0f}<br><small>(${budget_data['daily']['food']}/day)</small></div>
426
+ <div><strong>πŸš— Local Transport</strong><br>${budget_data['transport']:.0f}<br><small>(${budget_data['daily']['transport']}/day)</small></div>
427
+ <div><strong>🎟️ Activities & Tours</strong><br>${budget_data['activities']:.0f}<br><small>(${budget_data['daily']['activities']}/day)</small></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  <div style="border-top: 2px solid rgba(255,255,255,0.3); padding-top: 10px; grid-column: 1/-1;">
429
+ <strong>πŸ’° Total Estimated Cost</strong><br><span style="font-size: 1.2em;">${budget_data['total']:.0f}</span>
 
430
  {f" (Your budget: ${budget_amount:.0f})" if budget_amount else ""}
431
  </div>
432
  </div>
433
  </div>
434
  """
435
 
436
+ # Travel tips
437
  tips_html = """
438
  <div style="background: #f0f4ff; padding: 20px; border-radius: 12px; margin: 20px 0;">
439
  <h3 style="margin: 0 0 15px 0; color: #667eea;">πŸ’‘ Smart Travel Tips</h3>
440
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
441
+ <div>🎫 <strong>Book in Advance</strong><br>Save time and money</div>
442
+ <div>πŸš‡ <strong>Public Transport</strong><br>Get day passes for savings</div>
443
+ <div>πŸ“± <strong>Offline Maps</strong><br>Download before you go</div>
444
+ <div>πŸ’΅ <strong>Local Currency</strong><br>Carry cash for markets</div>
 
 
445
  </div>
446
  </div>
447
  """
448
 
449
  # Complete HTML
450
  full_html = f"""
451
+ <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 100%;">
452
  <!-- Hero Section -->
453
  <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 20px; color: white; margin-bottom: 30px; text-align: center;">
454
  <h1 style="margin: 0; font-size: 2.5em;">🌍 {destination}</h1>
455
+ <p style="margin: 10px 0 0; opacity: 0.9;">
456
  {num_days} Days of Adventure β€’ {start_date.strftime('%B %d')} - {end_date.strftime('%B %d, %Y')}
457
  </p>
458
  {f"<p style='margin: 5px 0 0; opacity: 0.8;'>✈️ From: {departure_city}</p>" if departure_city else ""}
 
487
  <!-- Budget Section -->
488
  {budget_html}
489
 
490
+ <!-- Top Attractions -->
491
  <div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
492
  <h2 style="margin: 0 0 15px 0; color: #667eea;">✨ Top Attractions in {destination}</h2>
493
  {attractions_html}
494
  </div>
495
 
496
+ <!-- Daily Itinerary -->
497
  <div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
498
  <h2 style="margin: 0 0 15px 0; color: #667eea;">πŸ“… Your {num_days}-Day Itinerary</h2>
499
  {daily_html}
 
506
  <div style="text-align: center; padding: 20px; margin-top: 20px; background: #f8f9fa; border-radius: 12px;">
507
  <h3 style="margin: 0 0 15px 0;">Ready to Book Your Trip?</h3>
508
  <p>
509
+ πŸ›οΈ <a href="https://www.booking.com/searchresults.html?ss={destination.replace(' ', '+')}" target="_blank" style="color: #667eea;">Search Hotels</a> |
510
+ ✈️ <a href="https://www.skyscanner.net/" target="_blank" style="color: #667eea;">Search Flights</a> |
511
+ 🎟️ <a href="https://www.tripadvisor.com/Search?q={destination}" target="_blank" style="color: #667eea;">Read Reviews</a>
512
  </p>
513
  </div>
514
 
515
  <!-- Footer -->
516
  <div style="text-align: center; padding: 20px; margin-top: 20px; font-size: 0.85em; color: #666;">
517
+ <p>✨ TravelBuddy AI β€’ Powered by OpenAI β€’ Real-time Weather β€’ Smart Recommendations</p>
 
518
  </div>
519
  </div>
520
  """
 
524
  # -------------------- GRADIO INTERFACE --------------------
525
  css = """
526
  .gradio-container {
527
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
528
  max-width: 1200px;
529
  margin: 0 auto;
530
  }
 
543
  border-radius: 8px !important;
544
  border: 1px solid #e0e0e0 !important;
545
  }
 
 
 
 
546
  """
547
 
548
  with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as demo:
549
  gr.HTML("""
550
  <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 20px; margin-bottom: 30px;">
551
  <h1 style="color: white; margin: 0; font-size: 2.5em;">✨ TravelBuddy AI</h1>
552
+ <p style="color: white; margin: 10px 0 0; opacity: 0.95;">
553
+ Your Intelligent Travel Companion - Create Beautiful, Personalized Itineraries for Any Destination
554
  </p>
555
  </div>
556
  """)
 
561
  gr.Markdown("### 🎯 Where's Your Next Adventure?")
562
  destination = gr.Textbox(
563
  label="Destination",
564
+ placeholder="e.g., Paris, Kigali, Tokyo, New York, Bali...",
565
  lines=1,
566
  show_label=False
567
  )
 
592
  value=None
593
  )
594
  budget_currency = gr.Dropdown(
595
+ ["USD", "EUR", "GBP", "JPY", "CAD", "AUD"],
596
  label="Currency",
597
  value="USD"
598
  )
599
  gr.HTML("""
600
  <div style="background: #e8f0fe; padding: 10px; border-radius: 8px; margin-top: 10px;">
601
+ <small>πŸ’‘ <strong>Smart Tip:</strong> We'll suggest the best travel style based on your budget!</small>
602
  </div>
603
  """)
604
 
 
606
  gr.Markdown("### ✈️ Departure Info")
607
  departure_city = gr.Textbox(
608
  label="Departure City (Optional)",
609
+ placeholder="e.g., New York, London",
610
  lines=1
611
  )
612
 
 
624
  # Examples section
625
  gr.HTML("""
626
  <div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee;">
627
+ <h3>🌟 Try These Destinations</h3>
628
  <div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
629
  <button onclick="document.querySelector('#destination input').value='Paris';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πŸ—Ό Paris</button>
630
+ <button onclick="document.querySelector('#destination input').value='Kigali';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">🌍 Kigali</button>
631
  <button onclick="document.querySelector('#destination input').value='Tokyo';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πŸ—Ύ Tokyo</button>
632
  <button onclick="document.querySelector('#destination input').value='New York';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πŸ—½ New York</button>
 
633
  <button onclick="document.querySelector('#destination input').value='Rome';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">πŸ›οΈ Rome</button>
 
634
  </div>
635
  </div>
636
 
637
  <div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee; color: #666;">
638
+ <small>Powered by OpenAI, Weather API β€’ Smart travel planning for the modern explorer ✨</small>
639
  </div>
640
  """)
641
 
642
  if __name__ == "__main__":
643
+ demo.launch(share=False, server_name="0.0.0.0")