Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import requests | |
| import os | |
| from datetime import datetime | |
| API_KEY = os.getenv("OPENWEATHER_API_KEYY") | |
| BASE_URL = "http://api.openweathermap.org/data/2.5/weather" | |
| def get_weather(city): | |
| try: | |
| if not API_KEY: | |
| raise ValueError("API key not configured") | |
| params = { | |
| "q": city, | |
| "appid": API_KEY, | |
| "units": "metric" | |
| } | |
| res = requests.get(BASE_URL, params=params, timeout=10) | |
| res.raise_for_status() | |
| data = res.json() | |
| temp = data["main"]["temp"] | |
| condition = data["weather"][0]["description"].capitalize() | |
| humidity = data["main"]["humidity"] | |
| wind_speed = data.get("wind", {}).get("speed", 0) | |
| # Weather icon mapping | |
| weather_id = data["weather"][0]["icon"] | |
| icon_map = { | |
| "01d": "βοΈ", "01n": "π", "02d": "β ", "02n": "β ", | |
| "03d": "βοΈ", "03n": "βοΈ", "04d": "βοΈ", "04n": "βοΈ", | |
| "09d": "π§οΈ", "09n": "π§οΈ", "10d": "π¦οΈ", "10n": "π¦οΈ", | |
| "11d": "βοΈ", "11n": "βοΈ", "13d": "π¨οΈ", "13n": "π¨οΈ", | |
| "50d": "π«οΈ", "50n": "π«οΈ" | |
| } | |
| icon = icon_map.get(weather_id, "π€οΈ") | |
| # Generate hourly forecast (text only) | |
| hourly = [ | |
| f"{datetime.now().strftime('%H:%M')} β {round(temp + (i - 2))}Β°C" | |
| for i in range(6) | |
| ] | |
| return temp, condition, humidity, wind_speed, icon, hourly, city | |
| except Exception as e: | |
| return f"Error: {str(e)}", "", "", "", "β", [], city | |
| def create_weather_display(temp, condition, humidity, wind_speed, icon, hourly, city): | |
| """Create beautiful weather display HTML with added classes for responsiveness""" | |
| if isinstance(temp, str) and "Error" in temp: | |
| # Error case | |
| return f""" | |
| <div class="weather-main" style=" | |
| background: linear-gradient(135deg, #2b2b2b 0%, #3a3a3a 100%); | |
| padding: 40px; | |
| border-radius: 24px; | |
| box-shadow: 0 0 40px rgba(203,77,11,0.2); | |
| color: white; | |
| font-family: 'Inter', 'SF Pro Display', sans-serif; | |
| text-align: center; | |
| border: 1px solid rgba(203,77,11,0.2); | |
| "> | |
| <div style="font-size: 80px; margin-bottom: 20px;">β</div> | |
| <h2 style="color: rgb(203,77,11); margin: 0;">{temp}</h2> | |
| <p style="color: #ccc; margin-top: 10px;">Please check your city name and API key</p> | |
| </div> | |
| """ | |
| # Format temperature | |
| temp_display = f"{round(temp)}" if isinstance(temp, (int, float)) else temp | |
| # Create main weather display with classes | |
| main_display = f""" | |
| <div class="weather-main" style=" | |
| background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%); | |
| padding: 48px; | |
| border-radius: 24px; | |
| box-shadow: 0 0 50px rgba(203,77,11,0.15), 0 20px 40px rgba(0,0,0,0.3); | |
| color: white; | |
| font-family: 'Inter', 'SF Pro Display', sans-serif; | |
| margin: 24px 0; | |
| border: 1px solid rgba(203,77,11,0.2); | |
| backdrop-filter: blur(20px); | |
| "> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 36px;"> | |
| <h2 style="color: rgb(203,77,11); font-size: 32px; margin: 0; font-weight: 700; letter-spacing: -0.5px;"> | |
| π¦ Weather Wizard | |
| </h2> | |
| <div style=" | |
| color: white; | |
| font-size: 28px; | |
| font-weight: 600; | |
| background: linear-gradient(135deg, rgba(203,77,11,0.2) 0%, rgba(255,140,60,0.1) 100%); | |
| padding: 12px 24px; | |
| border-radius: 16px; | |
| border: 1px solid rgba(203,77,11,0.3); | |
| "> | |
| π {city.title()} | |
| </div> | |
| </div> | |
| <div class="weather-flex" style="display: flex; justify-content: space-between; align-items: center;"> | |
| <div style="flex: 1;"> | |
| <h1 style=" | |
| font-size: 84px; | |
| margin: 0; | |
| font-weight: 200; | |
| background: linear-gradient(135deg, white 0%, rgba(203,77,11,0.8) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| letter-spacing: -2px; | |
| ">{temp_display}Β°C</h1> | |
| <p style="font-size: 28px; margin: 12px 0 0 0; color: #e0e0e0; font-weight: 500;">{condition}</p> | |
| <div style="margin-top: 32px; display: flex; gap: 48px; font-size: 20px;"> | |
| <div style=" | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| background: rgba(203,77,11,0.1); | |
| padding: 12px 20px; | |
| border-radius: 12px; | |
| border: 1px solid rgba(203,77,11,0.2); | |
| "> | |
| <span style="font-size: 24px;">π¨</span> | |
| <span style="font-weight: 600;">{wind_speed} m/s</span> | |
| </div> | |
| <div style=" | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| background: rgba(203,77,11,0.1); | |
| padding: 12px 20px; | |
| border-radius: 12px; | |
| border: 1px solid rgba(203,77,11,0.2); | |
| "> | |
| <span style="font-size: 24px;">π§</span> | |
| <span style="font-weight: 600;">{humidity}%</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="flex: 1; text-align: center;"> | |
| <div class="weather-icon" style=" | |
| font-size: 140px; | |
| margin: 20px 0; | |
| filter: drop-shadow(0 0 20px rgba(203,77,11,0.3)); | |
| ">{icon}</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| # Create hourly forecast with classes | |
| hourly_html = "" | |
| if hourly: | |
| hourly_items = "" | |
| for forecast in hourly: | |
| # Extract temp and time from the format "HH:MM β XXXΒ°C" | |
| parts = forecast.split(' β ') | |
| time_part = parts[0] if len(parts) > 0 else "" | |
| temp_part = parts[1].replace('Β°C', 'Β°') if len(parts) > 1 else "" | |
| hourly_items += f""" | |
| <div style=" | |
| text-align: center; | |
| flex: 1; | |
| padding: 20px 10px; | |
| background: linear-gradient(135deg, rgba(203,77,11,0.1) 0%, rgba(255,140,60,0.05) 100%); | |
| border-radius: 16px; | |
| margin: 0 4px; | |
| border: 1px solid rgba(203,77,11,0.2); | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| "> | |
| <div style=" | |
| font-size: 22px; | |
| font-weight: 700; | |
| margin-bottom: 12px; | |
| color: white; | |
| background: linear-gradient(135deg, white 0%, rgb(203,77,11) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| "> | |
| {temp_part} | |
| </div> | |
| <div style="font-size: 16px; color: #ccc; font-weight: 500;"> | |
| {time_part} | |
| </div> | |
| </div> | |
| """ | |
| hourly_html = f""" | |
| <div class="hourly-forecast" style=" | |
| background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%); | |
| padding: 36px; | |
| border-radius: 24px; | |
| box-shadow: 0 0 50px rgba(203,77,11,0.15), 0 20px 40px rgba(0,0,0,0.3); | |
| color: white; | |
| font-family: 'Inter', 'SF Pro Display', sans-serif; | |
| margin-top: 24px; | |
| border: 1px solid rgba(203,77,11,0.2); | |
| backdrop-filter: blur(20px); | |
| "> | |
| <h3 style=" | |
| margin: 0 0 24px 0; | |
| color: rgb(203,77,11); | |
| font-size: 24px; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| ">π Hourly Forecast</h3> | |
| <div class="hourly-items" style="display: flex; gap: 8px; justify-content: space-between;"> | |
| {hourly_items} | |
| </div> | |
| </div> | |
| """ | |
| return main_display + hourly_html | |
| # Custom CSS with media queries for responsiveness | |
| custom_css = """ | |
| body, .gradio-container { | |
| background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%) !important; | |
| color: white !important; | |
| font-family: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif !important; | |
| } | |
| /* Modern search bar styling */ | |
| .search-container input { | |
| background: linear-gradient(135deg, rgba(203,77,11,0.1) 0%, rgba(255,140,60,0.05) 100%) !important; | |
| color: white !important; | |
| border: 2px solid rgba(203,77,11,0.3) !important; | |
| border-radius: 16px !important; | |
| padding: 16px 24px !important; | |
| font-size: 18px !important; | |
| font-weight: 500 !important; | |
| backdrop-filter: blur(10px) !important; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| box-shadow: 0 8px 32px rgba(203,77,11,0.1) !important; | |
| } | |
| .search-container input:focus { | |
| border-color: rgb(203,77,11) !important; | |
| box-shadow: 0 0 0 4px rgba(203,77,11,0.2), 0 12px 40px rgba(203,77,11,0.15) !important; | |
| background: linear-gradient(135deg, rgba(203,77,11,0.15) 0%, rgba(255,140,60,0.08) 100%) !important; | |
| transform: translateY(-1px) !important; | |
| } | |
| .search-container input::placeholder { | |
| color: rgba(255,255,255,0.6) !important; | |
| font-weight: 400 !important; | |
| } | |
| /* 2025 Modern Button */ | |
| .modern-button { | |
| background: linear-gradient(135deg, rgb(203,77,11) 0%, #ff6b35 100%) !important; | |
| border: none !important; | |
| padding: 16px 32px !important; | |
| border-radius: 16px !important; | |
| font-weight: 600 !important; | |
| font-size: 16px !important; | |
| letter-spacing: 0.5px !important; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| box-shadow: 0 8px 32px rgba(203,77,11,0.25) !important; | |
| position: relative !important; | |
| overflow: hidden !important; | |
| color: white !important; | |
| } | |
| .modern-button:hover { | |
| transform: translateY(-3px) !important; | |
| box-shadow: 0 16px 40px rgba(203,77,11,0.4) !important; | |
| background: linear-gradient(135deg, #e8550d 0%, #ff7c47 100%) !important; | |
| } | |
| .modern-button:active { | |
| transform: translateY(-1px) !important; | |
| } | |
| .gradio-html { | |
| background: transparent !important; | |
| } | |
| /* Enhanced container styling */ | |
| .gradio-container { | |
| background: radial-gradient(ellipse at top, rgba(203,77,11,0.1) 0%, transparent 70%) !important; | |
| } | |
| /* Media queries for mobile responsiveness */ | |
| @media (max-width: 768px) { | |
| .header { | |
| padding: 16px !important; | |
| } | |
| .header h1 { | |
| font-size: 28px !important; | |
| } | |
| .header p { | |
| font-size: 16px !important; | |
| } | |
| .search-container input { | |
| padding: 12px 16px !important; | |
| font-size: 16px !important; | |
| } | |
| .modern-button { | |
| padding: 12px 24px !important; | |
| font-size: 14px !important; | |
| } | |
| .weather-main { | |
| padding: 24px !important; | |
| } | |
| .weather-flex { | |
| flex-direction: column !important; | |
| } | |
| .weather-flex > div { | |
| width: 100% !important; | |
| text-align: center !important; | |
| } | |
| .weather-main h1 { | |
| font-size: 48px !important; | |
| } | |
| .weather-main p { | |
| font-size: 20px !important; | |
| } | |
| .weather-icon { | |
| font-size: 100px !important; | |
| margin: 20px 0 !important; | |
| } | |
| .hourly-forecast { | |
| padding: 24px !important; | |
| } | |
| .hourly-items { | |
| overflow-x: auto !important; | |
| flex-wrap: nowrap !important; | |
| } | |
| .hourly-items > div { | |
| min-width: 100px !important; | |
| margin: 0 4px !important; | |
| } | |
| } | |
| """ | |
| # Create Gradio interface | |
| with gr.Blocks(css=custom_css, title="Weather Wizard", theme=gr.themes.Glass()) as app: | |
| gr.HTML(""" | |
| <div class="header" style=" | |
| text-align: center; | |
| padding: 32px; | |
| background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%); | |
| border-bottom: 1px solid rgba(203,77,11,0.2); | |
| "> | |
| <h1 style=" | |
| color: rgb(203,77,11); | |
| font-size: 42px; | |
| margin: 0; | |
| font-family: 'Inter', 'SF Pro Display', sans-serif; | |
| font-weight: 800; | |
| letter-spacing: -1px; | |
| background: linear-gradient(135deg, rgb(203,77,11) 0%, #ff8c3c 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| "> | |
| π¦οΈ Weather Wizard | |
| </h1> | |
| <p style=" | |
| color: rgba(255,255,255,0.7); | |
| font-size: 20px; | |
| margin: 12px 0 0 0; | |
| font-weight: 400; | |
| letter-spacing: 0.5px; | |
| "> | |
| AI-powered weather forecasts for the modern world | |
| </p> | |
| </div> | |
| """) | |
| with gr.Column(elem_classes=["search-section"]): | |
| city = gr.Textbox( | |
| label="", | |
| placeholder="Search any city worldwide... (e.g. New York, Tokyo, London)", | |
| container=False, | |
| elem_classes=["search-container"], | |
| show_label=False | |
| ) | |
| btn = gr.Button( | |
| "β¨ Discover Weather", | |
| variant="primary", | |
| size="lg", | |
| elem_classes=["modern-button"] | |
| ) | |
| # Weather Display | |
| weather_display = gr.HTML() | |
| def update_weather_display(city): | |
| if not city.strip(): | |
| city = "London" # Default city | |
| temp, condition, humidity, wind_speed, icon, hourly, city_name = get_weather(city.strip()) | |
| weather_html = create_weather_display(temp, condition, humidity, wind_speed, icon, hourly, city_name) | |
| return weather_html | |
| # Event handlers | |
| btn.click( | |
| fn=update_weather_display, | |
| inputs=city, | |
| outputs=weather_display | |
| ) | |
| city.submit( | |
| fn=update_weather_display, | |
| inputs=city, | |
| outputs=weather_display | |
| ) | |
| # Load default weather on startup | |
| app.load( | |
| fn=lambda: update_weather_display("London"), | |
| outputs=weather_display | |
| ) | |
| if __name__ == "__main__": | |
| app.launch() |