Spaces:
Sleeping
Sleeping
import os | |
import gradio as gr | |
import requests | |
from openai import OpenAI | |
import ast | |
# API Keys | |
weather_api_key = os.getenv("openweather") | |
openai_api_key = os.getenv("OPENAI_API_KEY") | |
client = OpenAI(api_key=openai_api_key) | |
# Weather fetch | |
def get_weather(city_name): | |
if not city_name.strip(): | |
city_name = "Dubai" | |
try: | |
url = f"https://api.openweathermap.org/data/2.5/weather?q={city_name}&appid={weather_api_key}&units=metric" | |
data = requests.get(url).json() | |
if data.get("cod") == 200: | |
rain = data.get("rain", {}).get("1h", 0) | |
condition = data["weather"][0]["main"] | |
emoji_map = { | |
"Clear": "βοΈ", "Clouds": "βοΈ", "Rain": "π§οΈ", | |
"Snow": "βοΈ", "Thunderstorm": "βοΈ", "Drizzle": "π¦οΈ", | |
"Mist": "π«οΈ", "Haze": "π", "Fog": "π«οΈ" | |
} | |
emoji = emoji_map.get(condition, "π") | |
return { | |
"city": data["name"], | |
"country": data["sys"]["country"], | |
"temperature": int(data["main"]["temp"]), | |
"feels_like": int(data["main"]["feels_like"]), | |
"humidity": data["main"]["humidity"], | |
"pressure": data["main"]["pressure"], | |
"description": f"{data['weather'][0]['description'].title()} {emoji}", | |
"wind_speed": data["wind"]["speed"], | |
"visibility": data.get("visibility", 10000) // 1000, | |
"rain_chance": f"{rain} mm" | |
} | |
return None | |
except Exception: | |
return None | |
# Weather display | |
def format_weather_display(data): | |
if not data: | |
return "<div style='text-align:center; color: #e74c3c; font-size: 18px; padding: 40px;'>β City not found. Please try again.</div>" | |
font_color = "#2d3436" | |
card_bg = "#e3f2fd" | |
main_bg = "#ffffff" | |
return f""" | |
<div style="background: {main_bg}; border-radius: 16px; padding: 25px; box-shadow: 0 10px 30px rgba(0,0,0,0.1);"> | |
<div style="text-align: center; margin-bottom: 25px;"> | |
<h2 style="margin: 0; color: {font_color}; font-size: 24px; font-weight: 600;">π {data['city']}, {data['country']}</h2> | |
<h1 style="margin: 10px 0; font-size: 64px; color: {font_color}; font-weight: 300;">{data['temperature']}Β°C</h1> | |
<p style="margin: 5px 0; color: {font_color}; font-size: 18px; font-weight: 500;">{data['description']}</p> | |
</div> | |
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-top: 25px;"> | |
<div style="background: {card_bg}; border-radius: 12px; padding: 16px; text-align: center; color: {font_color};"> | |
<div style="font-size: 24px; margin-bottom: 8px;">π§</div> | |
<strong style="font-size: 16px; display: block;">{data['rain_chance']}</strong> | |
<span style="font-size: 12px; opacity: 0.8;">Precipitation</span> | |
</div> | |
<div style="background: {card_bg}; border-radius: 12px; padding: 16px; text-align: center; color: {font_color};"> | |
<div style="font-size: 24px; margin-bottom: 8px;">π</div> | |
<strong style="font-size: 16px; display: block;">{data['pressure']} mb</strong> | |
<span style="font-size: 12px; opacity: 0.8;">Pressure</span> | |
</div> | |
<div style="background: {card_bg}; border-radius: 12px; padding: 16px; text-align: center; color: {font_color};"> | |
<div style="font-size: 24px; margin-bottom: 8px;">π¨</div> | |
<strong style="font-size: 16px; display: block;">{data['wind_speed']} km/h</strong> | |
<span style="font-size: 12px; opacity: 0.8;">Wind Speed</span> | |
</div> | |
<div style="background: {card_bg}; border-radius: 12px; padding: 16px; text-align: center; color: {font_color};"> | |
<div style="font-size: 24px; margin-bottom: 8px;">π‘οΈ</div> | |
<strong style="font-size: 16px; display: block;">{data['feels_like']}Β°C</strong> | |
<span style="font-size: 12px; opacity: 0.8;">Feels Like</span> | |
</div> | |
<div style="background: {card_bg}; border-radius: 12px; padding: 16px; text-align: center; color: {font_color};"> | |
<div style="font-size: 24px; margin-bottom: 8px;">ποΈ</div> | |
<strong style="font-size: 16px; display: block;">{data['visibility']} km</strong> | |
<span style="font-size: 12px; opacity: 0.8;">Visibility</span> | |
</div> | |
<div style="background: {card_bg}; border-radius: 12px; padding: 16px; text-align: center; color: {font_color};"> | |
<div style="font-size: 24px; margin-bottom: 8px;">π¦</div> | |
<strong style="font-size: 16px; display: block;">{data['humidity']}%</strong> | |
<span style="font-size: 12px; opacity: 0.8;">Humidity</span> | |
</div> | |
</div> | |
</div> | |
""" | |
# Chatbot | |
def travel_chat(msg, history): | |
messages = [{"role": "system", "content": "You are a helpful travel assistant. Suggest tourist attractions, activities, and travel tips for any city."}] | |
for h in history: | |
messages.append({"role": h["role"], "content": h["content"]}) | |
messages.append({"role": "user", "content": msg}) | |
try: | |
response = client.chat.completions.create( | |
model="gpt-4o-mini", | |
messages=messages, | |
temperature=0.7 | |
) | |
reply = response.choices[0].message.content | |
except Exception: | |
reply = "β οΈ Unable to fetch suggestions right now. Try again later." | |
history.append({"role": "user", "content": msg}) | |
history.append({"role": "assistant", "content": reply}) | |
return history, history | |
# Attractions | |
def get_attractions(city, country, temp, weather_desc): | |
try: | |
messages = [ | |
{"role": "system", "content": ( | |
"You are a travel expert. Based on the city, country, and current weather, " | |
"suggest 6 to 9 top attractions. Return each as a Python tuple like: " | |
"(\"Burj Khalifa\", \"World's tallest building with stunning views.\")." | |
)}, | |
{"role": "user", "content": ( | |
f"Suggest attractions for {city}, {country} (Weather: {temp}Β°C, {weather_desc}). " | |
"Output as Python tuples only." | |
)} | |
] | |
response = client.chat.completions.create( | |
model="gpt-4o-mini", | |
messages=messages | |
) | |
content = response.choices[0].message.content.strip() | |
attractions = [] | |
for line in content.splitlines(): | |
line = line.strip().rstrip(',') | |
if line.startswith("(") and line.endswith(")"): | |
try: | |
attraction_tuple = ast.literal_eval(line) | |
if isinstance(attraction_tuple, tuple) and len(attraction_tuple) == 2: | |
attractions.append(attraction_tuple) | |
except Exception: | |
continue | |
return attractions if attractions else [("No attractions found", "Try another city.")] | |
except Exception: | |
return [("Error", "Could not fetch attractions.")] | |
def format_attraction_card(name, details): | |
return f""" | |
<div class='card'> | |
<div class='card-title'>{name}</div> | |
<div class='card-desc'>{details}</div> | |
</div> | |
""" | |
# CSS | |
custom_css = """ | |
body, .gradio-container { | |
background: linear-gradient(135deg, #f5f7fa 0%, #bbdefb 100%) !important; | |
font-family: 'Segoe UI', 'Roboto', sans-serif; | |
min-height: 100vh; | |
} | |
#main-title { | |
text-align: center; | |
font-size: 2.8rem; | |
font-weight: 800; | |
margin: 20px 0; | |
color: #0d47a1; | |
} | |
.section-header { | |
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%); | |
color: white; | |
padding: 12px 20px; | |
border-radius: 12px 12px 0 0; | |
margin: 0; | |
font-size: 1.3rem; | |
font-weight: 600; | |
text-align: center; | |
} | |
.content-box { | |
background-color: #ffffff; | |
border-radius: 0 0 16px 16px; | |
padding: 25px; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
min-height: 520px; | |
border-top: 3px solid #1976d2; | |
} | |
.card-grid { | |
display: grid; | |
grid-template-columns: repeat(3, 1fr); | |
gap: 18px; | |
margin-top: 25px; | |
} | |
.card { | |
background: #ffffff; | |
border: 1px solid #bbdefb; | |
border-radius: 16px; | |
padding: 20px; | |
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.1); | |
transition: transform 0.2s ease, box-shadow 0.2s ease; | |
} | |
.card:hover { | |
transform: translateY(-4px); | |
box-shadow: 0 8px 20px rgba(25, 118, 210, 0.2); | |
} | |
.card-title { | |
font-size: 20px; | |
font-weight: 600; | |
color: #1565c0; | |
margin-bottom: 12px; | |
text-align: center; | |
} | |
.card-desc { | |
font-size: 15px; | |
color: #444; | |
line-height: 1.5; | |
text-align: center; | |
} | |
.footer { | |
text-align: center; | |
font-size: 14px; | |
padding: 20px; | |
color: #555; | |
margin-top: 20px; | |
} | |
""" | |
def generate_attraction_cards(city): | |
weather = get_weather(city) | |
if not weather: | |
return "<div style='padding:20px; color:red;'>β οΈ Couldn't fetch attractions due to missing weather data.</div>" | |
attractions = get_attractions( | |
city=weather["city"], | |
country=weather["country"], | |
temp=weather["temperature"], | |
weather_desc=weather["description"] | |
) | |
return "<div class='card-grid'>" + "".join(format_attraction_card(name, details) for name, details in attractions) + "</div>" | |
# UI | |
def launch_ui(): | |
with gr.Blocks(css=custom_css, title="TripMate AI") as demo: | |
# Header | |
gr.HTML("<div id='main-title'>π TripMate AI</div>") | |
with gr.Row(equal_height=True): | |
with gr.Column(scale=1): | |
gr.HTML("<div class='section-header'>π€οΈ Weather Dashboard</div>") | |
with gr.Group(elem_classes="content-box"): | |
city_input = gr.Textbox(label="ποΈ City Name", value="Dubai") | |
update_btn = gr.Button("π Get Weather Data") | |
weather_html = gr.HTML() | |
with gr.Column(scale=1): | |
gr.HTML("<div class='section-header'>π€ Travel Assistant</div>") | |
with gr.Group(elem_classes="content-box"): | |
chat = gr.Chatbot(height=350, type="messages") | |
with gr.Row(): | |
message = gr.Textbox(placeholder="Ask about places to visit, local attractions...", show_label=False, scale=4) | |
ask_btn = gr.Button("Send", scale=1) | |
state = gr.State([]) | |
gr.HTML("<div class='section-header'>ποΈ Top Attractions You Can Visit</div>") | |
attractions_html = gr.HTML() | |
def update_all(city): | |
return format_weather_display(get_weather(city)), generate_attraction_cards(city) | |
update_btn.click(fn=update_all, inputs=city_input, outputs=[weather_html, attractions_html]) | |
city_input.submit(fn=update_all, inputs=city_input, outputs=[weather_html, attractions_html]) | |
ask_btn.click(fn=travel_chat, inputs=[message, state], outputs=[chat, state]) | |
message.submit(fn=travel_chat, inputs=[message, state], outputs=[chat, state]) | |
demo.load(fn=lambda: update_all("Dubai"), inputs=None, outputs=[weather_html, attractions_html]) | |
# Footer | |
gr.HTML("<div class='footer'>Β© 2025 TripMate AI β Your AI-powered travel companion.</div>") | |
demo.launch() | |
launch_ui() | |