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() | |