"""
app.py
------
EcoAgent — Gradio Blocks UI with 4 tabs:
Chat | Dashboard | Recycling Guide | Household Profile
Features:
- IBM Granite 4 H Small via watsonx.ai (ibm-watsonx-ai APIClient)
- Clean light-theme CSS, no dark mode
- Session-based eco score, action logging, impact dashboard
- India-specific recycling guide and product recommendations
- Household/family profile with habit tracking
- Hugging Face Spaces compatible
"""
import os
import logging
# Fix SSL certificate path for environments where SSL_CERT_FILE points to wrong Python
if "SSL_CERT_FILE" not in os.environ or not os.path.isfile(os.environ.get("SSL_CERT_FILE", "")):
try:
import certifi
os.environ["SSL_CERT_FILE"] = certifi.where()
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
except ImportError:
pass
import gradio as gr
from dotenv import load_dotenv
from watsonx_client import (
get_eco_answer,
get_recycling_guide,
compute_session_impact,
IMPACT_TABLE,
PRODUCT_RECS,
INDIAN_CITIES,
)
from agent import agent_loop, format_tool_calls
# ---------------------------------------------------------------------------
load_dotenv(".env")
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Custom CSS — light-only eco green theme, polished cards and layout
# ---------------------------------------------------------------------------
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--eco-primary: #2e7d50;
--eco-secondary: #43a868;
--eco-light: #f0f7f3;
--eco-bg: #fcfcfd;
--eco-card: #ffffff;
--eco-text: #1c1c1e;
--eco-muted: #6b6b6f;
--eco-border: #e4e4e7;
--eco-shadow: 0 1px 3px rgba(0,0,0,0.04);
--radius: 6px;
}
body, .gradio-container {
font-family: 'Inter', system-ui, sans-serif !important;
background: var(--eco-bg) !important;
color: var(--eco-text) !important;
}
/* ── Gradio component overrides for ultra-light look ── */
.gradio-container { background: var(--eco-bg) !important; }
.gradio-container .wrap { background: var(--eco-bg) !important; }
/* Tabs */
.tab-nav button {
font-weight: 500 !important;
color: var(--eco-muted) !important;
border: none !important;
border-bottom: 2px solid transparent !important;
background: transparent !important;
border-radius: 0 !important;
padding: 8px 16px !important;
}
.tab-nav button.selected {
color: var(--eco-primary) !important;
border-bottom: 2.5px solid var(--eco-primary) !important;
font-weight: 600 !important;
}
.tab-nav button:hover {
color: var(--eco-primary) !important;
background: var(--eco-light) !important;
}
/* Buttons */
button.primary, button.primary.lg {
background: var(--eco-primary) !important;
border-color: var(--eco-primary) !important;
color: #fff !important;
border-radius: var(--radius) !important;
font-weight: 600 !important;
}
button.primary:hover {
background: #256a42 !important;
border-color: #256a42 !important;
}
button.secondary, button.secondary.lg {
background: var(--eco-light) !important;
border: 1px solid var(--eco-border) !important;
color: var(--eco-text) !important;
border-radius: var(--radius) !important;
font-weight: 500 !important;
}
button.secondary:hover {
background: #e4eff0 !important;
border-color: var(--eco-border) !important;
}
/* Textbox / Dropdown / Slider */
textarea, input[type="text"], .wrap textarea, .wrap input[type="text"] {
border: 1px solid var(--eco-border) !important;
border-radius: var(--radius) !important;
background: var(--eco-card) !important;
color: var(--eco-text) !important;
}
textarea:focus, input[type="text"]:focus {
border-color: var(--eco-primary) !important;
box-shadow: 0 0 0 2px rgba(46,125,80,0.12) !important;
}
/* Fix Gradio 6 Dropdown*/
.gr-dropdown-menu,
.gr-select-dropdown,
.gradio-dropdown-menu,
[role="listbox"] {
background: #ffffff !important;
border: 1px solid #dcdcdc !important;
color: #1c1c1e !important;
}
.gr-dropdown-menu li,
.gr-select-dropdown li,
[role="option"] {
background: #ffffff !important;
color: #1c1c1e !important;
padding: 10px 14px !important;
}
.gr-dropdown-menu li:hover,
.gr-select-dropdown li:hover,
[role="option"]:hover,
[aria-selected="true"] {
background: #e8f5e9 !important;
color: #2e7d50 !important;
}
/* Slider */
input[type="range"] {
accent-color: #2e7d50 !important;
}
input[type="range"]::-webkit-slider-thumb {
background: #2e7d50 !important;
}
input[type="range"]::-webkit-slider-runnable-track {
background: #d8e9dc !important;
}
input[type="range"]::-moz-range-thumb {
background: #2e7d50 !important;
}
input[type="range"]::-moz-range-track {
background: #d8e9dc !important;
}
/* CheckboxGroup — pills/chips style */
/* ---------- CheckboxGroup ---------- */
.gr-checkboxgroup label,
.gradio-checkbox label {
background: #f0f7f3 !important;
border: 1px solid #d6e7da !important;
color: #1c1c1e !important;
border-radius: 18px !important;
padding: 8px 14px !important;
}
.gr-checkboxgroup label:hover,
.gradio-checkbox label:hover {
background: #e6f3ea !important;
}
.gr-checkboxgroup input:checked + span,
.gradio-checkbox input:checked + span {
color: white !important;
}
.gr-checkboxgroup label:has(input:checked),
.gradio-checkbox label:has(input:checked) {
background: #2e7d50 !important;
border-color: #2e7d50 !important;
}
/* Chatbot */
.message.user {
background: var(--eco-light) !important;
border: 1px solid var(--eco-border) !important;
border-radius: var(--radius) !important;
}
.message.bot {
background: var(--eco-card) !important;
border: 1px solid var(--eco-border) !important;
border-radius: var(--radius) !important;
}
/* Prevent white flash during model thinking */
#chatbot, #chatbot > .wrap, #chatbot > .wrap > .panel {
background: var(--eco-bg) !important;
}
/* Markdown headings inside tabs */
h3, h2 { color: var(--eco-text) !important; }
/* ── Header ── */
.eco-header {
background: var(--eco-card);
border: 1px solid var(--eco-border);
border-radius: var(--radius);
padding: 22px 28px;
margin-bottom: 16px;
color: var(--eco-text);
box-shadow: var(--eco-shadow);
}
.eco-header h1 {
margin: 0 0 4px;
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.3px;
color: var(--eco-primary);
}
.eco-header p {
margin: 0;
font-size: 0.85rem;
color: var(--eco-muted);
}
.eco-model-badge {
display: inline-block;
margin-top: 8px;
background: var(--eco-light);
border: 1px solid var(--eco-border);
border-radius: 20px;
padding: 3px 12px;
font-size: 0.73rem;
font-weight: 600;
color: var(--eco-primary);
letter-spacing: 0.3px;
}
/* ── Metric cards ── */
.metric-card {
background: var(--eco-card);
border: 1px solid var(--eco-border);
border-radius: var(--radius);
padding: 18px 16px;
text-align: center;
box-shadow: var(--eco-shadow);
}
.metric-value {
font-size: 1.8rem;
font-weight: 700;
color: var(--eco-primary);
line-height: 1.1;
}
.metric-label {
font-size: 0.7rem;
color: var(--eco-muted);
margin-top: 3px;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: 500;
}
.metric-unit {
font-size: 0.82rem;
color: var(--eco-muted);
}
/* ── Score ring ── */
.score-ring-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.score-ring {
width: 80px; height: 80px; border-radius: 50%;
background: conic-gradient(var(--eco-secondary) calc(var(--pct) * 1%), #eaeaea 0);
display: flex; align-items: center; justify-content: center;
position: relative;
}
.score-ring::after {
content: '';
width: 60px; height: 60px;
background: var(--eco-card);
border-radius: 50%;
position: absolute;
}
.score-number {
font-size: 1rem; font-weight: 700;
color: var(--eco-primary);
position: relative; z-index: 1;
}
.score-label {
font-size: 0.68rem;
color: var(--eco-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.score-count {
background: var(--eco-light);
color: var(--eco-primary);
border: 1px solid var(--eco-border);
border-radius: 20px;
padding: 2px 10px;
font-size: 0.72rem;
font-weight: 600;
}
/* ── Recycling output ── */
.recycling-output {
background: var(--eco-card);
border: 1px solid var(--eco-border);
border-radius: var(--radius);
padding: 14px;
box-shadow: var(--eco-shadow);
}
/* ── Product cards ── */
.product-card {
background: var(--eco-card);
border-left: 3px solid var(--eco-secondary);
border-radius: 0 var(--radius) var(--radius) 0;
padding: 10px 14px;
margin: 5px 0;
box-shadow: var(--eco-shadow);
font-size: 0.86rem;
color: var(--eco-text);
}
/* ── Profile card ── */
.profile-card {
background: var(--eco-card);
border: 1px solid var(--eco-border);
border-radius: var(--radius);
padding: 14px;
box-shadow: var(--eco-shadow);
}
/* ── Disclaimer ── */
.disclaimer {
font-size: 0.72rem;
color: var(--eco-muted);
text-align: center;
padding: 8px 0 4px;
border-top: 1px solid var(--eco-border);
margin-top: 10px;
}
/* Agent Mode container */
.gr-form,
.gr-block,
.gr-group {
background: #fcfcfd !important;
border: 1px solid #e4e4e7 !important;
}
/* Agent Mode row */
#agent-mode {
background: #ffffff !important;
border: 1px solid #e4e4e7 !important;
border-radius: 8px !important;
padding: 10px !important;
}
#agent-mode label {
color: #1c1c1e !important;
}
#agent-mode input:checked {
accent-color: #2e7d50 !important;
}
#agent-mode-row {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
}
/* Checkbox container */
.gr-checkbox {
background: #ffffff !important;
border: 1px solid #e4e4e7 !important;
border-radius: 6px !important;
padding: 8px 12px !important;
}
/* Checkbox label */
.gr-checkbox label {
color: #1c1c1e !important;
font-weight: 500;
}
"""
# ---------------------------------------------------------------------------
# Static HTML — clean light header, no dark-mode button
# ---------------------------------------------------------------------------
HEADER_HTML = """
"""
DISCLAIMER_HTML = """
Answers are AI-generated by IBM Granite. Impact figures marked [Lookup] are from IPCC/BEE/CPCB data;
[Estimate] figures are model-generated. Verify government schemes at official sources.
Not financial or legal advice.
"""
# ---------------------------------------------------------------------------
# Quick-action chips for logging eco actions
# ---------------------------------------------------------------------------
ACTION_CHIPS = [
("cloth_bags", "Used cloth bags"),
("led_bulbs", "Switched to LED"),
("short_shower", "Short shower"),
("no_plastic_bottles", "Used steel bottle"),
("public_transport", "Public transport"),
("composting", "Composted waste"),
("segregate_waste", "Segregated waste"),
("seasonal_local_produce","Bought local produce"),
("line_dry_clothes", "Line-dried clothes"),
("fix_water_leaks", "Fixed a water leak"),
]
EXAMPLE_PROMPTS = [
"How can I reduce plastic use in my Indian kitchen?",
"What government schemes help with solar panel installation in India?",
"Give me a week-long plan to reduce my family's water usage.",
"How do I properly dispose of old mobile phones and laptops?",
"What are the most eco-friendly travel options in a Tier-2 Indian city?",
]
# ---------------------------------------------------------------------------
# Dashboard HTML builder
# ---------------------------------------------------------------------------
def _build_dashboard_html(impact: dict, profile: dict) -> str:
score = impact.get("eco_score", 0)
co2 = impact.get("co2_kg_year", 0.0)
water = impact.get("water_L_day", 0.0)
waste = impact.get("waste_kg_year", 0.0)
actions = impact.get("actions_count", 0)
location = profile.get("location", "India") if profile else "India"
members = profile.get("members", 1) if profile else 1
return f"""
{score}
Eco Score
{actions} action{'s' if actions != 1 else ''} logged
{members}
household member{'s' if members != 1 else ''}
{location}
Estimated annual savings if logged actions are sustained
{co2:,.0f}
kg CO₂
Carbon Avoided
{water:,.0f}
L / day
Water Saved
{waste:,.0f}
kg / year
Waste Diverted
{_equivalencies_html(co2, water) if co2 > 0 or water > 0 else ""}
{_actions_html(impact)}
"""
def _equivalencies_html(co2: float, water: float) -> str:
lines = []
if co2 > 0:
trees = round(co2 / 21)
lines.append(f"CO₂ savings equivalent to planting {trees} trees per year")
if water > 0:
baths = round(water / 150)
lines.append(f"Water savings equivalent to {baths} bucket baths per day")
if not lines:
return ""
return (
""
+ "
".join(lines)
+ "
"
)
def _actions_html(impact: dict) -> str:
count = impact.get("actions_count", 0)
if count == 0:
return (
""
"No actions logged yet. Chat with EcoAgent, then use the action buttons "
"to log what you have done today.
"
)
return (
f""
f"You have logged {count} unique action{'s' if count != 1 else ''} this session. "
f"Each sustained action compounds over the year.
"
)
# ---------------------------------------------------------------------------
# Recycling products HTML builder
# ---------------------------------------------------------------------------
def _build_products_html(material: str) -> str:
recs = PRODUCT_RECS.get(material, [])
if not recs:
return ""
items = "".join(f'{r}
' for r in recs)
return (
f""
f"
"
f"Eco-friendly alternatives and resources for {material}:
"
f"{items}
"
)
# ---------------------------------------------------------------------------
# Profile summary HTML builder
# ---------------------------------------------------------------------------
def _build_profile_html(profile: dict) -> str:
if not profile or not profile.get("name"):
return "No profile saved yet.
"
habits = profile.get("habits", [])
habit_list = (
""
+ "".join(f"- {h}
" for h in habits)
+ "
"
if habits
else "No habits recorded.
"
)
return f"""
{profile['name']}
Location: {profile.get('location','—')}
Members: {profile.get('members',1)}
Current eco habits:
{habit_list}
"""
# ---------------------------------------------------------------------------
# Core chat callback
# ---------------------------------------------------------------------------
def chat_submit(
message: str,
history: list,
profile_state: dict,
actions_state: list,
agent_mode: bool = False,
):
"""Handle a user message: call Granite, update history.
Gradio 6.x passes history as list[dict] where content is a list of
content blocks: [{"role": "user", "content": [{"type": "text", "text": "..."}]}].
We extract plain text for the API call and return structured blocks for display.
When agent_mode is True, uses the agentic loop with tool calls.
"""
if not message or not message.strip():
return history, profile_state, actions_state, _score_html(actions_state), ""
# Gradio 6 delivers history with structured content blocks
# Extract plain text for the watsonx API call
messages: list[dict] = []
for turn in history:
role = turn.get("role", "")
content = turn.get("content", "")
if role in ("user", "assistant") and content:
# Content can be a string or a list of content blocks
if isinstance(content, list):
text = " ".join(
block.get("text", "") for block in content
if isinstance(block, dict) and block.get("type") == "text"
)
else:
text = str(content)
if text:
messages.append({"role": role, "content": text})
messages.append({"role": "user", "content": message})
tool_display = ""
try:
if agent_mode:
# Use agentic loop with tools
reply, tool_calls = agent_loop(message, profile_state, messages[:-1])
tool_display = format_tool_calls(tool_calls)
else:
reply = get_eco_answer(messages, profile_state)
except EnvironmentError as exc:
reply = (
"**Setup required.**\n\n"
f"{exc}\n\n"
"Please set your credentials in `.env` and restart the app."
)
except RuntimeError as exc:
logger.error("watsonx error: %s", exc)
reply = (
"**EcoAgent is temporarily unavailable.**\n\n"
f"Error: {exc}\n\n"
"Please check your internet connection or try again in a moment."
)
except Exception as exc: # noqa: BLE001
logger.exception("Unexpected error in chat_submit")
reply = f"An unexpected error occurred ({type(exc).__name__}). Please try again."
# Append in Gradio 6 structured content block format
history = history + [
{"role": "user", "content": [{"type": "text", "text": message}]},
{"role": "assistant", "content": [{"type": "text", "text": reply}]},
]
return history, profile_state, actions_state, _score_html(actions_state), tool_display
def _score_html(actions_state: list) -> str:
impact = compute_session_impact(actions_state)
score = impact["eco_score"]
count = impact["actions_count"]
return (
f""
f"
"
f"{score}
"
f"
Eco Score"
f"
{count} action{'s' if count != 1 else ''} "
)
def log_action(slug: str, actions_state: list) -> tuple[list, str]:
"""Toggle an action slug in the logged actions list."""
if slug in actions_state:
actions_state = [a for a in actions_state if a != slug]
else:
actions_state = actions_state + [slug]
return actions_state, _score_html(actions_state)
def update_dashboard(actions_state: list, profile_state: dict) -> str:
members = profile_state.get("members", 1) if profile_state else 1
impact = compute_session_impact(actions_state, members)
return _build_dashboard_html(impact, profile_state or {})
def _search_local_recycling(material: str, city: str) -> str:
"""Search for local recycling centers and latest info using web search."""
from tools import execute_tool
# Build search queries for comprehensive results
queries = [
f"{material} recycling center {city} India",
f"{material} waste disposal {city} India 2026",
]
all_results = []
for query in queries:
try:
result = execute_tool("web_search", {"query": query})
if result and "No results" not in result and "Error" not in result:
# Extract just the search results, not the header
lines = result.split("\n")
# Find the numbered results
for line in lines:
if line.strip().startswith("1.") or line.strip().startswith("2.") or line.strip().startswith("3."):
all_results.append(line.strip())
break # Stop after first successful query
except Exception:
continue
if all_results:
return (
f"\n\n---\n\n"
f"### Local Recycling Options in {city}\n\n"
f"{chr(10).join(all_results[:5])}\n\n"
f"*For the most current information, please verify directly with these centers.*"
)
else:
return (
f"\n\n---\n\n"
f"### Local Recycling Options in {city}\n\n"
f"*Web search did not return specific results for {material} recycling in {city}. "
f"Please contact your local municipal corporation or waste management authority for current information.*"
)
def get_recycling(material: str, city: str) -> tuple[str, str, str]:
"""Fetch guide + local info + product recs for a material/city combination.
Returns:
Tuple of (guide_markdown, local_info_markdown, products_html)
"""
if not material or not city:
return "Please select both a material and a city.", "", ""
# 1. Get LLM-generated guide (existing)
try:
guide = get_recycling_guide(material, city)
except EnvironmentError as exc:
guide = f"Setup required: {exc}"
except RuntimeError as exc:
guide = f"Could not fetch guide: {exc}"
# 2. Web search for local recycling info (NEW)
local_info = _search_local_recycling(material, city)
# 3. Product recommendations (existing)
products_html = _build_products_html(material)
return guide, local_info, products_html
def save_profile(
name: str,
location: str,
members: int,
habits: list,
actions_state: list,
) -> tuple[dict, str, str]:
"""Save household profile and return updated state + HTML summary."""
profile = {
"name": name.strip() if name else "My Household",
"location": location,
"members": int(members),
"habits": habits,
}
summary = _build_profile_html(profile)
dashboard = update_dashboard(actions_state, profile)
return profile, summary, dashboard
# ===========================================================================
# GRADIO BLOCKS APP
# ===========================================================================
with gr.Blocks(title="EcoAgent — Eco Lifestyle Assistant") as demo:
profile_state = gr.State({})
actions_state = gr.State([])
gr.HTML(HEADER_HTML)
with gr.Tabs(elem_classes="tab-nav"):
# ================================================================
# TAB 1: CHAT
# ================================================================
with gr.Tab("Chat"):
with gr.Row():
with gr.Column(scale=3):
chatbot = gr.Chatbot(
height=430,
show_label=False,
elem_id="chatbot",
placeholder=(
""
"
Welcome to EcoAgent
"
"
Ask me anything about sustainable living, "
"eco travel, recycling, or Indian government green schemes.
"
"
"
),
)
# Agent Mode toggle and tool call display
with gr.Row(elem_id="agent-mode-row"):
agent_mode_toggle = gr.Checkbox(
label="Agent Mode",
value=False,
info="Multi-step reasoning with tools",
scale=1,
elem_id="agent-mode",
)
tool_calls_display = gr.Markdown(
"",
visible=True,
label="Tool Calls",
)
with gr.Row():
msg_input = gr.Textbox(
placeholder="Ask EcoAgent a question...",
show_label=False,
scale=5,
lines=1,
)
send_btn = gr.Button("Send",variant="primary", scale=1)
gr.Markdown("**Quick start questions:**")
with gr.Row():
for prompt in EXAMPLE_PROMPTS[:3]:
ex_btn = gr.Button(prompt, size="sm", variant="secondary")
ex_btn.click(lambda p=prompt: p, outputs=msg_input)
with gr.Row():
for prompt in EXAMPLE_PROMPTS[3:]:
ex_btn = gr.Button(prompt, size="sm", variant="secondary")
ex_btn.click(lambda p=prompt: p, outputs=msg_input)
with gr.Column(scale=1, min_width=190):
score_display = gr.HTML(_score_html([]))
gr.Markdown("**Log today's actions:**")
chip_btns = []
for slug, label in ACTION_CHIPS:
btn = gr.Button(label, size="sm", variant="secondary")
chip_btns.append((slug, btn))
def _submit(message, history, profile, actions, agent_mode):
return chat_submit(message, history, profile, actions, agent_mode)
send_btn.click(
_submit,
inputs=[msg_input, chatbot, profile_state, actions_state, agent_mode_toggle],
outputs=[chatbot, profile_state, actions_state, score_display, tool_calls_display],
).then(lambda: "", outputs=msg_input)
msg_input.submit(
_submit,
inputs=[msg_input, chatbot, profile_state, actions_state, agent_mode_toggle],
outputs=[chatbot, profile_state, actions_state, score_display, tool_calls_display],
).then(lambda: "", outputs=msg_input)
for slug, btn in chip_btns:
btn.click(
lambda a, s=slug: log_action(s, a),
inputs=[actions_state],
outputs=[actions_state, score_display],
)
# ================================================================
# TAB 2: DASHBOARD
# ================================================================
with gr.Tab("Dashboard"):
gr.Markdown(
"### Your Eco Impact Dashboard\n"
"Log actions in the Chat tab to see your estimated savings grow."
)
dashboard_html = gr.HTML(
_build_dashboard_html(compute_session_impact([]), {}),
)
refresh_btn = gr.Button("Refresh Dashboard", variant="secondary")
refresh_btn.click(
update_dashboard,
inputs=[actions_state, profile_state],
outputs=dashboard_html,
)
# ================================================================
# TAB 3: RECYCLING GUIDE
# ================================================================
with gr.Tab("Recycling Guide"):
gr.Markdown(
"### Local Recycling Guide\n"
"Select a waste material and your city to get India-specific "
"recycling instructions and eco-friendly product alternatives.\n\n"
"*Includes web search for local recycling centers in your city.*"
)
with gr.Row():
material_dd = gr.Dropdown(
choices=list(PRODUCT_RECS.keys()),
label="Waste Material",
value="E-waste",
scale=1,
)
city_dd = gr.Dropdown(
choices=INDIAN_CITIES,
label="Your City",
value="Bangalore",
scale=1,
)
guide_btn = gr.Button("Get Recycling Guide", variant="primary", scale=1)
guide_output = gr.Markdown(
"*Select a material and city above, then click Get Recycling Guide.*",
label="Recycling Instructions",
elem_classes="recycling-output",
)
local_output = gr.Markdown(
"",
label="Local Recycling Centers",
elem_classes="recycling-output",
)
products_output = gr.HTML("", label="Eco-Friendly Alternatives")
guide_btn.click(
get_recycling,
inputs=[material_dd, city_dd],
outputs=[guide_output, local_output, products_output],
)
material_dd.change(
lambda m: _build_products_html(m),
inputs=material_dd,
outputs=products_output,
)
# ================================================================
# TAB 4: PROFILE
# ================================================================
with gr.Tab("Profile"):
gr.Markdown(
"### Household Profile\n"
"Set your household details so EcoAgent can give personalised, "
"family-scale advice relevant to your location."
)
with gr.Row():
with gr.Column(scale=2):
hh_name = gr.Textbox(
label="Household Name",
placeholder="e.g. The Sharma Family",
)
with gr.Row():
hh_location = gr.Dropdown(
choices=INDIAN_CITIES + [
"Other - North India", "Other - South India",
"Other - East India", "Other - West India",
"Rural area",
],
label="City / Location",
value="Bangalore",
scale=2,
)
hh_members = gr.Slider(
minimum=1, maximum=12, value=4, step=1,
label="Household Members",
scale=2,
)
hh_habits = gr.CheckboxGroup(
choices=[
"Vegetarian / vegan diet",
"Use public transport regularly",
"Have solar panels installed",
"Compost kitchen waste",
"Use cloth / jute bags",
"Avoid single-use plastics",
"Harvest rainwater",
"Segregate wet and dry waste",
"Use LED bulbs throughout",
"Grow some food at home",
],
label="Current Eco Habits (select all that apply)",
)
save_btn = gr.Button("Save Profile", variant="primary")
with gr.Column(scale=1):
profile_summary = gr.HTML(
""
"Fill in your details and click Save Profile.
",
)
# Save updates profile_state, profile HTML, and dashboard
save_btn.click(
save_profile,
inputs=[hh_name, hh_location, hh_members, hh_habits, actions_state],
outputs=[profile_state, profile_summary, dashboard_html],
)
# ── Footer disclaimer ──────────────────────────────────────────────────
gr.HTML(DISCLAIMER_HTML)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
eco_theme = gr.themes.Soft(
primary_hue="green",
secondary_hue="emerald",
neutral_hue="gray",
font="Inter, system-ui, sans-serif",
font_mono="Fira Code, monospace",
).set(
body_background_fill="#fcfcfd",
body_background_fill_dark="#fcfcfd",
body_text_color="#1c1c1e",
body_text_color_dark="#1c1c1e",
block_background_fill="#ffffff",
block_border_color="#e4e4e7",
block_label_text_color="#4a4a4e",
block_title_text_color="#1c1c1e",
input_background_fill="#ffffff",
input_border_color="#e4e4e7",
input_border_color_focus="#2e7d50",
button_primary_background_fill="#2e7d50",
button_primary_background_fill_hover="#256a42",
button_primary_border_color="#2e7d50",
button_primary_text_color="#ffffff",
button_secondary_background_fill="#f0f7f3",
button_secondary_background_fill_hover="#e4eff0",
button_secondary_border_color="#e4e4e7",
button_secondary_text_color="#1c1c1e",
checkbox_background_color="#ffffff",
checkbox_background_color_selected="#2e7d50",
checkbox_border_color="#e4e4e7",
checkbox_border_color_selected="#2e7d50",
checkbox_label_text_color="#1c1c1e",
checkbox_label_text_color_selected="#ffffff",
checkbox_label_background_fill="#f0f7f3",
checkbox_label_background_fill_hover="#e4eff0",
checkbox_label_background_fill_selected="#2e7d50",
checkbox_label_border_color="#e4e4e7",
checkbox_label_border_color_selected="#2e7d50",
checkbox_label_border_width="1px",
slider_color="#2e7d50",
)
demo.launch(
server_name="0.0.0.0",
server_port=int(os.environ.get("PORT", 7860)),
share=False,
theme=eco_theme,
css=CUSTOM_CSS,
)