NSamson1 commited on
Commit
803333c
Β·
verified Β·
1 Parent(s): 547cc10

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +393 -541
app.py CHANGED
@@ -1,10 +1,9 @@
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 re
8
  import random
9
  from typing import List, Dict, Any
10
 
@@ -13,115 +12,49 @@ 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,190 +67,104 @@ 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."""
@@ -325,8 +172,7 @@ def get_attractions(city: str) -> Dict:
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,297 +194,288 @@ 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,31 +483,44 @@ css = """
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,71 +530,73 @@ with gr.Blocks(css=css, title="✨ TravelBuddy AI - Smart Travel Planner") as de
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")
 
 
 
 
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
 
 
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.")
16
  client = None
17
  else:
18
  client = openai.OpenAI(api_key=OPENAI_API_KEY)
19
 
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:
 
67
  "humidity": 60,
68
  "wind_speed": 10,
69
  "precipitation": 0,
70
+ "note": "Demo mode - Add OpenWeather API key for real data"
71
  }
72
  try:
73
  url = "https://api.openweathermap.org/data/2.5/weather"
74
+ params = {'q': city, 'appid': OPENWEATHER_API_KEY, 'units': 'metric'}
75
  response = requests.get(url, params=params, timeout=10)
76
  data = response.json()
77
  if response.status_code != 200:
78
  return {"error": f"Weather API error: {data.get('message', 'unknown')}"}
79
+
80
  return {
81
  "city": city,
82
+ "temperature": data['main']['temp'],
83
+ "feels_like": data['main']['feels_like'],
84
+ "condition": data['weather'][0]['description'],
85
+ "humidity": data['main']['humidity'],
86
+ "wind_speed": data['wind']['speed'],
87
+ "precipitation": data.get('rain', {}).get('1h', 0)
88
  }
89
  except Exception as e:
90
  return {"error": f"Weather service unavailable: {str(e)}"}
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",
113
  messages=[{"role": "user", "content": prompt}],
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)
145
 
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."""
 
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
  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,
205
+ "food": food,
206
+ "transport": transport,
207
+ "activities": activities,
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:
218
+ return "<div style='color: red; padding: 20px; text-align: center;'>❌ Please enter a destination city.</div>"
219
+
220
  if num_days < 1 or num_days > 14:
221
+ return "<div style='color: red; padding: 20px; text-align: center;'>❌ Number of days must be between 1 and 14.</div>"
222
+
223
+ # Get data
224
  weather = get_weather(destination)
225
  if "error" in weather:
226
+ return f"<div style='color: red; padding: 20px; text-align: center;'>❌ Weather error: {weather['error']}</div>"
227
+
228
  attractions_data = get_attractions(destination)
229
  attractions = attractions_data["attractions"]
230
+
231
+ # Calculate budget
 
 
 
 
 
 
232
  budget_data = calculate_budget(num_days, budget_amount)
233
+
234
+ # Format dates
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
 
 
 
242
  )
243
+
244
+ return html
245
+
246
  except Exception as e:
247
+ return f"<div style='color: red; padding: 20px; text-align: center;'>❌ An unexpected error occurred: {str(e)}</div>"
248
+
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'])
256
+ weather_condition = weather['condition'].capitalize()
257
+
258
+ # Budget warning
259
  budget_warning = ""
260
+ if budget_amount and budget_data['total'] > budget_amount:
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
+
302
  for day in range(1, num_days + 1):
303
+ current_date = start_date + timedelta(days=day-1)
304
  date_str = current_date.strftime("%A, %B %d")
305
+
306
+ start_idx = (day-1) * per_day
307
  end_idx = min(day * per_day, len(attractions))
308
  day_attractions = attractions[start_idx:end_idx]
309
+
 
 
310
  if day_attractions:
311
  daily_html += f"""
312
+ <div style="background: white; border-radius: 12px; margin-bottom: 20px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
313
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 12px 20px; color: white;">
314
+ <h3 style="margin: 0; font-size: 1.2em;">Day {day} Β· {date_str}</h3>
 
 
315
  </div>
316
+ <div style="padding: 20px;">
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;">
325
+ <strong>{attr['name']}</strong><br>
326
+ <span style="font-size: 0.85em; color: #666;">{attr['description'][:100]}...</span>
327
+ <div style="font-size: 0.75em; color: #888; margin-top: 5px;">
328
+ ⏱️ {attr['duration_hours']} hrs | 🎟️ {attr['entry_fee']}
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>
345
+ """
346
+
347
+ # Budget breakdown
348
+ level_names = {"budget": "Budget", "moderate": "Moderate", "comfortable": "Comfortable", "luxury": "Luxury"}
349
+ level_display = level_names.get(budget_data['level'], "Moderate")
350
+
351
+ budget_html = f"""
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 ""}
409
+ </div>
410
+
411
+ <!-- Quick Stats -->
412
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
413
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
414
+ <div style="font-size: 2em;">🌀️</div>
415
+ <strong>{weather_temp}</strong><br>
416
+ <small>{weather_condition}</small>
417
+ </div>
418
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
419
+ <div style="font-size: 2em;">πŸ“…</div>
420
+ <strong>{num_days} Days</strong><br>
421
+ <small>Full Itinerary</small>
422
+ </div>
423
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
424
+ <div style="font-size: 2em;">🎯</div>
425
+ <strong>{len(attractions)}+ Attractions</strong><br>
426
+ <small>To Explore</small>
427
+ </div>
428
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 12px; text-align: center;">
429
+ <div style="font-size: 2em;">πŸ’°</div>
430
+ <strong>${budget_data['total']:.0f}</strong><br>
431
+ <small>Estimated Total</small>
432
+ </div>
433
+ </div>
434
+
435
  {budget_warning}
436
+
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}
450
  </div>
451
+
452
+ <!-- Travel Tips -->
453
  {tips_html}
454
+
455
+ <!-- Booking Links -->
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
+ """
472
+
473
+ return full_html
474
 
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
  }
 
483
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
484
  border: none !important;
485
  font-weight: bold !important;
486
+ padding: 12px 30px !important;
487
  font-size: 1.1em !important;
488
+ transition: transform 0.2s !important;
489
+ }
490
+ .gr-button-primary:hover {
491
+ transform: translateY(-2px) !important;
492
+ }
493
+ 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
+ """)
512
+
513
  with gr.Row(equal_height=True):
514
  with gr.Column(scale=2):
515
  with gr.Group():
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
  )
523
+
524
  with gr.Group():
525
  gr.Markdown("### πŸ“… When Are You Traveling?")
526
  with gr.Row():
 
530
  placeholder="YYYY-MM-DD"
531
  )
532
  num_days = gr.Slider(
533
+ label="Duration (Days)",
534
+ minimum=1,
535
+ maximum=14,
536
+ value=3,
537
+ step=1
538
  )
539
+
540
  with gr.Column(scale=1):
541
  with gr.Group():
542
  gr.Markdown("### πŸ’° Your Budget")
543
  with gr.Row():
544
  budget_amount = gr.Number(
545
+ label="Total Budget (Optional)",
546
+ placeholder="Enter amount",
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
+
560
  with gr.Group():
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
+
568
  with gr.Row():
569
+ generate_btn = gr.Button("✨ Generate My Personalized Itinerary", variant="primary", size="lg")
570
+
 
 
571
  output = gr.HTML()
572
+
573
  generate_btn.click(
574
  fn=generate_itinerary,
575
  inputs=[destination, start_date, num_days, budget_amount, budget_currency, departure_city],
576
+ outputs=output
577
  )
578
+
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
+