import pandas as pd from datetime import datetime AQI_BANDS = [ (0, 50, "Good", "#10b981", "#ecfdf5"), (51, 100, "Moderate", "#f59e0b", "#fffbeb"), (101, 150, "Unhealthy for Sensitive Groups", "#f97316", "#fff7ed"), (151, 200, "Unhealthy", "#ef4444", "#fef2f2"), (201, 300, "Very Unhealthy", "#8b5cf6", "#f5f3ff"), (301, 999, "Hazardous", "#7f1d1d", "#fef2f2"), ] AQI_TIPS = { "Good": "Great day to be outdoors!", "Moderate": "Sensitive groups should reduce prolonged outdoor exertion.", "Unhealthy for Sensitive Groups": "Limit outdoor activity if you have asthma or heart conditions.", "Unhealthy": "Wear a mask outdoors and keep windows closed.", "Very Unhealthy": "Avoid going outside; use an air purifier indoors.", "Hazardous": "Stay indoors — this is a health emergency level.", } CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); .gradio-container { background: #f0fdfa !important; background-image: linear-gradient(180deg, #99f6e4 0%, #f0fdfa 40%, #ffffff 100%) !important; font-family: 'Inter', sans-serif !important; max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 40px !important; } #main-container { max-width: 1400px; margin: 0 auto; width: 100%; } /* Header Styles */ .app-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; } .location-info h2 { margin: 0; font-size: 1.2rem; font-weight: 700; color: #0f172a; } .location-info p { margin: 0; font-size: 0.9rem; color: #64748b; } /* Card Styles */ .mobile-card { background: white !important; border-radius: 24px !important; border: none !important; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05) !important; padding: 20px !important; overflow: hidden; margin-top: 20px; } /* Status Card */ .status-hero { position: relative; border-radius: 24px; padding: 24px; color: white; text-align: left; overflow: hidden; margin-bottom: 20px; } .status-hero h1 { font-size: 2.5rem; font-weight: 800; margin: 0; } .status-hero p { font-size: 1.1rem; opacity: 0.9; margin: 4px 0 0 0; } /* Metric Grid */ .metric-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } .metric-item { background: #f8fafc; border-radius: 20px; padding: 16px; display: flex; flex-direction: column; gap: 8px; } .metric-top { display: flex; align-items: center; gap: 10px; } .metric-icon-bg { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; } .metric-label { font-size: 0.85rem; font-weight: 500; color: #64748b; } .metric-value { font-size: 1.1rem; font-weight: 700; color: #0f172a; } /* Forecast Grid */ .forecast-scroll { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: none; } .forecast-scroll::-webkit-scrollbar { display: none; } .forecast-item { min-width: 80px; text-align: center; padding: 12px; border-radius: 16px; background: #f8fafc; } .forecast-day { font-size: 0.8rem; font-weight: 600; color: #64748b; margin-bottom: 8px; } .forecast-icon { font-size: 1.5rem; margin-bottom: 8px; } .forecast-temp { font-size: 1rem; font-weight: 700; color: #0f172a; } /* Buttons */ #predict-btn { background: #0f172a !important; color: white !important; border-radius: 16px !important; font-weight: 600 !important; padding: 12px !important; } /* Window toggle */ #window-radio { display: flex; align-items: center; gap: 0 !important; } #window-radio .wrap { display: flex; gap: 6px; flex-direction: row !important; } #window-radio label { background: #f1f5f9; border-radius: 10px; padding: 4px 14px; font-size: 0.8rem; font-weight: 600; color: #64748b; cursor: pointer; border: none; } #window-radio input[type=radio]:checked + label, #window-radio label:has(input:checked) { background: #0f172a; color: white; } /* Tabs row alignment */ .tab-row-header { display: flex; justify-content: space-between; align-items: center; } """ def get_header_html(): now = datetime.now() date_str = now.strftime("%d %B %Y") return f"""

Karachi, Pakistan ⌵

{date_str}

""" def make_metric_cards(pm25, fc): n = len(fc) idx_24 = min(23, n - 1) idx_48 = min(47, n - 1) metrics = [ ("💨", "PM2.5", f"{pm25:.1f} µg/m³", "#ecfdf5", "#10b981"), ("🌡️", "Temp", "28°C", "#fffbeb", "#f59e0b"), # Static for demo or fetch ("💧", "Humidity", "65%", "#eff6ff", "#3b82f6"), ("🌬️", "Wind", "4.2 km/h", "#f5f3ff", "#8b5cf6") ] html = '
' for icon, label, value, bg, color in metrics: html += f"""
{icon}
{label}
{value}
""" html += '
' return html def make_day_cards(fc): n_days = max(1, len(fc) // 24) day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] now = datetime.now() start_day = now.weekday() html = '
' for d in range(min(n_days, 7)): start = d * 24 end = min(start + 24, len(fc)) chunk = fc[start:end] if len(chunk) == 0: break avg = sum(chunk) / len(chunk) # Determine icon based on AQI if avg < 50: icon = "☀️" elif avg < 100: icon = "🌤️" elif avg < 150: icon = "🌥️" else: icon = "🌫️" day_name = day_names[(start_day + d) % 7] if d == 0: day_name = "Today" html += f"""
{day_name}
{icon}
{avg:.0f}
""" html += '
' return html def get_status_hero(aqi): cat = "Good" color = "#10b981" bg = "#10b981" # Solid for now, or gradient for lo, hi, c, col, b in AQI_BANDS: if aqi <= hi: cat, color, bg = c, col, col break return f"""

Current Observation

{aqi:.0f}

Air Quality Index: {cat}

"""