Update app.py
Browse files
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
|
| 17 |
client = None
|
| 18 |
else:
|
| 19 |
client = openai.OpenAI(api_key=OPENAI_API_KEY)
|
| 20 |
|
| 21 |
-
# Global
|
| 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 |
-
|
| 66 |
-
|
| 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 |
-
|
| 103 |
"action": "query",
|
| 104 |
-
"titles": city,
|
| 105 |
-
"prop": "pageimages",
|
| 106 |
-
"pithumbsize": 1200,
|
| 107 |
"format": "json",
|
| 108 |
-
"
|
|
|
|
|
|
|
| 109 |
}
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
except Exception:
|
| 120 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 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
|
| 138 |
}
|
| 139 |
try:
|
| 140 |
url = "https://api.openweathermap.org/data/2.5/weather"
|
| 141 |
-
params = {
|
| 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[
|
| 149 |
-
"feels_like": data[
|
| 150 |
-
"condition": data[
|
| 151 |
-
"humidity": data[
|
| 152 |
-
"wind_speed": data[
|
| 153 |
-
"precipitation": data.get(
|
| 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
|
| 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.
|
| 198 |
-
max_tokens=
|
| 199 |
)
|
| 200 |
content = response.choices[0].message.content.strip()
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
attractions = json.loads(content)
|
| 208 |
-
if isinstance(attractions, dict):
|
| 209 |
-
attractions = attractions
|
| 210 |
-
|
|
|
|
| 211 |
result = []
|
| 212 |
-
for a in attractions[:
|
| 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
|
| 239 |
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 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
|
| 257 |
-
{"name": "Louvre Museum", "description": "World's largest art museum
|
| 258 |
-
{"name": "
|
| 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": "
|
| 265 |
-
{"name": "Shibuya Crossing", "description": "
|
| 266 |
-
{"name": "
|
| 267 |
-
|
| 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 |
-
|
| 314 |
-
for key, attractions in curated.items():
|
| 315 |
-
if key in city_lower or city_lower in key:
|
| 316 |
return attractions
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 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": "
|
| 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":
|
| 355 |
-
"food":
|
| 356 |
-
"transport":
|
| 357 |
-
"activities":
|
| 358 |
-
"total":
|
| 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 |
-
|
| 369 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 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;'>β
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
weather_temp = f"{weather['temperature']:.1f}Β°C" if isinstance(weather['temperature'], (int, float)) else str(weather['temperature'])
|
| 411 |
-
weather_condition = weather[
|
| 412 |
-
|
|
|
|
| 413 |
budget_warning = ""
|
| 414 |
-
if budget_amount and budget_data[
|
| 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 |
-
|
| 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 Β·
|
| 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 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 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" (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 | ποΈ {attr['entry_fee']}
|
| 533 |
-
</div>
|
| 534 |
</div>
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
|
|
|
| 545 |
</div>
|
| 546 |
</div>
|
| 547 |
-
</div>
|
| 548 |
-
|
| 549 |
-
|
| 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
|
| 556 |
date_str = current_date.strftime("%A, %B %d")
|
| 557 |
-
|
|
|
|
| 558 |
end_idx = min(day * per_day, len(attractions))
|
| 559 |
day_attractions = attractions[start_idx:end_idx]
|
| 560 |
-
|
| 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 |
-
|
| 567 |
-
|
| 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 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
</div>
|
| 577 |
</div>
|
| 578 |
-
</div>
|
| 579 |
-
|
| 580 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
tips_html = """
|
| 582 |
-
<div style="background:#f0f4ff;padding:
|
| 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:
|
| 585 |
-
<div>π« <strong>Book in Advance</strong><br>Save time and money by booking popular
|
| 586 |
-
<div>π <strong>Public Transport</strong><br>
|
| 587 |
-
<div>π± <strong>Offline Maps</strong><br>Download Google Maps offline
|
| 588 |
-
<div>π΅ <strong>Local Currency</strong><br>Carry
|
| 589 |
-
<div>π <strong>Learn Basic Phrases</strong><br>A few local words
|
| 590 |
-
<div>πΈ <strong>
|
| 591 |
</div>
|
| 592 |
-
</div>
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
<
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
{budget_warning}
|
|
|
|
|
|
|
| 623 |
{budget_html}
|
| 624 |
-
|
| 625 |
-
|
|
|
|
|
|
|
| 626 |
{attractions_html}
|
| 627 |
</div>
|
| 628 |
-
|
| 629 |
-
|
|
|
|
|
|
|
| 630 |
{daily_html}
|
| 631 |
</div>
|
|
|
|
|
|
|
| 632 |
{tips_html}
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 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 |
-
|
| 659 |
-
<
|
| 660 |
-
|
| 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.,
|
| 672 |
-
lines=1,
|
|
|
|
| 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)",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)",
|
|
|
|
|
|
|
| 691 |
)
|
| 692 |
budget_currency = gr.Dropdown(
|
| 693 |
-
["USD", "EUR", "GBP", "JPY", "
|
| 694 |
-
|
| 695 |
-
|
| 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
|
| 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.,
|
| 706 |
lines=1
|
| 707 |
)
|
| 708 |
-
|
| 709 |
with gr.Row():
|
| 710 |
-
generate_btn = gr.Button(
|
| 711 |
-
|
| 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
|
| 727 |
-
|
| 728 |
-
<button onclick="document.querySelector('input
|
| 729 |
-
|
| 730 |
-
<button onclick="document.querySelector('input
|
| 731 |
-
|
| 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:
|
| 746 |
-
<small>Powered by OpenAI
|
| 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 |
+
|