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