NSamson1 commited on
Commit
547cc10
·
verified ·
1 Parent(s): 86a862e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +538 -481
app.py CHANGED
@@ -2,7 +2,6 @@ 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
@@ -12,100 +11,117 @@ from typing import List, Dict, Any
12
  # -------------------- CONFIGURATION --------------------
13
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
14
  OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
15
- SERP_API_KEY = os.getenv("SERPAPI_API_KEY", "")
16
 
17
  if not OPENAI_API_KEY:
18
- print("⚠️ OPENAI_API_KEY not set. AI itinerary generation will fall back to manual mode.")
19
  client = None
20
  else:
21
  client = openai.OpenAI(api_key=OPENAI_API_KEY)
22
 
23
- # Global attractions cache
24
  ATTRACTIONS_CACHE = {}
25
-
26
- # Unsplash API for free images (optional but recommended)
27
- UNSPLASH_API_KEY = os.getenv("UNSPLASH_API_KEY", "")
28
-
29
- # Image cache to avoid repeated API calls
30
  IMAGE_CACHE = {}
31
 
32
- # -------------------- IMAGE FUNCTIONS --------------------
33
- def get_attraction_image(attraction_name: str, city: str) -> str:
34
- """Fetch a beautiful image for an attraction using Unsplash or fallback."""
35
- cache_key = f"{attraction_name}_{city}".lower()
36
-
37
- # Check cache first
 
 
 
38
  if cache_key in IMAGE_CACHE:
39
  return IMAGE_CACHE[cache_key]
40
-
41
- # Try Unsplash API if available
42
- if UNSPLASH_API_KEY:
 
 
 
 
 
43
  try:
44
- search_query = f"{attraction_name} {city} landmark"
45
- params = {
46
- "query": search_query,
47
- "client_id": UNSPLASH_API_KEY,
48
- "per_page": 1,
49
- "orientation": "landscape"
 
 
 
50
  }
51
- response = requests.get("https://api.unsplash.com/search/photos", params=params, timeout=5)
52
- if response.status_code == 200:
53
- data = response.json()
54
- if data.get("results") and len(data["results"]) > 0:
55
- img_url = data["results"][0]["urls"]["regular"]
56
- IMAGE_CACHE[cache_key] = img_url
57
- return img_url
58
- except:
59
- pass
60
-
61
- # High-quality fallback images for famous attractions
62
- famous_images = {
63
- "eiffel tower": "https://images.unsplash.com/photo-1543349689-9a4d426bee8e?w=800&h=500&fit=crop",
64
- "louvre museum": "https://images.unsplash.com/photo-1564910443436-deafeb5e7d6c?w=800&h=500&fit=crop",
65
- "notre-dame": "https://images.unsplash.com/photo-1491336477066-31156b5e4f35?w=800&h=500&fit=crop",
66
- "montmartre": "https://images.unsplash.com/photo-1522093007474-d86e9bf7ba6f?w=800&h=500&fit=crop",
67
- "sacré-cœur": "https://images.unsplash.com/photo-1522093007474-d86e9bf7ba6f?w=800&h=500&fit=crop",
68
- "seine river": "https://images.unsplash.com/photo-1493707553966-283afac8c358?w=800&h=500&fit=crop",
69
- "champs-élysées": "https://images.unsplash.com/photo-1508804185872-ad7fcd0ae6d9?w=800&h=500&fit=crop",
70
- "arc de triomphe": "https://images.unsplash.com/photo-1566566108883-476d3293f1d3?w=800&h=500&fit=crop",
71
- "tokyo tower": "https://images.unsplash.com/photo-1542051841857-5f90071e7989?w=800&h=500&fit=crop",
72
- "senso-ji": "https://images.unsplash.com/photo-1564507592333-c60657eea523?w=800&h=500&fit=crop",
73
- "shibuya": "https://images.unsplash.com/photo-1542051841857-5f90071e7989?w=800&h=500&fit=crop",
74
- }
75
-
76
- # Check if attraction name matches any famous landmarks
77
- attraction_lower = attraction_name.lower()
78
- for key, url in famous_images.items():
79
- if key in attraction_lower:
80
- IMAGE_CACHE[cache_key] = url
81
- return url
82
-
83
- # City-specific fallback images
84
- city_lower = city.lower()
85
- city_images = {
86
- "paris": "https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&h=500&fit=crop",
87
- "tokyo": "https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=800&h=500&fit=crop",
88
- "new york": "https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&h=500&fit=crop",
89
- "london": "https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?w=800&h=500&fit=crop",
90
- "rome": "https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=800&h=500&fit=crop",
91
- "bali": "https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=800&h=500&fit=crop",
92
- "bangkok": "https://images.unsplash.com/photo-1508009603885-50cf7c579365?w=800&h=500&fit=crop",
93
- }
94
-
95
- for key, url in city_images.items():
96
- if key in city_lower:
97
- IMAGE_CACHE[cache_key] = url
98
- return url
99
-
100
- # Generic beautiful travel image
101
- generic_images = [
102
- "https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800&h=500&fit=crop",
103
- "https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&h=500&fit=crop",
104
- "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=800&h=500&fit=crop"
105
- ]
106
- img_url = random.choice(generic_images)
107
- IMAGE_CACHE[cache_key] = img_url
108
- return img_url
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  # -------------------- WEATHER FUNCTION --------------------
111
  def get_weather(city: str) -> dict:
@@ -118,152 +134,199 @@ def get_weather(city: str) -> dict:
118
  "humidity": 60,
119
  "wind_speed": 10,
120
  "precipitation": 0,
121
- "note": "Demo mode - Add OpenWeather API key for real data"
122
  }
123
  try:
124
  url = "https://api.openweathermap.org/data/2.5/weather"
125
- params = {'q': city, 'appid': OPENWEATHER_API_KEY, 'units': 'metric'}
126
  response = requests.get(url, params=params, timeout=10)
127
  data = response.json()
128
  if response.status_code != 200:
129
  return {"error": f"Weather API error: {data.get('message', 'unknown')}"}
130
-
131
  return {
132
  "city": city,
133
- "temperature": data['main']['temp'],
134
- "feels_like": data['main']['feels_like'],
135
- "condition": data['weather'][0]['description'],
136
- "humidity": data['main']['humidity'],
137
- "wind_speed": data['wind']['speed'],
138
- "precipitation": data.get('rain', {}).get('1h', 0)
139
  }
140
  except Exception as e:
141
  return {"error": f"Weather service unavailable: {str(e)}"}
142
 
143
- # -------------------- ATTRACTIONS FUNCTIONS --------------------
 
144
  def fetch_attractions_via_openai(city: str) -> List[Dict]:
145
- """Fetch attractions for ANY city using OpenAI with rich descriptions."""
 
 
 
146
  if not client:
147
- return get_fallback_attractions(city)
148
-
149
- try:
150
- prompt = f"""
151
- You are a local expert in {city}. List the top 6-8 must-visit tourist attractions in {city}.
152
- For each attraction, provide:
153
- 1. Name (the actual famous attraction name, not generic)
154
- 2. A vivid, engaging 2-sentence description
155
- 3. Entry fee (in USD, use numbers only - 0 for free)
156
- 4. Visit duration in hours
157
- 5. Best time to visit (morning/afternoon/evening)
158
-
159
- Return ONLY a JSON array with these keys: name, description, entry_fee, duration_hours, best_time.
160
-
161
- Example for Paris:
162
  [
163
  {{
164
- "name": "Eiffel Tower",
165
- "description": "Iconic iron lattice tower offering breathtaking panoramic views of Paris. Visitors can take elevators or climb stairs to three observation levels, with spectacular sparkling lights every evening.",
166
- "entry_fee": 25,
167
- "duration_hours": 2.5,
168
- "best_time": "evening"
169
  }}
170
  ]
171
 
172
- Make sure to use REAL attractions specific to {city}.
173
  """
 
 
174
  response = client.chat.completions.create(
175
  model="gpt-3.5-turbo",
176
  messages=[{"role": "user", "content": prompt}],
177
- temperature=0.7,
178
- max_tokens=1500
179
  )
180
- content = response.choices[0].message.content
181
-
182
- # Clean the response
183
- content = content.strip()
184
- if content.startswith('```json'):
185
- content = content[7:]
186
- if content.startswith('```'):
187
- content = content[3:]
188
- if content.endswith('```'):
189
- content = content[:-3]
190
-
191
  attractions = json.loads(content)
192
-
193
- if isinstance(attractions, dict) and "attractions" in attractions:
194
- attractions = attractions["attractions"]
195
- elif not isinstance(attractions, list):
196
- attractions = [attractions]
197
-
198
  result = []
199
- for a in attractions[:8]:
200
  fee = a.get("entry_fee", 0)
201
- if isinstance(fee, (int, float)):
202
- if fee == 0:
203
- fee_display = "Free"
204
- else:
205
- fee_display = f"${fee}"
206
- else:
207
- fee_display = str(fee)
208
-
209
  result.append({
210
- "name": a.get("name", "Unknown"),
211
  "entry_fee": fee_display,
212
  "duration_hours": a.get("duration_hours", 2),
213
- "description": a.get("description", f"A must-visit attraction in {city}"),
214
- "best_time": a.get("best_time", "anytime")
215
  })
216
-
217
- # Cache the results
218
- ATTRACTIONS_CACHE[city] = result
 
 
 
 
 
 
 
 
 
219
  return result
220
-
221
  except Exception as e:
222
  print(f"OpenAI attractions error: {e}")
223
- return get_fallback_attractions(city)
224
-
225
- def get_fallback_attractions(city: str) -> List[Dict]:
226
- """Provide fallback attractions for any city."""
227
- city_lower = city.lower()
228
-
229
- # Real attractions for major cities
230
- real_attractions = {
 
 
 
 
 
 
 
 
 
231
  "paris": [
232
- {"name": "Eiffel Tower", "description": "Iconic iron lattice tower offering breathtaking panoramic views of Paris. A symbol of France and one of the most visited monuments in the world.", "entry_fee": "$25", "duration_hours": 2.5, "best_time": "evening"},
233
- {"name": "Louvre Museum", "description": "World's largest art museum and historic monument, home to the Mona Lisa and thousands of other priceless artworks.", "entry_fee": "$20", "duration_hours": 4, "best_time": "morning"},
234
- {"name": "Notre-Dame Cathedral", "description": "Magnificent Gothic cathedral known for its stunning architecture, flying buttresses, and beautiful rose windows.", "entry_fee": "Free", "duration_hours": 1.5, "best_time": "morning"},
235
- {"name": "Montmartre & Sacré-Cœur", "description": "Charming hilltop village with bohemian history, artists' square, and stunning white basilica with panoramic city views.", "entry_fee": "Free", "duration_hours": 3, "best_time": "afternoon"},
236
- {"name": "Seine River Cruise", "description": "Relaxing boat tour along the Seine River passing by Paris's most famous landmarks and bridges.", "entry_fee": "$15", "duration_hours": 1, "best_time": "sunset"},
237
- {"name": "Champs-Élysées & Arc de Triomphe", "description": "World-famous avenue lined with shops, cafes, and the monumental arch honoring French soldiers.", "entry_fee": "$13", "duration_hours": 2, "best_time": "afternoon"}
238
  ],
239
  "tokyo": [
240
- {"name": "Senso-ji Temple", "description": "Ancient Buddhist temple in Asakusa, Tokyo's oldest temple with a vibrant shopping street leading to it.", "entry_fee": "Free", "duration_hours": 2, "best_time": "morning"},
241
- {"name": "Shibuya Crossing", "description": "Famous pedestrian scramble crossing, symbol of Tokyo's energy and modernity.", "entry_fee": "Free", "duration_hours": 1, "best_time": "evening"},
242
- {"name": "Tokyo Tower", "description": "Iconic red and white tower offering observation decks with stunning city views.", "entry_fee": "$12", "duration_hours": 1.5, "best_time": "evening"},
243
- {"name": "Meiji Shrine", "description": "Peaceful Shinto shrine surrounded by forest in the heart of Tokyo.", "entry_fee": "Free", "duration_hours": 1.5, "best_time": "morning"}
244
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  }
246
-
247
- for key, attractions in real_attractions.items():
248
- if key in city_lower:
 
249
  return attractions
250
-
251
- # Generic attractions for any city
252
- return [
253
- {"name": f"{city} City Center", "description": f"The vibrant heart of {city} featuring historic architecture, bustling shops, and charming cafes.", "entry_fee": "Free", "duration_hours": 2, "best_time": "daytime"},
254
- {"name": f"{city} Main Square", "description": f"The central gathering place surrounded by beautiful buildings and local culture.", "entry_fee": "Free", "duration_hours": 1.5, "best_time": "afternoon"},
255
- {"name": f"{city} Cathedral", "description": f"A magnificent religious site showcasing stunning architecture and centuries of history.", "entry_fee": "Free", "duration_hours": 1, "best_time": "morning"},
256
- {"name": f"{city} Museum", "description": f"Discover the rich cultural heritage and artistic treasures of {city}.", "entry_fee": "$12", "duration_hours": 2, "best_time": "afternoon"}
257
- ]
258
 
259
  def get_attractions(city: str) -> Dict:
260
  """Get attractions from cache or fetch new ones."""
261
  city_clean = city.strip().lower()
262
  if city_clean in ATTRACTIONS_CACHE:
263
  return {"city": city, "attractions": ATTRACTIONS_CACHE[city_clean], "source": "cache"}
264
-
265
  attractions = fetch_attractions_via_openai(city)
266
- return {"city": city, "attractions": attractions, "source": "OpenAI"}
 
267
 
268
  # -------------------- BUDGET CALCULATION --------------------
269
  def calculate_budget(num_days: int, budget_amount: float = None) -> Dict:
@@ -285,291 +348,297 @@ def calculate_budget(num_days: int, budget_amount: float = None) -> Dict:
285
  else:
286
  level = "moderate"
287
  daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
288
-
289
- accommodation = daily_rates["accommodation"] * num_days
290
- food = daily_rates["food"] * num_days
291
- transport = daily_rates["transport"] * num_days
292
- activities = daily_rates["activities"] * num_days
293
- total = accommodation + food + transport + activities
294
-
295
  return {
296
  "level": level,
297
- "accommodation": accommodation,
298
- "food": food,
299
- "transport": transport,
300
- "activities": activities,
301
- "total": total,
302
- "daily": daily_rates
303
  }
304
 
 
305
  # -------------------- ITINERARY GENERATION --------------------
306
- def generate_itinerary(destination: str, start_date: str, num_days: int,
307
  budget_amount: float, budget_currency: str, departure_city: str = ""):
308
- """Main itinerary generation function with beautiful formatting."""
309
  try:
310
- # Validate inputs
311
- if not destination:
312
- return "<div style='color: red; padding: 20px; text-align: center;'>❌ Please enter a destination city.</div>"
313
-
314
  if num_days < 1 or num_days > 14:
315
- return "<div style='color: red; padding: 20px; text-align: center;'>❌ Number of days must be between 1 and 14.</div>"
316
-
317
- # Get data
318
  weather = get_weather(destination)
319
  if "error" in weather:
320
- return f"<div style='color: red; padding: 20px; text-align: center;'>❌ Weather error: {weather['error']}</div>"
321
-
322
  attractions_data = get_attractions(destination)
323
  attractions = attractions_data["attractions"]
324
-
325
- # Calculate budget
 
 
 
 
 
 
326
  budget_data = calculate_budget(num_days, budget_amount)
327
-
328
- # Format dates
329
  start = datetime.strptime(start_date, "%Y-%m-%d")
330
  end = start + timedelta(days=int(num_days) - 1)
331
-
332
- # Generate beautiful HTML itinerary with images
333
- html = generate_beautiful_itinerary(
334
- destination, weather, attractions, budget_data,
335
- num_days, start, end, budget_amount, budget_currency, departure_city
 
 
 
336
  )
337
-
338
- return html
339
-
340
  except Exception as e:
341
- return f"<div style='color: red; padding: 20px; text-align: center;'>❌ An unexpected error occurred: {str(e)}</div>"
342
-
343
- def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
344
- num_days, start_date, end_date, budget_amount,
345
- budget_currency, departure_city):
346
- """Create a stunning, visually appealing itinerary with images."""
347
-
348
- # Weather details
349
  weather_temp = f"{weather['temperature']:.1f}°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
350
- weather_condition = weather['condition'].capitalize()
351
-
352
- # Budget warning
353
  budget_warning = ""
354
- if budget_amount and budget_data['total'] > budget_amount:
355
  budget_warning = f"""
356
- <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; border-radius: 8px; margin: 15px 0;">
357
- <strong>⚠️ Budget Alert:</strong> Estimated costs (${budget_data['total']:.0f}) exceed your budget (${budget_amount:.0f}).
358
- Consider reducing days or choosing budget-friendly options like street food and free attractions.
359
- </div>
 
 
 
 
 
 
360
  """
361
-
362
- # Attractions list with images
363
- attractions_html = ""
364
- for attr in attractions[:6]:
365
- time_icon = "🌅" if attr.get('best_time') == "morning" else "☀️" if attr.get('best_time') == "afternoon" else "🌙"
366
- # Get image for this attraction
367
- img_url = get_attraction_image(attr['name'], destination)
368
-
369
- attractions_html += f"""
370
- <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;">
371
- <div style="display: flex; flex-wrap: wrap;">
372
- <div style="flex: 0 0 200px; overflow: hidden;">
373
- <img src="{img_url}" style="width: 100%; height: 150px; object-fit: cover;" alt="{attr['name']}">
374
- </div>
375
- <div style="flex: 1; padding: 15px;">
376
- <div style="display: flex; justify-content: space-between; align-items: start;">
377
- <div style="flex: 1;">
378
- <strong style="font-size: 1.1em; color: #333;">📍 {attr['name']}</strong>
379
- <div style="color: #666; margin: 8px 0; font-size: 0.95em;">{attr['description']}</div>
380
- <div style="display: flex; gap: 15px; margin-top: 8px; font-size: 0.85em;">
381
- <span>⏱️ {attr['duration_hours']} hrs</span>
382
- <span>🎟️ {attr['entry_fee']}</span>
383
- <span>{time_icon} Best: {attr.get('best_time', 'anytime')}</span>
384
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  </div>
386
  </div>
387
  </div>
388
- </div>
389
- </div>
390
- """
391
-
392
- # Daily itinerary with images
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  daily_html = ""
394
  per_day = max(1, len(attractions) // max(1, num_days))
395
-
396
  for day in range(1, num_days + 1):
397
- current_date = start_date + timedelta(days=day-1)
398
  date_str = current_date.strftime("%A, %B %d")
399
-
400
- start_idx = (day-1) * per_day
401
  end_idx = min(day * per_day, len(attractions))
402
  day_attractions = attractions[start_idx:end_idx]
403
-
 
 
404
  if day_attractions:
405
  daily_html += f"""
406
- <div style="background: white; border-radius: 12px; margin-bottom: 20px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
407
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 12px 20px; color: white;">
408
- <h3 style="margin: 0; font-size: 1.2em;">Day {day} · {date_str}</h3>
 
 
409
  </div>
410
- <div style="padding: 20px;">
411
- """
412
-
413
- for attr in day_attractions:
414
- img_url = get_attraction_image(attr['name'], destination)
415
- daily_html += f"""
416
- <div style="display: flex; gap: 15px; margin-bottom: 20px; padding: 10px; background: #f8f9fa; border-radius: 8px;">
417
- <img src="{img_url}" style="width: 80px; height: 80px; object-fit: cover; border-radius: 8px;" alt="{attr['name']}">
418
- <div style="flex: 1;">
419
- <strong>{attr['name']}</strong><br>
420
- <span style="font-size: 0.85em; color: #666;">{attr['description'][:100]}...</span>
421
- <div style="font-size: 0.75em; color: #888; margin-top: 5px;">
422
- ⏱️ {attr['duration_hours']} hrs | 🎟️ {attr['entry_fee']}
423
- </div>
424
- </div>
425
- </div>
426
- """
427
-
428
- daily_html += f"""
429
- <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
430
- <div style="margin-bottom: 10px;">
431
- <span style="font-size: 1.1em;">🍽️</span> <strong>Lunch Recommendation:</strong> Try authentic local cuisine at a nearby restaurant
432
- </div>
433
- <div>
434
- <span style="font-size: 1.1em;">🌙</span> <strong>Evening Activity:</strong> Explore local markets, enjoy a cultural show, or relax at a cafe
435
- </div>
436
  </div>
437
  </div>
438
- </div>
439
- """
440
-
441
- # Budget breakdown
442
- level_names = {"budget": "Budget", "moderate": "Moderate", "comfortable": "Comfortable", "luxury": "Luxury"}
443
- level_display = level_names.get(budget_data['level'], "Moderate")
444
-
445
- budget_html = f"""
446
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white; margin: 20px 0;">
447
- <h3 style="margin: 0 0 15px 0;">💰 Budget Breakdown ({level_display} Travel Style)</h3>
448
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px;">
449
- <div>
450
- <strong>🏨 Accommodation</strong><br>
451
- ${budget_data['accommodation']:.0f}<br>
452
- <small>(${budget_data['daily']['accommodation']}/day)</small>
453
- </div>
454
- <div>
455
- <strong>🍽️ Food & Dining</strong><br>
456
- ${budget_data['food']:.0f}<br>
457
- <small>(${budget_data['daily']['food']}/day)</small>
458
- </div>
459
- <div>
460
- <strong>🚗 Local Transport</strong><br>
461
- ${budget_data['transport']:.0f}<br>
462
- <small>(${budget_data['daily']['transport']}/day)</small>
463
- </div>
464
- <div>
465
- <strong>🎟️ Activities & Tours</strong><br>
466
- ${budget_data['activities']:.0f}<br>
467
- <small>(${budget_data['daily']['activities']}/day)</small>
468
- </div>
469
- <div style="border-top: 2px solid rgba(255,255,255,0.3); padding-top: 10px; grid-column: 1/-1;">
470
- <strong>💰 Total Estimated Cost</strong><br>
471
- <span style="font-size: 1.2em;">${budget_data['total']:.0f}</span>
472
- {f" (Your budget: ${budget_amount:.0f})" if budget_amount else ""}
473
- </div>
474
- </div>
475
- </div>
476
- """
477
-
478
- # Travel tips with icons
479
  tips_html = """
480
- <div style="background: #f0f4ff; padding: 20px; border-radius: 12px; margin: 20px 0;">
481
- <h3 style="margin: 0 0 15px 0; color: #667eea;">💡 Smart Travel Tips</h3>
482
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
483
- <div>🎫 <strong>Book in Advance</strong><br>Save time and money by booking popular attractions online</div>
484
- <div>🚇 <strong>Public Transport</strong><br>Get a day pass for unlimited travel and better savings</div>
485
- <div>📱 <strong>Offline Maps</strong><br>Download Google Maps offline to navigate without data</div>
486
- <div>💵 <strong>Local Currency</strong><br>Carry cash for markets and small vendors</div>
487
- <div>🌍 <strong>Learn Basic Phrases</strong><br>A few local words go a long way with locals</div>
488
- <div>📸 <strong>Early Bird</strong><br>Visit popular spots early morning to avoid crowds</div>
489
- </div>
490
- </div>
491
- """
492
-
493
- # Complete HTML
494
- full_html = f"""
495
- <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; max-width: 100%;">
496
- <!-- Hero Section -->
497
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 20px; color: white; margin-bottom: 30px; text-align: center;">
498
- <h1 style="margin: 0; font-size: 2.5em;">�� {destination}</h1>
499
- <p style="margin: 10px 0 0; opacity: 0.9; font-size: 1.1em;">
500
- {num_days} Days of Adventure • {start_date.strftime('%B %d')} - {end_date.strftime('%B %d, %Y')}
501
- </p>
502
- {f"<p style='margin: 5px 0 0; opacity: 0.8;'>✈️ From: {departure_city}</p>" if departure_city else ""}
503
- </div>
504
-
505
- <!-- Quick Stats -->
506
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
507
- <div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
508
- <div style="font-size: 2em;">🌤️</div>
509
- <strong>{weather_temp}</strong><br>
510
- <small>{weather_condition}</small>
511
- </div>
512
- <div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
513
- <div style="font-size: 2em;">📅</div>
514
- <strong>{num_days} Days</strong><br>
515
- <small>Full Itinerary</small>
516
- </div>
517
- <div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
518
- <div style="font-size: 2em;">🎯</div>
519
- <strong>{len(attractions)}+ Attractions</strong><br>
520
- <small>To Explore</small>
521
- </div>
522
- <div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
523
- <div style="font-size: 2em;">💰</div>
524
- <strong>${budget_data['total']:.0f}</strong><br>
525
- <small>Estimated Total</small>
526
- </div>
527
  </div>
528
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  {budget_warning}
530
-
531
- <!-- Budget Section -->
532
  {budget_html}
533
-
534
- <!-- Top Attractions with Images -->
535
- <div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
536
- <h2 style="margin: 0 0 15px 0; color: #667eea;">✨ Top Attractions in {destination}</h2>
537
  {attractions_html}
538
  </div>
539
-
540
- <!-- Daily Itinerary with Images -->
541
- <div style="background: white; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
542
- <h2 style="margin: 0 0 15px 0; color: #667eea;">📅 Your {num_days}-Day Itinerary</h2>
543
  {daily_html}
544
  </div>
545
-
546
- <!-- Travel Tips -->
547
  {tips_html}
548
-
549
- <!-- Booking Links -->
550
- <div style="text-align: center; padding: 20px; margin-top: 20px; background: #f8f9fa; border-radius: 12px;">
551
- <h3 style="margin: 0 0 15px 0;">Ready to Book Your Trip?</h3>
552
- <p>
553
- 🛏️ <a href="https://www.booking.com/searchresults.html?ss={destination.replace(' ', '+')}" target="_blank" style="color: #667eea; text-decoration: none;">Search Hotels</a> |
554
- ✈️ <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> |
555
- 🎟️ <a href="https://www.tripadvisor.com/Search?q={destination}" target="_blank" style="color: #667eea; text-decoration: none;">Read Reviews</a>
556
- </p>
557
- </div>
558
-
559
- <!-- Footer -->
560
- <div style="text-align: center; padding: 20px; margin-top: 20px; font-size: 0.85em; color: #666;">
561
- <p>✨ TravelBuddy AI • Powered by OpenAI • Real-time Weather • Beautiful Images • Smart Recommendations</p>
562
- <p>🌍 Plan your perfect adventure with confidence</p>
563
- </div>
564
- </div>
565
- """
566
-
567
- return full_html
568
 
569
  # -------------------- GRADIO INTERFACE --------------------
570
  css = """
571
  .gradio-container {
572
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
573
  max-width: 1200px;
574
  margin: 0 auto;
575
  }
@@ -577,44 +646,31 @@ css = """
577
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
578
  border: none !important;
579
  font-weight: bold !important;
580
- padding: 12px 30px !important;
581
  font-size: 1.1em !important;
582
- transition: transform 0.2s !important;
583
- }
584
- .gr-button-primary:hover {
585
- transform: translateY(-2px) !important;
586
- }
587
- input, select, textarea {
588
- border-radius: 8px !important;
589
- border: 1px solid #e0e0e0 !important;
590
- }
591
- label {
592
- font-weight: 500 !important;
593
- color: #333 !important;
594
  }
 
 
595
  """
596
 
597
  with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as demo:
598
  gr.HTML("""
599
- <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 20px; margin-bottom: 30px;">
600
- <h1 style="color: white; margin: 0; font-size: 2.5em;">✨ TravelBuddy AI</h1>
601
- <p style="color: white; margin: 10px 0 0; opacity: 0.95; font-size: 1.1em;">
602
- Your Intelligent Travel Companion - Create Beautiful, Personalized Itineraries for Any Destination Worldwide
 
603
  </p>
604
- </div>
605
- """)
606
-
607
  with gr.Row(equal_height=True):
608
  with gr.Column(scale=2):
609
  with gr.Group():
610
  gr.Markdown("### 🎯 Where's Your Next Adventure?")
611
  destination = gr.Textbox(
612
  label="Destination",
613
- placeholder="e.g., Paris, Tokyo, New York, Bali, Cape Town...",
614
- lines=1,
615
- show_label=False
616
  )
617
-
618
  with gr.Group():
619
  gr.Markdown("### 📅 When Are You Traveling?")
620
  with gr.Row():
@@ -624,70 +680,71 @@ with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as de
624
  placeholder="YYYY-MM-DD"
625
  )
626
  num_days = gr.Slider(
627
- label="Duration (Days)",
628
- minimum=1,
629
- maximum=14,
630
- value=3,
631
- step=1
632
  )
633
-
634
  with gr.Column(scale=1):
635
  with gr.Group():
636
  gr.Markdown("### 💰 Your Budget")
637
  with gr.Row():
638
  budget_amount = gr.Number(
639
- label="Total Budget (Optional)",
640
- placeholder="Enter amount",
641
- value=None
642
  )
643
  budget_currency = gr.Dropdown(
644
- ["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF"],
645
- label="Currency",
646
- value="USD"
647
  )
648
  gr.HTML("""
649
- <div style="background: #e8f0fe; padding: 10px; border-radius: 8px; margin-top: 10px;">
650
- <small>💡 <strong>Smart Tip:</strong> We'll automatically suggest the best travel style based on your budget and trip duration!</small>
651
- </div>
652
- """)
653
-
654
  with gr.Group():
655
  gr.Markdown("### ✈️ Departure Info")
656
  departure_city = gr.Textbox(
657
  label="Departure City (Optional)",
658
- placeholder="e.g., New York, London, Sydney",
659
  lines=1
660
  )
661
-
662
  with gr.Row():
663
- generate_btn = gr.Button("✨ Generate My Personalized Itinerary", variant="primary", size="lg")
664
-
 
 
665
  output = gr.HTML()
666
-
667
  generate_btn.click(
668
  fn=generate_itinerary,
669
  inputs=[destination, start_date, num_days, budget_amount, budget_currency, departure_city],
670
- outputs=output
671
  )
672
-
673
- # Examples section
674
  gr.HTML("""
675
- <div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee;">
676
  <h3>🌟 Popular Destinations to Try</h3>
677
- <div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
678
- <button onclick="document.querySelector('#destination input').value='Paris';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">🗼 Paris</button>
679
- <button onclick="document.querySelector('#destination input').value='Tokyo';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">🗾 Tokyo</button>
680
- <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>
681
- <button onclick="document.querySelector('#destination input').value='Bali';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">🏝️ Bali</button>
682
- <button onclick="document.querySelector('#destination input').value='Rome';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">🏛️ Rome</button>
683
- <button onclick="document.querySelector('#destination input').value='Bangkok';" style="background: #f0f4ff; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer;">🍜 Bangkok</button>
 
 
 
 
 
 
 
 
 
 
684
  </div>
685
  </div>
686
-
687
- <div style="text-align: center; padding: 20px; margin-top: 20px; border-top: 1px solid #eee; color: #666;">
688
- <small>Powered by OpenAI, Weather API, Unsplash Smart travel planning with beautiful visuals ✨</small>
689
- </div>
690
- """)
691
 
692
  if __name__ == "__main__":
693
- demo.launch(share=False, server_name="0.0.0.0")
 
2
  import json
3
  import requests
4
  from datetime import datetime, timedelta
 
5
  import gradio as gr
6
  import openai
7
  import re
 
11
  # -------------------- CONFIGURATION --------------------
12
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
13
  OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
 
14
 
15
  if not OPENAI_API_KEY:
16
+ print("⚠️ OPENAI_API_KEY not set. AI itinerary generation will fall back to curated data.")
17
  client = None
18
  else:
19
  client = openai.OpenAI(api_key=OPENAI_API_KEY)
20
 
21
+ # Global caches
22
  ATTRACTIONS_CACHE = {}
 
 
 
 
 
23
  IMAGE_CACHE = {}
24
 
25
+ # -------------------- REAL IMAGE FUNCTIONS --------------------
26
+
27
+ def get_wikimedia_image(attraction_name: str, city: str) -> str | None:
28
+ """
29
+ Fetch a REAL image from Wikipedia/Wikimedia Commons.
30
+ Returns a URL string if found, None if not available.
31
+ Never returns a fake or placeholder image.
32
+ """
33
+ cache_key = f"{attraction_name}_{city}".lower().replace(" ", "_")
34
  if cache_key in IMAGE_CACHE:
35
  return IMAGE_CACHE[cache_key]
36
+
37
+ # Try searching Wikipedia for the attraction
38
+ search_queries = [
39
+ attraction_name,
40
+ f"{attraction_name} {city}",
41
+ ]
42
+
43
+ for query in search_queries:
44
  try:
45
+ # Step 1: Search Wikipedia for the page
46
+ search_url = "https://en.wikipedia.org/w/api.php"
47
+ search_params = {
48
+ "action": "query",
49
+ "list": "search",
50
+ "srsearch": query,
51
+ "srlimit": 1,
52
+ "format": "json",
53
+ "origin": "*"
54
  }
55
+ resp = requests.get(search_url, params=search_params, timeout=8)
56
+ if resp.status_code != 200:
57
+ continue
58
+
59
+ results = resp.json().get("query", {}).get("search", [])
60
+ if not results:
61
+ continue
62
+
63
+ page_title = results[0]["title"]
64
+
65
+ # Step 2: Get the main image for that Wikipedia page
66
+ image_params = {
67
+ "action": "query",
68
+ "titles": page_title,
69
+ "prop": "pageimages",
70
+ "pithumbsize": 800,
71
+ "format": "json",
72
+ "origin": "*"
73
+ }
74
+ img_resp = requests.get(search_url, params=image_params, timeout=8)
75
+ if img_resp.status_code != 200:
76
+ continue
77
+
78
+ pages = img_resp.json().get("query", {}).get("pages", {})
79
+ for page in pages.values():
80
+ thumbnail = page.get("thumbnail", {})
81
+ if thumbnail.get("source"):
82
+ url = thumbnail["source"]
83
+ IMAGE_CACHE[cache_key] = url
84
+ return url
85
+
86
+ except Exception:
87
+ continue
88
+
89
+ # No real image found — return None (never fake)
90
+ IMAGE_CACHE[cache_key] = None
91
+ return None
92
+
93
+
94
+ def get_city_hero_image(city: str) -> str | None:
95
+ """Get a real Wikipedia image for the city itself as a hero/banner."""
96
+ cache_key = f"city_hero_{city}".lower().replace(" ", "_")
97
+ if cache_key in IMAGE_CACHE:
98
+ return IMAGE_CACHE[cache_key]
99
+
100
+ try:
101
+ search_url = "https://en.wikipedia.org/w/api.php"
102
+ image_params = {
103
+ "action": "query",
104
+ "titles": city,
105
+ "prop": "pageimages",
106
+ "pithumbsize": 1200,
107
+ "format": "json",
108
+ "origin": "*"
109
+ }
110
+ resp = requests.get(search_url, params=image_params, timeout=8)
111
+ if resp.status_code == 200:
112
+ pages = resp.json().get("query", {}).get("pages", {})
113
+ for page in pages.values():
114
+ thumbnail = page.get("thumbnail", {})
115
+ if thumbnail.get("source"):
116
+ url = thumbnail["source"]
117
+ IMAGE_CACHE[cache_key] = url
118
+ return url
119
+ except Exception:
120
+ pass
121
+
122
+ IMAGE_CACHE[cache_key] = None
123
+ return None
124
+
125
 
126
  # -------------------- WEATHER FUNCTION --------------------
127
  def get_weather(city: str) -> dict:
 
134
  "humidity": 60,
135
  "wind_speed": 10,
136
  "precipitation": 0,
137
+ "note": "Demo mode add OpenWeather API key for real data"
138
  }
139
  try:
140
  url = "https://api.openweathermap.org/data/2.5/weather"
141
+ params = {"q": city, "appid": OPENWEATHER_API_KEY, "units": "metric"}
142
  response = requests.get(url, params=params, timeout=10)
143
  data = response.json()
144
  if response.status_code != 200:
145
  return {"error": f"Weather API error: {data.get('message', 'unknown')}"}
 
146
  return {
147
  "city": city,
148
+ "temperature": data["main"]["temp"],
149
+ "feels_like": data["main"]["feels_like"],
150
+ "condition": data["weather"][0]["description"],
151
+ "humidity": data["main"]["humidity"],
152
+ "wind_speed": data["wind"]["speed"],
153
+ "precipitation": data.get("rain", {}).get("1h", 0),
154
  }
155
  except Exception as e:
156
  return {"error": f"Weather service unavailable: {str(e)}"}
157
 
158
+
159
+ # -------------------- ATTRACTIONS VIA OPENAI --------------------
160
  def fetch_attractions_via_openai(city: str) -> List[Dict]:
161
+ """
162
+ Fetch REAL, specific attractions for ANY city using OpenAI.
163
+ The prompt strictly forbids generic placeholders.
164
+ """
165
  if not client:
166
+ return get_curated_fallback(city)
167
+
168
+ prompt = f"""
169
+ You are an expert travel guide with deep knowledge of {city}.
170
+
171
+ List the 6 most famous and iconic real tourist attractions in {city}.
172
+
173
+ STRICT RULES:
174
+ - Use ONLY real, named, well-known attractions that actually exist in {city}.
175
+ - Do NOT use generic names like "City Center", "Main Square", "Cathedral", "Museum", or "{city} [generic word]".
176
+ - Each attraction must be a specific, recognizable landmark or site.
177
+ - Entry fees should be realistic for {city} in USD (0 for free).
178
+
179
+ Return ONLY a JSON array (no markdown, no extra text) with this exact structure:
 
180
  [
181
  {{
182
+ "name": "Exact Real Attraction Name",
183
+ "description": "Vivid 2-sentence description of this specific place.",
184
+ "entry_fee": 0,
185
+ "duration_hours": 2,
186
+ "best_time": "morning"
187
  }}
188
  ]
189
 
190
+ best_time must be one of: morning, afternoon, evening, anytime.
191
  """
192
+
193
+ try:
194
  response = client.chat.completions.create(
195
  model="gpt-3.5-turbo",
196
  messages=[{"role": "user", "content": prompt}],
197
+ temperature=0.5,
198
+ max_tokens=1800,
199
  )
200
+ content = response.choices[0].message.content.strip()
201
+
202
+ # Strip markdown fences if present
203
+ content = re.sub(r"^```json\s*", "", content)
204
+ content = re.sub(r"^```\s*", "", content)
205
+ content = re.sub(r"\s*```$", "", content)
206
+
 
 
 
 
207
  attractions = json.loads(content)
208
+ if isinstance(attractions, dict):
209
+ attractions = attractions.get("attractions", [attractions])
210
+
 
 
 
211
  result = []
212
+ for a in attractions[:6]:
213
  fee = a.get("entry_fee", 0)
214
+ fee_display = "Free" if fee == 0 else f"${fee}"
 
 
 
 
 
 
 
215
  result.append({
216
+ "name": a.get("name", ""),
217
  "entry_fee": fee_display,
218
  "duration_hours": a.get("duration_hours", 2),
219
+ "description": a.get("description", ""),
220
+ "best_time": a.get("best_time", "anytime"),
221
  })
222
+
223
+ # Filter out any that look generic (safety net)
224
+ generic_keywords = ["city center", "main square", "cathedral", "museum", "old town", "market"]
225
+ result = [
226
+ a for a in result
227
+ if not any(
228
+ a["name"].lower() == f"{city.lower()} {kw}" or a["name"].lower() == kw
229
+ for kw in generic_keywords
230
+ )
231
+ ]
232
+
233
+ ATTRACTIONS_CACHE[city.strip().lower()] = result
234
  return result
235
+
236
  except Exception as e:
237
  print(f"OpenAI attractions error: {e}")
238
+ return get_curated_fallback(city)
239
+
240
+
241
+ def get_curated_fallback(city: str) -> List[Dict]:
242
+ """
243
+ Curated real attractions for major cities when OpenAI is unavailable.
244
+ Only real named places no generics.
245
+ """
246
+ curated = {
247
+ "nairobi": [
248
+ {"name": "Nairobi National Park", "description": "The world's only national park within a capital city, home to lions, rhinos, giraffes and over 400 bird species against a dramatic city skyline backdrop.", "entry_fee": "$60", "duration_hours": 4, "best_time": "morning"},
249
+ {"name": "David Sheldrick Wildlife Trust", "description": "World-famous elephant orphanage where you can watch baby elephants being fed and bathed — an unforgettable wildlife encounter.", "entry_fee": "$7", "duration_hours": 1, "best_time": "morning"},
250
+ {"name": "Giraffe Centre", "description": "A sanctuary for endangered Rothschild giraffes where you can hand-feed them from an elevated platform just inches from their faces.", "entry_fee": "$15", "duration_hours": 1, "best_time": "morning"},
251
+ {"name": "Karen Blixen Museum", "description": "The beautifully preserved farmhouse of 'Out of Africa' author Karen Blixen, offering a glimpse into colonial Kenya's history and culture.", "entry_fee": "$10", "duration_hours": 1.5, "best_time": "afternoon"},
252
+ {"name": "Nairobi National Museum", "description": "Kenya's flagship museum showcasing natural history, cultural heritage, prehistoric fossils and stunning art collections.", "entry_fee": "$12", "duration_hours": 2, "best_time": "afternoon"},
253
+ {"name": "Bomas of Kenya", "description": "A vibrant cultural center featuring traditional homesteads of Kenya's 42 tribes and daily performances of traditional music and dance.", "entry_fee": "$8", "duration_hours": 2, "best_time": "afternoon"},
254
+ ],
255
  "paris": [
256
+ {"name": "Eiffel Tower", "description": "Iconic iron lattice tower offering breathtaking panoramic views of Paris from three observation levels. Built in 1889, it remains the world's most visited paid monument.", "entry_fee": "$25", "duration_hours": 2.5, "best_time": "evening"},
257
+ {"name": "Louvre Museum", "description": "World's largest art museum housing over 35,000 works including the Mona Lisa and Venus de Milo, set in a stunning 12th-century palace.", "entry_fee": "$20", "duration_hours": 4, "best_time": "morning"},
258
+ {"name": "Musée d'Orsay", "description": "Housed in a stunning Beaux-Arts railway station, this museum holds the world's finest collection of Impressionist and Post-Impressionist art.", "entry_fee": "$16", "duration_hours": 3, "best_time": "morning"},
259
+ {"name": "Sacré-Cœur Basilica", "description": "Magnificent white-domed basilica crowning Montmartre hill, offering panoramic views across Paris and a peaceful spiritual atmosphere.", "entry_fee": "Free", "duration_hours": 1.5, "best_time": "morning"},
260
+ {"name": "Palace of Versailles", "description": "Opulent royal palace with stunning Hall of Mirrors, magnificent formal gardens and the grandeur of French royal history.", "entry_fee": "$20", "duration_hours": 5, "best_time": "morning"},
261
+ {"name": "Seine River Cruise", "description": "Relaxing Bateaux Mouches cruise past Notre-Dame, the Louvre and the Eiffel Tower Paris's landmarks look magical from the water.", "entry_fee": "$15", "duration_hours": 1, "best_time": "evening"},
262
  ],
263
  "tokyo": [
264
+ {"name": "Senso-ji Temple", "description": "Tokyo's oldest and most famous Buddhist temple in Asakusa, with its iconic Thunder Gate and bustling Nakamise shopping street.", "entry_fee": "Free", "duration_hours": 2, "best_time": "morning"},
265
+ {"name": "Shibuya Crossing", "description": "The world's busiest pedestrian crossing, a mesmerizing spectacle of thousands of people crossing simultaneously in perfect choreography.", "entry_fee": "Free", "duration_hours": 1, "best_time": "evening"},
266
+ {"name": "Meiji Shrine", "description": "Serene Shinto shrine set in 70 hectares of forested parkland, dedicated to Emperor Meiji — a peaceful escape from city life.", "entry_fee": "Free", "duration_hours": 1.5, "best_time": "morning"},
267
+ {"name": "Tokyo Skytree", "description": "The world's second-tallest structure at 634m, offering unparalleled 360° views of Tokyo and on clear days, Mount Fuji.", "entry_fee": "$18", "duration_hours": 2, "best_time": "evening"},
268
+ {"name": "Tsukiji Outer Market", "description": "Famous seafood and food market where you can enjoy the freshest sushi breakfast, street food and browse unique Japanese kitchen goods.", "entry_fee": "Free", "duration_hours": 2, "best_time": "morning"},
269
+ {"name": "Shinjuku Gyoen National Garden", "description": "Stunning garden blending Japanese, French and English landscape styles — the most popular cherry blossom viewing spot in Tokyo.", "entry_fee": "$2", "duration_hours": 2, "best_time": "afternoon"},
270
+ ],
271
+ "new york": [
272
+ {"name": "Statue of Liberty", "description": "America's most iconic landmark, a gift from France standing 93 meters tall on Liberty Island, welcoming visitors since 1886.", "entry_fee": "$24", "duration_hours": 4, "best_time": "morning"},
273
+ {"name": "Central Park", "description": "Manhattan's 843-acre green oasis with iconic landmarks, lakes, meadows and world-class attractions — New York's living room.", "entry_fee": "Free", "duration_hours": 3, "best_time": "morning"},
274
+ {"name": "The Metropolitan Museum of Art", "description": "One of the world's greatest art museums with over 2 million works spanning 5,000 years of history across 17 curatorial departments.", "entry_fee": "$30", "duration_hours": 4, "best_time": "morning"},
275
+ {"name": "Brooklyn Bridge", "description": "Iconic 1883 suspension bridge offering spectacular Manhattan skyline views — best experienced by walking across it.", "entry_fee": "Free", "duration_hours": 1, "best_time": "morning"},
276
+ {"name": "Times Square", "description": "The glittering 'Crossroads of the World' — an electrifying spectacle of towering billboards, Broadway theaters and constant energy.", "entry_fee": "Free", "duration_hours": 1.5, "best_time": "evening"},
277
+ {"name": "9/11 Memorial & Museum", "description": "A deeply moving tribute to the 2001 attacks, featuring twin reflecting pools where the towers once stood and a powerful underground museum.", "entry_fee": "$29", "duration_hours": 3, "best_time": "morning"},
278
+ ],
279
+ "london": [
280
+ {"name": "Tower of London", "description": "Historic royal palace and fortress housing the Crown Jewels, with 1,000 years of history as a palace, prison and place of execution.", "entry_fee": "$32", "duration_hours": 3, "best_time": "morning"},
281
+ {"name": "British Museum", "description": "One of the world's greatest museums with over 8 million works spanning human history, including the Rosetta Stone and Elgin Marbles.", "entry_fee": "Free", "duration_hours": 3, "best_time": "morning"},
282
+ {"name": "Buckingham Palace", "description": "The official London residence of the British monarch, famous for the Changing of the Guard ceremony and State Rooms open in summer.", "entry_fee": "$30", "duration_hours": 2, "best_time": "morning"},
283
+ {"name": "Westminster Abbey", "description": "UNESCO-listed Gothic church where British monarchs are crowned and buried, with 1,000 years of royal history within its walls.", "entry_fee": "$28", "duration_hours": 2, "best_time": "morning"},
284
+ {"name": "Tate Modern", "description": "World-class modern and contemporary art gallery in a spectacular converted power station on the South Bank of the Thames.", "entry_fee": "Free", "duration_hours": 2.5, "best_time": "afternoon"},
285
+ {"name": "Hyde Park", "description": "London's most famous Royal Park covering 350 acres, home to the Serpentine Gallery, Speaker's Corner and the Diana Memorial Fountain.", "entry_fee": "Free", "duration_hours": 2, "best_time": "afternoon"},
286
+ ],
287
+ "rome": [
288
+ {"name": "Colosseum", "description": "The world's greatest amphitheater, built in 70-80 AD and capable of holding 50,000 spectators for gladiatorial contests and public spectacles.", "entry_fee": "$18", "duration_hours": 3, "best_time": "morning"},
289
+ {"name": "Vatican Museums & Sistine Chapel", "description": "One of the world's most important art collections, culminating in Michelangelo's breathtaking ceiling fresco in the Sistine Chapel.", "entry_fee": "$20", "duration_hours": 4, "best_time": "morning"},
290
+ {"name": "Trevi Fountain", "description": "Rome's most famous Baroque fountain, where legend says tossing a coin guarantees your return to Rome — best visited at dawn.", "entry_fee": "Free", "duration_hours": 1, "best_time": "morning"},
291
+ {"name": "Roman Forum", "description": "The ancient heart of Rome, a sprawling ruined complex of temples, arches and government buildings that was once the center of the Roman Empire.", "entry_fee": "$18", "duration_hours": 2, "best_time": "morning"},
292
+ {"name": "Pantheon", "description": "The best-preserved ancient building in Rome, a 2,000-year-old temple with a remarkable unreinforced concrete dome and open oculus.", "entry_fee": "$6", "duration_hours": 1, "best_time": "morning"},
293
+ {"name": "Borghese Gallery", "description": "A world-class museum in a stunning villa park, housing a breathtaking collection of Bernini sculptures and Caravaggio paintings.", "entry_fee": "$15", "duration_hours": 2, "best_time": "afternoon"},
294
+ ],
295
+ "bali": [
296
+ {"name": "Tanah Lot Temple", "description": "Bali's most iconic sea temple perched dramatically on a rocky outcrop in the ocean, best seen at sunset when the sky turns golden.", "entry_fee": "$4", "duration_hours": 2, "best_time": "evening"},
297
+ {"name": "Ubud Monkey Forest", "description": "A sacred sanctuary home to over 700 Balinese long-tailed macaques, set among ancient Hindu temples and lush tropical forest.", "entry_fee": "$5", "duration_hours": 1.5, "best_time": "morning"},
298
+ {"name": "Tegalalang Rice Terraces", "description": "Stunning UNESCO-recognized rice terraces north of Ubud, carved into the hillside with traditional Balinese subak irrigation — breathtaking at sunrise.", "entry_fee": "$2", "duration_hours": 2, "best_time": "morning"},
299
+ {"name": "Uluwatu Temple", "description": "Clifftop sea temple perched 70m above the Indian Ocean, famous for its nightly Kecak fire dance performances at sunset.", "entry_fee": "$3", "duration_hours": 2, "best_time": "evening"},
300
+ {"name": "Mount Batur", "description": "Active volcano offering a spectacular pre-dawn trek rewarded with breathtaking sunrise views over the caldera lake and surrounding mountains.", "entry_fee": "$40", "duration_hours": 6, "best_time": "morning"},
301
+ {"name": "Besakih Temple", "description": "Bali's most important and largest Hindu temple, the 'Mother Temple' complex of 23 temples sprawling up the slopes of Mount Agung.", "entry_fee": "$15", "duration_hours": 3, "best_time": "morning"},
302
+ ],
303
+ "bangkok": [
304
+ {"name": "Wat Pho (Temple of the Reclining Buddha)", "description": "Home to a magnificent 46-meter gilded reclining Buddha statue and one of Bangkok's oldest temples, also the birthplace of traditional Thai massage.", "entry_fee": "$4", "duration_hours": 1.5, "best_time": "morning"},
305
+ {"name": "Grand Palace", "description": "Bangkok's most dazzling complex of ornate buildings, temples and halls that served as the official residence of Thai kings since 1782.", "entry_fee": "$15", "duration_hours": 3, "best_time": "morning"},
306
+ {"name": "Wat Arun (Temple of Dawn)", "description": "Stunning riverside temple adorned with colorful porcelain mosaic spires — most spectacular at dawn and dusk from across the Chao Phraya.", "entry_fee": "$2", "duration_hours": 1.5, "best_time": "morning"},
307
+ {"name": "Chatuchak Weekend Market", "description": "One of the world's largest markets with over 15,000 stalls selling everything from street food to antiques, clothing and exotic pets.", "entry_fee": "Free", "duration_hours": 3, "best_time": "morning"},
308
+ {"name": "Chao Phraya River Cruise", "description": "Scenic boat ride along Bangkok's 'River of Kings', passing temples, palaces and traditional wooden houses on stilts.", "entry_fee": "$10", "duration_hours": 2, "best_time": "evening"},
309
+ {"name": "Jim Thompson House", "description": "Beautifully preserved traditional Thai house museum of the American silk entrepreneur, showcasing Asian art and antiquities in a lush garden setting.", "entry_fee": "$6", "duration_hours": 1.5, "best_time": "afternoon"},
310
+ ],
311
  }
312
+
313
+ city_lower = city.strip().lower()
314
+ for key, attractions in curated.items():
315
+ if key in city_lower or city_lower in key:
316
  return attractions
317
+
318
+ # If city not in curated list and OpenAI is unavailable, return empty with message
319
+ return []
320
+
 
 
 
 
321
 
322
  def get_attractions(city: str) -> Dict:
323
  """Get attractions from cache or fetch new ones."""
324
  city_clean = city.strip().lower()
325
  if city_clean in ATTRACTIONS_CACHE:
326
  return {"city": city, "attractions": ATTRACTIONS_CACHE[city_clean], "source": "cache"}
 
327
  attractions = fetch_attractions_via_openai(city)
328
+ return {"city": city, "attractions": attractions, "source": "api"}
329
+
330
 
331
  # -------------------- BUDGET CALCULATION --------------------
332
  def calculate_budget(num_days: int, budget_amount: float = None) -> Dict:
 
348
  else:
349
  level = "moderate"
350
  daily_rates = {"accommodation": 100, "food": 60, "transport": 25, "activities": 20}
351
+
 
 
 
 
 
 
352
  return {
353
  "level": level,
354
+ "accommodation": daily_rates["accommodation"] * num_days,
355
+ "food": daily_rates["food"] * num_days,
356
+ "transport": daily_rates["transport"] * num_days,
357
+ "activities": daily_rates["activities"] * num_days,
358
+ "total": sum(daily_rates[k] * num_days for k in daily_rates),
359
+ "daily": daily_rates,
360
  }
361
 
362
+
363
  # -------------------- ITINERARY GENERATION --------------------
364
+ def generate_itinerary(destination: str, start_date: str, num_days: int,
365
  budget_amount: float, budget_currency: str, departure_city: str = ""):
366
+ """Main itinerary generation function."""
367
  try:
368
+ if not destination or not destination.strip():
369
+ return "<div style='color:red;padding:20px;text-align:center;'>❌ Please enter a destination city.</div>"
 
 
370
  if num_days < 1 or num_days > 14:
371
+ return "<div style='color:red;padding:20px;text-align:center;'>❌ Number of days must be between 1 and 14.</div>"
372
+
373
+ destination = destination.strip()
374
  weather = get_weather(destination)
375
  if "error" in weather:
376
+ return f"<div style='color:red;padding:20px;text-align:center;'>❌ {weather['error']}</div>"
377
+
378
  attractions_data = get_attractions(destination)
379
  attractions = attractions_data["attractions"]
380
+
381
+ if not attractions:
382
+ return f"""
383
+ <div style='color:#856404;background:#fff3cd;border:1px solid #ffc107;padding:20px;border-radius:12px;text-align:center;'>
384
+ ⚠️ Could not find specific attractions for <strong>{destination}</strong>.<br>
385
+ Please check the city name or add an OpenAI API key for AI-powered recommendations.
386
+ </div>"""
387
+
388
  budget_data = calculate_budget(num_days, budget_amount)
 
 
389
  start = datetime.strptime(start_date, "%Y-%m-%d")
390
  end = start + timedelta(days=int(num_days) - 1)
391
+
392
+ # Fetch city hero image (real, from Wikipedia)
393
+ city_img_url = get_city_hero_image(destination)
394
+
395
+ return generate_beautiful_itinerary(
396
+ destination, weather, attractions, budget_data,
397
+ num_days, start, end, budget_amount, budget_currency,
398
+ departure_city, city_img_url
399
  )
400
+
 
 
401
  except Exception as e:
402
+ return f"<div style='color:red;padding:20px;text-align:center;'>❌ Unexpected error: {str(e)}</div>"
403
+
404
+
405
+ def generate_beautiful_itinerary(destination, weather, attractions, budget_data,
406
+ num_days, start_date, end_date, budget_amount,
407
+ budget_currency, departure_city, city_img_url):
408
+ """Create a beautiful itinerary HTML — only showing REAL images."""
409
+
410
  weather_temp = f"{weather['temperature']:.1f}°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
411
+ weather_condition = weather["condition"].capitalize()
412
+
 
413
  budget_warning = ""
414
+ if budget_amount and budget_data["total"] > budget_amount:
415
  budget_warning = f"""
416
+ <div style="background:#fff3cd;border-left:4px solid #ffc107;padding:15px;border-radius:8px;margin:15px 0;">
417
+ <strong>⚠️ Budget Alert:</strong> Estimated costs (${budget_data['total']:.0f}) exceed your budget
418
+ ({budget_currency} {budget_amount:.0f}). Consider choosing budget-friendly options.
419
+ </div>"""
420
+
421
+ # Hero banner — show real city image or a clean text-only banner
422
+ if city_img_url:
423
+ hero_style = f"""
424
+ background: linear-gradient(rgba(40,0,80,0.55), rgba(40,0,80,0.55)),
425
+ url('{city_img_url}') center/cover no-repeat;
426
  """
427
+ else:
428
+ hero_style = "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);"
429
+
430
+ hero_html = f"""
431
+ <div style="{hero_style} padding:50px 30px; border-radius:20px; color:white;
432
+ margin-bottom:30px; text-align:center;">
433
+ <h1 style="margin:0;font-size:2.8em;text-shadow:0 2px 8px rgba(0,0,0,0.4);">🌍 {destination}</h1>
434
+ <p style="margin:10px 0 0;font-size:1.15em;opacity:0.95;">
435
+ {num_days} Days of Adventure &nbsp;·&nbsp;
436
+ {start_date.strftime('%B %d')} {end_date.strftime('%B %d, %Y')}
437
+ </p>
438
+ {f"<p style='margin:6px 0 0;opacity:0.85;'>✈️ Departing from: {departure_city}</p>" if departure_city else ""}
439
+ </div>"""
440
+
441
+ # Quick stats
442
+ stats_html = f"""
443
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:28px;">
444
+ <div style="background:#f8f9fa;padding:18px;border-radius:12px;text-align:center;">
445
+ <div style="font-size:2em;">🌤️</div>
446
+ <strong>{weather_temp}</strong><br><small>{weather_condition}</small>
447
+ </div>
448
+ <div style="background:#f8f9fa;padding:18px;border-radius:12px;text-align:center;">
449
+ <div style="font-size:2em;">📅</div>
450
+ <strong>{num_days} Days</strong><br><small>Full Itinerary</small>
451
+ </div>
452
+ <div style="background:#f8f9fa;padding:18px;border-radius:12px;text-align:center;">
453
+ <div style="font-size:2em;">🎯</div>
454
+ <strong>{len(attractions)} Attractions</strong><br><small>To Explore</small>
455
+ </div>
456
+ <div style="background:#f8f9fa;padding:18px;border-radius:12px;text-align:center;">
457
+ <div style="font-size:2em;">💰</div>
458
+ <strong>{budget_currency} {budget_data['total']:.0f}</strong><br><small>Estimated Total</small>
459
+ </div>
460
+ </div>"""
461
+
462
+ # Budget breakdown
463
+ level_names = {"budget": "Budget", "moderate": "Moderate", "comfortable": "Comfortable", "luxury": "Luxury"}
464
+ level_display = level_names.get(budget_data["level"], "Moderate")
465
+ budget_html = f"""
466
+ <div style="background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);padding:22px;border-radius:12px;color:white;margin:20px 0;">
467
+ <h3 style="margin:0 0 15px 0;">💰 Budget Breakdown ({level_display} Travel Style)</h3>
468
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:15px;">
469
+ <div><strong>🏨 Accommodation</strong><br>{budget_currency} {budget_data['accommodation']:.0f}<br><small>({budget_currency} {budget_data['daily']['accommodation']}/day)</small></div>
470
+ <div><strong>🍽️ Food & Dining</strong><br>{budget_currency} {budget_data['food']:.0f}<br><small>({budget_currency} {budget_data['daily']['food']}/day)</small></div>
471
+ <div><strong>🚗 Local Transport</strong><br>{budget_currency} {budget_data['transport']:.0f}<br><small>({budget_currency} {budget_data['daily']['transport']}/day)</small></div>
472
+ <div><strong>🎟️ Activities</strong><br>{budget_currency} {budget_data['activities']:.0f}<br><small>({budget_currency} {budget_data['daily']['activities']}/day)</small></div>
473
+ <div style="border-top:2px solid rgba(255,255,255,0.3);padding-top:10px;grid-column:1/-1;">
474
+ <strong>💰 Total Estimated Cost:</strong>
475
+ <span style="font-size:1.25em;"> {budget_currency} {budget_data['total']:.0f}</span>
476
+ {f" &nbsp;(Your budget: {budget_currency} {budget_amount:.0f})" if budget_amount else ""}
477
+ </div>
478
+ </div>
479
+ </div>"""
480
+
481
+ # Top Attractions — real images only
482
+ def attraction_image_html(attr, size="large"):
483
+ """Render attraction with real image or image-free card."""
484
+ img_url = get_wikimedia_image(attr["name"], destination)
485
+ time_icon = {"morning": "🌅", "afternoon": "☀️", "evening": "🌙"}.get(attr.get("best_time", ""), "🕐")
486
+
487
+ if size == "large" and img_url:
488
+ return f"""
489
+ <div style="background:white;border-radius:12px;margin-bottom:20px;overflow:hidden;
490
+ box-shadow:0 2px 10px rgba(0,0,0,0.08);">
491
+ <div style="display:flex;flex-wrap:wrap;">
492
+ <div style="flex:0 0 220px;">
493
+ <img src="{img_url}" style="width:220px;height:160px;object-fit:cover;display:block;"
494
+ alt="{attr['name']}" onerror="this.parentElement.style.display='none'">
495
+ </div>
496
+ <div style="flex:1;padding:18px;min-width:200px;">
497
+ <strong style="font-size:1.1em;color:#333;">📍 {attr['name']}</strong>
498
+ <div style="color:#555;margin:8px 0;font-size:0.95em;line-height:1.5;">{attr['description']}</div>
499
+ <div style="display:flex;gap:15px;font-size:0.85em;color:#777;">
500
+ <span>⏱️ {attr['duration_hours']} hrs</span>
501
+ <span>🎟️ {attr['entry_fee']}</span>
502
+ <span>{time_icon} Best: {attr.get('best_time','anytime')}</span>
503
  </div>
504
  </div>
505
  </div>
506
+ </div>"""
507
+ elif size == "large":
508
+ # No image available — clean text card
509
+ return f"""
510
+ <div style="background:white;border-radius:12px;margin-bottom:20px;overflow:hidden;
511
+ box-shadow:0 2px 10px rgba(0,0,0,0.08);padding:18px;">
512
+ <strong style="font-size:1.1em;color:#333;">📍 {attr['name']}</strong>
513
+ <div style="color:#555;margin:8px 0;font-size:0.95em;line-height:1.5;">{attr['description']}</div>
514
+ <div style="display:flex;gap:15px;font-size:0.85em;color:#777;">
515
+ <span>⏱️ {attr['duration_hours']} hrs</span>
516
+ <span>🎟️ {attr['entry_fee']}</span>
517
+ <span>{time_icon} Best: {attr.get('best_time','anytime')}</span>
518
+ </div>
519
+ </div>"""
520
+
521
+ # Small card for daily schedule
522
+ if img_url:
523
+ return f"""
524
+ <div style="display:flex;gap:14px;margin-bottom:18px;padding:12px;
525
+ background:#f8f9fa;border-radius:10px;align-items:flex-start;">
526
+ <img src="{img_url}" style="width:90px;height:90px;object-fit:cover;border-radius:8px;flex-shrink:0;"
527
+ alt="{attr['name']}" onerror="this.style.display='none'">
528
+ <div>
529
+ <strong style="color:#333;">{attr['name']}</strong><br>
530
+ <span style="font-size:0.87em;color:#666;">{attr['description'][:110]}…</span>
531
+ <div style="font-size:0.78em;color:#888;margin-top:5px;">
532
+ ⏱️ {attr['duration_hours']} hrs &nbsp;|&nbsp; 🎟️ {attr['entry_fee']}
533
+ </div>
534
+ </div>
535
+ </div>"""
536
+ else:
537
+ return f"""
538
+ <div style="display:flex;gap:14px;margin-bottom:18px;padding:12px;
539
+ background:#f8f9fa;border-radius:10px;">
540
+ <div>
541
+ <strong style="color:#333;">📍 {attr['name']}</strong><br>
542
+ <span style="font-size:0.87em;color:#666;">{attr['description'][:110]}…</span>
543
+ <div style="font-size:0.78em;color:#888;margin-top:5px;">
544
+ ⏱️ {attr['duration_hours']} hrs &nbsp;|&nbsp; 🎟️ {attr['entry_fee']}
545
+ </div>
546
+ </div>
547
+ </div>"""
548
+
549
+ attractions_html = "".join(attraction_image_html(a, "large") for a in attractions[:6])
550
+
551
+ # Daily itinerary
552
  daily_html = ""
553
  per_day = max(1, len(attractions) // max(1, num_days))
 
554
  for day in range(1, num_days + 1):
555
+ current_date = start_date + timedelta(days=day - 1)
556
  date_str = current_date.strftime("%A, %B %d")
557
+ start_idx = (day - 1) * per_day
 
558
  end_idx = min(day * per_day, len(attractions))
559
  day_attractions = attractions[start_idx:end_idx]
560
+ if not day_attractions and day <= len(attractions):
561
+ day_attractions = [attractions[day - 1]]
562
+
563
  if day_attractions:
564
  daily_html += f"""
565
+ <div style="background:white;border-radius:12px;margin-bottom:20px;overflow:hidden;
566
+ box-shadow:0 2px 8px rgba(0,0,0,0.06);">
567
+ <div style="background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
568
+ padding:12px 20px;color:white;">
569
+ <h3 style="margin:0;font-size:1.2em;">Day {day} · {date_str}</h3>
570
  </div>
571
+ <div style="padding:20px;">
572
+ {"".join(attraction_image_html(a, "small") for a in day_attractions)}
573
+ <div style="margin-top:15px;padding-top:15px;border-top:1px solid #eee;">
574
+ <div style="margin-bottom:8px;">🍽️ <strong>Lunch:</strong> Ask locals for the best neighbourhood restaurant</div>
575
+ <div>🌙 <strong>Evening:</strong> Explore night markets, enjoy live music or a cultural performance</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  </div>
577
  </div>
578
+ </div>"""
579
+
580
+ # Tips
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
  tips_html = """
582
+ <div style="background:#f0f4ff;padding:22px;border-radius:12px;margin:20px 0;">
583
+ <h3 style="margin:0 0 15px 0;color:#667eea;">💡 Smart Travel Tips</h3>
584
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;">
585
+ <div>🎫 <strong>Book in Advance</strong><br>Save time and money by booking popular sites online</div>
586
+ <div>🚇 <strong>Public Transport</strong><br>A day pass often gives unlimited travel at lower cost</div>
587
+ <div>📱 <strong>Offline Maps</strong><br>Download Google Maps offline before you arrive</div>
588
+ <div>💵 <strong>Local Currency</strong><br>Carry some cash for markets and smaller vendors</div>
589
+ <div>🌍 <strong>Learn Basic Phrases</strong><br>A few local words are always warmly received</div>
590
+ <div>📸 <strong>Go Early</strong><br>Visit popular attractions first thing to beat the crowds</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  </div>
592
+ </div>"""
593
+
594
+ dest_encoded = destination.replace(" ", "+")
595
+ dep_encoded = departure_city.lower().replace(" ", "-") if departure_city else ""
596
+
597
+ booking_html = f"""
598
+ <div style="text-align:center;padding:22px;background:#f8f9fa;border-radius:12px;margin-top:20px;">
599
+ <h3 style="margin:0 0 12px 0;">Ready to Book Your Trip?</h3>
600
+ <p style="margin:0;">
601
+ 🛏️ <a href="https://www.booking.com/searchresults.html?ss={dest_encoded}" target="_blank"
602
+ style="color:#667eea;text-decoration:none;font-weight:600;">Search Hotels</a>
603
+ &nbsp;|&nbsp;
604
+ ✈️ <a href="https://www.skyscanner.net/transport/flights/{dep_encoded}/{destination.lower().replace(' ','-')}/"
605
+ target="_blank" style="color:#667eea;text-decoration:none;font-weight:600;">Search Flights</a>
606
+ &nbsp;|&nbsp;
607
+ 🎟️ <a href="https://www.tripadvisor.com/Search?q={dest_encoded}" target="_blank"
608
+ style="color:#667eea;text-decoration:none;font-weight:600;">Read Reviews</a>
609
+ </p>
610
+ </div>"""
611
+
612
+ footer_html = """
613
+ <div style="text-align:center;padding:20px;margin-top:16px;font-size:0.85em;color:#888;">
614
+ <p>✨ TravelBuddy AI — Powered by OpenAI · Real-time Weather · Wikipedia Images · Smart Recommendations</p>
615
+ <p>🌍 Plan your perfect adventure with confidence</p>
616
+ </div>"""
617
+
618
+ return f"""
619
+ <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:100%;">
620
+ {hero_html}
621
+ {stats_html}
622
  {budget_warning}
 
 
623
  {budget_html}
624
+ <div style="background:white;padding:22px;border-radius:12px;margin:20px 0;box-shadow:0 2px 10px rgba(0,0,0,0.05);">
625
+ <h2 style="margin:0 0 18px 0;color:#667eea;">✨ Top Attractions in {destination}</h2>
 
 
626
  {attractions_html}
627
  </div>
628
+ <div style="background:white;padding:22px;border-radius:12px;margin:20px 0;box-shadow:0 2px 10px rgba(0,0,0,0.05);">
629
+ <h2 style="margin:0 0 18px 0;color:#667eea;">📅 Your {num_days}-Day Itinerary</h2>
 
 
630
  {daily_html}
631
  </div>
 
 
632
  {tips_html}
633
+ {booking_html}
634
+ {footer_html}
635
+ </div>"""
636
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
 
638
  # -------------------- GRADIO INTERFACE --------------------
639
  css = """
640
  .gradio-container {
641
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
642
  max-width: 1200px;
643
  margin: 0 auto;
644
  }
 
646
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
647
  border: none !important;
648
  font-weight: bold !important;
 
649
  font-size: 1.1em !important;
 
 
 
 
 
 
 
 
 
 
 
 
650
  }
651
+ input, select, textarea { border-radius: 8px !important; border: 1px solid #e0e0e0 !important; }
652
+ label { font-weight: 500 !important; color: #333 !important; }
653
  """
654
 
655
  with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as demo:
656
  gr.HTML("""
657
+ <div style="text-align:center;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
658
+ padding:40px;border-radius:20px;margin-bottom:30px;">
659
+ <h1 style="color:white;margin:0;font-size:2.5em;">✨ TravelBuddy AI</h1>
660
+ <p style="color:white;margin:10px 0 0;opacity:0.95;font-size:1.1em;">
661
+ Your Intelligent Travel Companion — Personalized Itineraries for Any Destination Worldwide
662
  </p>
663
+ </div>""")
664
+
 
665
  with gr.Row(equal_height=True):
666
  with gr.Column(scale=2):
667
  with gr.Group():
668
  gr.Markdown("### 🎯 Where's Your Next Adventure?")
669
  destination = gr.Textbox(
670
  label="Destination",
671
+ placeholder="e.g., Nairobi, Paris, Tokyo, Cairo, Buenos Aires, Sydney…",
672
+ lines=1, show_label=False
 
673
  )
 
674
  with gr.Group():
675
  gr.Markdown("### 📅 When Are You Traveling?")
676
  with gr.Row():
 
680
  placeholder="YYYY-MM-DD"
681
  )
682
  num_days = gr.Slider(
683
+ label="Duration (Days)", minimum=1, maximum=14, value=3, step=1
 
 
 
 
684
  )
 
685
  with gr.Column(scale=1):
686
  with gr.Group():
687
  gr.Markdown("### 💰 Your Budget")
688
  with gr.Row():
689
  budget_amount = gr.Number(
690
+ label="Total Budget (Optional)", placeholder="Enter amount", value=None
 
 
691
  )
692
  budget_currency = gr.Dropdown(
693
+ ["USD", "EUR", "GBP", "JPY", "KES", "RWF", "ZAR", "NGN",
694
+ "EGP", "CAD", "AUD", "CHF", "INR", "BRL", "MXN"],
695
+ label="Currency", value="USD"
696
  )
697
  gr.HTML("""
698
+ <div style="background:#e8f0fe;padding:10px;border-radius:8px;margin-top:10px;">
699
+ <small>💡 We'll suggest the best travel style based on your budget.</small>
700
+ </div>""")
 
 
701
  with gr.Group():
702
  gr.Markdown("### ✈️ Departure Info")
703
  departure_city = gr.Textbox(
704
  label="Departure City (Optional)",
705
+ placeholder="e.g., Kigali, London, Lagos, Mumbai",
706
  lines=1
707
  )
708
+
709
  with gr.Row():
710
+ generate_btn = gr.Button(
711
+ "✨ Generate My Personalized Itinerary", variant="primary", size="lg"
712
+ )
713
+
714
  output = gr.HTML()
715
+
716
  generate_btn.click(
717
  fn=generate_itinerary,
718
  inputs=[destination, start_date, num_days, budget_amount, budget_currency, departure_city],
719
+ outputs=output,
720
  )
721
+
 
722
  gr.HTML("""
723
+ <div style="text-align:center;padding:20px;margin-top:20px;border-top:1px solid #eee;">
724
  <h3>🌟 Popular Destinations to Try</h3>
725
+ <div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
726
+ <button onclick="document.querySelector('input[placeholder*=Nairobi]').value='Nairobi';"
727
+ style="background:#f0f4ff;border:none;padding:8px 16px;border-radius:20px;cursor:pointer;">🦒 Nairobi</button>
728
+ <button onclick="document.querySelector('input[placeholder*=Nairobi]').value='Paris';"
729
+ style="background:#f0f4ff;border:none;padding:8px 16px;border-radius:20px;cursor:pointer;">🗼 Paris</button>
730
+ <button onclick="document.querySelector('input[placeholder*=Nairobi]').value='Tokyo';"
731
+ style="background:#f0f4ff;border:none;padding:8px 16px;border-radius:20px;cursor:pointer;">🗾 Tokyo</button>
732
+ <button onclick="document.querySelector('input[placeholder*=Nairobi]').value='New York';"
733
+ style="background:#f0f4ff;border:none;padding:8px 16px;border-radius:20px;cursor:pointer;">🗽 New York</button>
734
+ <button onclick="document.querySelector('input[placeholder*=Nairobi]').value='Bali';"
735
+ style="background:#f0f4ff;border:none;padding:8px 16px;border-radius:20px;cursor:pointer;">🏝️ Bali</button>
736
+ <button onclick="document.querySelector('input[placeholder*=Nairobi]').value='Cairo';"
737
+ style="background:#f0f4ff;border:none;padding:8px 16px;border-radius:20px;cursor:pointer;">🏛️ Cairo</button>
738
+ <button onclick="document.querySelector('input[placeholder*=Nairobi]').value='Cape Town';"
739
+ style="background:#f0f4ff;border:none;padding:8px 16px;border-radius:20px;cursor:pointer;">🌍 Cape Town</button>
740
+ <button onclick="document.querySelector('input[placeholder*=Nairobi]').value='Bangkok';"
741
+ style="background:#f0f4ff;border:none;padding:8px 16px;border-radius:20px;cursor:pointer;">🍜 Bangkok</button>
742
  </div>
743
  </div>
744
+
745
+ <div style="text-align:center;padding:16px;margin-top:10px;border-top:1px solid #eee;color:#888;">
746
+ <small>Powered by OpenAI · OpenWeather · Wikipedia Images · For travellers from every corner of the world ✨</small>
747
+ </div>""")
 
748
 
749
  if __name__ == "__main__":
750
+ demo.launch(share=False, server_name="0.0.0.0")