Liat2025 commited on
Commit
8dde4c9
Β·
verified Β·
1 Parent(s): 7ee6115

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +273 -295
app.py CHANGED
@@ -1,314 +1,292 @@
1
- # app.py β€” WaterWise Home (uses /tmp cache; loads dataset from HF; returns top-3)
2
 
3
  import os
4
- import json
5
- import random
6
- from datetime import datetime
7
-
8
- import pandas as pd
9
  import gradio as gr
10
- from sentence_transformers import SentenceTransformer, util
11
-
12
- # ======= FIX: put HF caches in a writable dir (/tmp) =======
13
- CACHE_DIR = "/tmp/hf_cache"
14
- os.environ["HOME"] = "/tmp"
15
- os.environ["HF_HOME"] = CACHE_DIR
16
- os.environ["TRANSFORMERS_CACHE"] = CACHE_DIR
17
- os.environ["HUGGINGFACE_HUB_CACHE"] = CACHE_DIR
18
- os.environ["SENTENCE_TRANSFORMERS_HOME"] = CACHE_DIR
19
- os.makedirs(CACHE_DIR, exist_ok=True)
20
- # ===========================================================
21
-
22
- # =======================
23
- # Config & Theme
24
- # =======================
25
- DATA_URL = "https://huggingface.co/datasets/Liat2025/waterwise_tips_2025/resolve/main/tips_dataset.csv"
26
- HISTORY_FILE = "history.json"
27
-
28
- NAVY = "#0f2c56"
29
- LIGHT_BLUE = "#e6f2ff"
30
- BUTTON_BLUE = "#287bff"
31
-
32
- QUOTES = [
33
- "Every drop counts. πŸŒπŸ’§",
34
- "Small changes make a big wave. 🌊",
35
- "Water saved today is life saved tomorrow. 🌱",
36
- "The future flows from the choices you make. πŸ’¦",
37
- "Your efforts ripple into something bigger. 🌊",
38
- "Conserve water, preserve life. 🌍",
39
- "Drops become streams, streams become rivers. 🌊",
40
- "Saving water saves the planet. 🌱",
41
- ]
42
-
43
- SEASONS = ["❄️ Winter", "🌸 Spring", "β˜€οΈ Summer", "πŸ‚ Fall"]
44
-
45
- SEASON_CONTEXT = {
46
- "❄️ Winter": "indoor efficiency, pipe protection, heater settings, hot water, low outdoor watering",
47
- "🌸 Spring": "garden prep, soil moisture, planting, rain barrels, irrigation tune-up",
48
- "β˜€οΈ Summer": "hot weather, evaporation, early morning watering, mulch, lawn care, outdoor hoses",
49
- "πŸ‚ Fall": "leaf cleanup, aeration, winterizing irrigation, reduce watering frequency, shutoff valves",
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  SEASON_KEYWORDS = {
52
- "❄️ Winter": ["heater", "insulate", "pipe", "hot water", "freeze", "indoors"],
53
- "🌸 Spring": ["plant", "soil", "rain barrel", "sprinkler", "garden prep", "seedling"],
54
- "β˜€οΈ Summer": ["mulch", "evaporation", "morning", "hose", "sprinkler", "lawn", "shade"],
55
- "πŸ‚ Fall": ["winterize", "shutoff", "aerate", "leaf", "drain", "blowout"],
56
  }
57
- SEASON_BONUS = 0.08
58
-
59
- # =======================
60
- # Data Loading (from HF Dataset)
61
- # =======================
62
- def load_tips():
63
- # Robust CSV read: take FIRST column only, skip malformed rows
64
- df = pd.read_csv(
65
- DATA_URL,
66
- engine="python",
67
- usecols=[0], # only the first column
68
- names=["tip"], # call that column "tip"
69
- header=0, # first row is header
70
- on_bad_lines="skip" # skip problematic lines
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  )
72
- tips = df["tip"].astype(str).str.strip()
73
- tips = tips[tips.str.len() > 0].drop_duplicates().tolist()
74
- if len(tips) < 10:
75
- raise ValueError(f"Need at least ~10 tips to run; found {len(tips)}.")
76
- return tips
77
-
78
- TIPS = load_tips()
79
-
80
- # =======================
81
- # Embeddings
82
- # =======================
83
- MODEL = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
84
- TIP_EMB = MODEL.encode(
85
- TIPS, convert_to_tensor=True, normalize_embeddings=True, show_progress_bar=False
86
- )
87
-
88
- # =======================
89
- # Helpers
90
- # =======================
91
- def estimate_water_saved(tip_text: str) -> int:
92
- s = tip_text.lower()
93
- if "shower" in s: return 9
94
- if "dishwasher" in s: return 6
95
- if "laundry" in s: return 7
96
- if "garden" in s or "watering" in s: return 4
97
- if "tap" in s or "faucet" in s: return 3
98
- return 2
99
-
100
- def encourage():
101
- return random.choice([
102
- "Nice! Small changes add up fast πŸ’§",
103
- "Great step β€” keep going! 🌱",
104
- "You’re building better habits already πŸ™Œ",
105
- "Love the effort β€” consistency wins! ⭐",
106
- ])
107
-
108
- def load_history():
109
- if not os.path.exists(HISTORY_FILE):
110
- return [], 0
111
- try:
112
- with open(HISTORY_FILE, "r", encoding="utf-8") as f:
113
- raw = f.read().strip()
114
- if not raw:
115
- return [], 0
116
- data = json.loads(raw)
117
- if not isinstance(data, list):
118
- return [], 0
119
- except Exception:
120
- return [], 0
121
- total = sum(int(item.get("liters_saved", 0)) for item in data)
122
- return data, total
123
-
124
- def save_history(data):
125
- with open(HISTORY_FILE, "w", encoding="utf-8") as f:
126
- json.dump(data, f, indent=2, ensure_ascii=False)
127
-
128
- def _plain_season(s):
129
- return (s or "").split()[-1] if s else ""
130
-
131
- def total_saved_card_html(total_liters: int):
132
- showers = total_liters / 9
133
- dishwashers = total_liters / 6
134
- watering_cans = total_liters / 4
135
- quote = random.choice(QUOTES)
136
- return f"""
137
- <div id='stat-card'>
138
- <div class='stat-number'>πŸ’§ {total_liters} liters saved</div>
139
- <div class='stat-quote'>{quote}</div>
140
- <div class='stat-impact'>β‰ˆ {showers:.1f} showers β€’ {dishwashers:.1f} dishwasher loads β€’ {watering_cans:.1f} watering cans</div>
141
- </div>
142
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- # =======================
145
- # Core: recommend & persist (season-aware)
146
- # =======================
147
- def recommend(activity, season):
148
- if not activity or not activity.strip():
149
- _, total = load_history()
150
- return "_Please describe a water-using activity first._", "", total_saved_card_html(total)
151
-
152
- ctx = SEASON_CONTEXT.get(season or "", "")
153
- query = activity.strip()
154
- if season:
155
- query += f" β€” season: {season}. Focus on: {ctx}"
156
-
157
- q_emb = MODEL.encode([query], convert_to_tensor=True, normalize_embeddings=True)
158
- sims = util.cos_sim(q_emb, TIP_EMB)[0].detach().cpu().tolist()
159
-
160
- if season:
161
- keys = SEASON_KEYWORDS.get(season, [])
162
- boosted = []
163
- for i, base in enumerate(sims):
164
- tip_lower = TIPS[i].lower()
165
- bonus = SEASON_BONUS if any(k.lower() in tip_lower for k in keys) else 0.0
166
- boosted.append((i, base + bonus))
167
- boosted.sort(key=lambda x: x[1], reverse=True)
168
- top_idx = [i for i, _ in boosted[:3]]
169
- else:
170
- top_idx = sorted(range(len(sims)), key=lambda i: sims[i], reverse=True)[:3]
171
-
172
- picked = [TIPS[i] for i in top_idx]
173
-
174
- if season:
175
- keys_l = [k.lower() for k in SEASON_KEYWORDS.get(season, [])]
176
- badged = []
177
- for i, t in enumerate(picked):
178
- mark = " _(great for " + season.split()[-1] + ")_" if any(k in t.lower() for k in keys_l) else ""
179
- badged.append(f"**{i+1}.** {t}{mark}")
180
- tips_md = "\n\n".join(badged)
181
- else:
182
- tips_md = "\n\n".join([f"**{i+1}.** {t}" for i, t in enumerate(picked)])
183
-
184
- saving = sum(estimate_water_saved(t) for t in picked)
185
-
186
- history, _ = load_history()
187
- history.append({
188
- "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
189
- "activity": activity.strip(),
190
- "season": _plain_season(season),
191
- "tips": picked,
192
- "liters_saved": saving,
193
- })
194
- save_history(history)
195
- _, total = load_history()
196
-
197
- return tips_md, encourage(), total_saved_card_html(total)
198
-
199
- def clear_all():
200
- save_history([])
201
- return "", "", total_saved_card_html(0)
202
-
203
- # =======================
204
- # UI (with CSS)
205
- # =======================
206
- CUSTOM_CSS = f"""
207
- :root {{ color-scheme: light !important; }}
208
- body, .gradio-container{{
209
- background-color: {NAVY} !important;
210
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, Noto Sans;
211
- color: #e6f2ff;
212
- }}
213
- #header-box{{
214
- background: linear-gradient(135deg, {LIGHT_BLUE}, #ffffff);
215
- padding: 16px; border-radius: 16px; text-align:center;
216
- box-shadow: 0 6px 14px rgba(0,0,0,0.12); margin: 12px auto 10px;
217
- }}
218
- #header-box h1{{ color: {NAVY}; font-size: 2rem; font-weight: 800; margin:0 0 6px; }}
219
- #header-box p{{ color: {NAVY}; margin:0; }}
220
- .label, .gr-markdown, .markdown-body, .gr-html{{ color:#e6f2ff !important; }}
221
- textarea, input{{
222
- background:#ffffff !important; color:#0b2540 !important;
223
- border:2px solid {BUTTON_BLUE} !important; border-radius:10px !important;
224
- }}
225
- button{{
226
- background:{BUTTON_BLUE} !important; color:#ffffff !important;
227
- font-weight:700 !important; border-radius:10px !important; border:none !important;
228
- box-shadow:0 4px 10px rgba(0,0,0,.18);
229
- }}
230
- #season_pills .gr-radio .wrap {{ display:flex; flex-wrap:wrap; gap:12px; }}
231
- #season_pills input[type="radio"] {{ position:absolute; opacity:0; width:0; height:0; }}
232
- #season_pills .gr-radio .wrap > input[type="radio"] + label {{
233
- display:inline-block; padding:14px 22px; border-radius:14px;
234
- background:#1c3e76; color:#ffffff; border:2px solid transparent;
235
- cursor:pointer; font-weight:800; letter-spacing:.2px;
236
- box-shadow:0 3px 10px rgba(0,0,0,.15);
237
- transition:transform .04s ease, background .15s ease, box-shadow .15s ease;
238
- }}
239
- #season_pills .gr-radio .wrap > input[type="radio"] + label:hover {{
240
- transform: translateY(-1px); box-shadow:0 6px 16px rgba(0,0,0,.22);
241
- }}
242
- #season_pills .gr-radio .wrap > input[type="radio"]:checked + label {{
243
- background:{BUTTON_BLUE}; border-color:#ffffff; color:#ffffff;
244
- box-shadow:0 10px 22px rgba(40,123,255,.40); transform: translateY(-1px);
245
- }}
246
- #season_pills .gr-radio .wrap > input[type="radio"]:focus + label {{
247
- outline:3px solid #a8c7ff; outline-offset:2px;
248
- }}
249
- #stat-card{{ background:linear-gradient(135deg, {LIGHT_BLUE}, #ffffff); border-radius:12px; padding:14px; text-align:center; box-shadow:0 6px 14px rgba(0,0,0,.12); }}
250
- #stat-card .stat-number{{ font-size:1.7rem; font-weight:900; color:{BUTTON_BLUE}; }}
251
- #stat-card .stat-quote{{ margin-top:6px; font-size:1.05rem; color:{NAVY}; opacity:.9; }}
252
- #stat-card .stat-impact{{ margin-top:8px; font-size:.95rem; color:{NAVY}; opacity:.85; font-weight:600; }}
253
- #examples_label {{ color:#cfe3ff; opacity:.9; margin-top:6px; }}
254
- """
255
 
256
- with gr.Blocks(css=CUSTOM_CSS, title="WaterWise Home") as demo:
257
- with gr.Column(elem_id="header-box"):
258
- gr.Markdown("<h1>πŸ’§ WaterWise Home</h1>")
259
- gr.Markdown("<p>I'm your smart chip! Tell me how you used water today, and I'll track your usage, give tips, and challenge you to save more.</p>")
260
 
261
- with gr.Row():
262
- activity = gr.Textbox(
263
- label="Log your water use for today",
264
- placeholder="e.g., Took a 10-minute shower β€’ Ran a full dishwasher β€’ Watered the garden",
265
- lines=2,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  )
267
- season = gr.Radio(SEASONS, label="🌀️ What season is it?", value=None, interactive=True, elem_id="season_pills")
268
 
269
- gr.Markdown("#### πŸ”Ž Try an example", elem_id="examples_label")
270
  with gr.Row():
271
- ex1 = gr.Button("🚿 10-min shower β€” example (Summer)", variant="secondary")
272
- ex2 = gr.Button("🍽 Full dishwasher β€” example (Winter)", variant="secondary")
273
- ex3 = gr.Button("🌱 Watered garden at noon β€” example (Summer)", variant="secondary")
274
-
275
- def _ex1(): return "Took a 10-minute shower", "β˜€οΈ Summer"
276
- def _ex2(): return "Ran a full dishwasher", "❄️ Winter"
277
- def _ex3(): return "Watered the garden at noon", "β˜€οΈ Summer"
278
- ex1.click(fn=_ex1, outputs=[activity, season])
279
- ex2.click(fn=_ex2, outputs=[activity, season])
280
- ex3.click(fn=_ex3, outputs=[activity, season])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
- selected_season_md = gr.Markdown("_Selected: **none**_")
283
 
284
  with gr.Row():
285
- get_btn = gr.Button("πŸ’‘ Get My Water-Saving Tips")
286
- clear_btn = gr.Button("🧹 Clear History")
287
-
288
- tips_md = gr.Markdown("_(Your top 3 tips will appear here.)_")
289
- msg_md = gr.Markdown("")
290
- gr.Markdown("### πŸ’§ Lifetime Water Saved")
291
- total_md = gr.HTML(total_saved_card_html(0))
292
-
293
- def on_season_change(s):
294
- if not s:
295
- return "_Selected: **none**_"
296
- return f"_Selected: **{s}**_"
297
- season.change(fn=on_season_change, inputs=season, outputs=selected_season_md)
298
-
299
- def on_get(activity_text, szn):
300
- tips_text, msg, stat_html = recommend(activity_text, szn)
301
- return tips_text, msg, stat_html
302
- get_btn.click(fn=on_get, inputs=[activity, season], outputs=[tips_md, msg_md, total_md])
303
-
304
- def on_clear_click():
305
- return clear_all()
306
- clear_btn.click(fn=on_clear_click, outputs=[tips_md, msg_md, total_md])
307
-
308
- def init_load():
309
- _, total = load_history()
310
- return "", "", total_saved_card_html(total), "_Selected: **none**_"
311
- demo.load(fn=init_load, inputs=None, outputs=[tips_md, msg_md, total_md, selected_season_md])
312
 
 
313
  if __name__ == "__main__":
314
  demo.launch()
 
1
+ # app.py β€” WaterWise Home (stable cache + robust handlers + light/dark-safe UI)
2
 
3
  import os
4
+ from pathlib import Path
5
+ import csv
6
+ import re
7
+ import numpy as np
 
8
  import gradio as gr
9
+
10
+ # -----------------------
11
+ # HF cache: FIX PERMISSIONS (must be before importing HF libs)
12
+ # -----------------------
13
+ os.environ["HF_HOME"] = "/home/user/.cache/huggingface"
14
+ os.environ["HF_HUB_CACHE"] = "/home/user/.cache/huggingface/hub"
15
+ os.environ["TRANSFORMERS_CACHE"] = "/home/user/.cache/huggingface/transformers"
16
+ Path("/home/user/.cache/huggingface").mkdir(parents=True, exist_ok=True)
17
+
18
+ # -----------------------
19
+ # Load tips CSV (strict)
20
+ # -----------------------
21
+ CSV_PATH = "tips_dataset.csv"
22
+ if not Path(CSV_PATH).exists():
23
+ raise FileNotFoundError(
24
+ f"Missing {CSV_PATH}. Upload it next to app.py with the first line exactly 'tip'."
25
+ )
26
+
27
+ tips: list[str] = []
28
+ with open(CSV_PATH, "r", encoding="utf-8") as f:
29
+ reader = csv.reader(f)
30
+ rows = list(reader)
31
+
32
+ if not rows:
33
+ raise ValueError("tips_dataset.csv is empty.")
34
+ header = rows[0]
35
+ if len(header) != 1 or header[0].strip().lower() != "tip":
36
+ raise ValueError("First line of tips_dataset.csv must be exactly one column named 'tip'.")
37
+
38
+ for r in rows[1:]:
39
+ if not r:
40
+ continue
41
+ t = (r[0] or "").strip()
42
+ if t:
43
+ tips.append(t)
44
+
45
+ if len(tips) != 1000:
46
+ raise ValueError(f"Expected exactly 1000 tips, found {len(tips)}. Check tips_dataset.csv formatting.")
47
+
48
+ # -----------------------
49
+ # Embedding model (robust)
50
+ # -----------------------
51
+ from sentence_transformers import SentenceTransformer # noqa: E402
52
+
53
+ def load_embedder():
54
+ names = [
55
+ "sentence-transformers/all-MiniLM-L6-v2", # canonical repo
56
+ "all-MiniLM-L6-v2", # alias fallback
57
+ ]
58
+ last_err = None
59
+ for name in names:
60
+ try:
61
+ return SentenceTransformer(name, cache_folder="models")
62
+ except Exception as e:
63
+ last_err = e
64
+ raise RuntimeError(f"Failed to load embedder. Last error: {last_err}")
65
+
66
+ model = load_embedder()
67
+ # Warmup so the first user click doesn't trigger a delayed download
68
+ _ = model.encode(["warmup"], convert_to_tensor=False)
69
+
70
+ # -----------------------
71
+ # Precompute tip embeddings
72
+ # -----------------------
73
+ def _normalize(m: np.ndarray) -> np.ndarray:
74
+ norms = np.linalg.norm(m, axis=1, keepdims=True) + 1e-12
75
+ return m / norms
76
+
77
+ tip_embs = model.encode(tips, convert_to_numpy=True, show_progress_bar=False)
78
+ tip_embs = _normalize(tip_embs.astype(np.float32))
79
+
80
+ # -----------------------
81
+ # Recommender
82
+ # -----------------------
83
  SEASON_KEYWORDS = {
84
+ "Spring": ["spring", "rain", "garden", "flowers", "bloom"],
85
+ "Summer": ["summer", "heat", "hot", "sprinkler", "pool", "evapor"],
86
+ "Fall": ["fall", "autumn", "leaves", "harvest"],
87
+ "Winter": ["winter", "cold", "freeze", "heater", "boiler", "pipe"],
88
  }
89
+
90
+ def recommend(user_text: str, season: str):
91
+ user_text = (user_text or "").strip()
92
+ if not user_text:
93
+ return (
94
+ "Tell me one thing you did with water today (e.g., 'long shower', 'ran dishwasher twice'). "
95
+ "Choose a season, then hit the button πŸ™‚",
96
+ "",
97
+ )
98
+
99
+ # Embed query
100
+ q = model.encode([user_text], convert_to_numpy=True).astype(np.float32)
101
+ q = q / (np.linalg.norm(q, axis=1, keepdims=True) + 1e-12)
102
+
103
+ # Cosine similarity
104
+ sims = (tip_embs @ q.T).squeeze(1)
105
+
106
+ # Gentle seasonal keyword boost
107
+ kws = SEASON_KEYWORDS.get(season, [])
108
+ if kws:
109
+ boost = np.zeros_like(sims, dtype=np.float32)
110
+ for i, tip in enumerate(tips):
111
+ tl = tip.lower()
112
+ if any(k in tl for k in kws):
113
+ boost[i] = 0.03
114
+ sims = sims + boost
115
+
116
+ top_idx = np.argsort(-sims)[:3].tolist()
117
+ top_tips = [f"β€’ {tips[i]}" for i in top_idx]
118
+
119
+ summary = (
120
+ f"Based on: **β€œ{user_text}”** Β· Season: **{season}**\n\n"
121
+ "Here are 3 smart, doable tips to try next:"
122
  )
123
+ return summary, "\n".join(top_tips)
124
+
125
+ # -----------------------
126
+ # Savings heuristics (safe)
127
+ # -----------------------
128
+ def parse_minutes(text: str, default=10):
129
+ if not text:
130
+ return default
131
+ m = re.search(r'(\d+)\s*(min|minute|minutes)\b', text.lower())
132
+ return int(m.group(1)) if m else default
133
+
134
+ def estimate_saved_liters(text: str, season: str) -> float:
135
+ """
136
+ Tiny heuristic. NEVER raises. Adjust numbers as you like.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  """
138
+ t = (text or "").lower()
139
+ mins = parse_minutes(t, default=10)
140
+ saved = 0.0
141
+ if "shower" in t:
142
+ # suggest -2 minutes ~ 6 L/min => ~12 L saved
143
+ saved += 12.0
144
+ if "dishwasher" in t:
145
+ # suggest full loads
146
+ saved += 6.0
147
+ if "garden" in t or "watering" in t or "lawn" in t:
148
+ saved += 8.0 if season == "Summer" else 5.0
149
+ return float(max(0.0, saved))
150
+
151
+ # -----------------------
152
+ # Combined submit handler (robust)
153
+ # -----------------------
154
+ def on_submit(user_text: str, season: str, state: dict | None):
155
+ try:
156
+ # State default
157
+ if not isinstance(state, dict):
158
+ state = {"saved_liters": 0.0, "history": []}
159
+
160
+ # Recs
161
+ summary_md, tips_md = recommend(user_text, season)
162
+
163
+ # Savings
164
+ delta = estimate_saved_liters(user_text, season)
165
+ state["saved_liters"] = float(state.get("saved_liters", 0.0)) + float(delta)
166
+ state["history"].append({"text": user_text, "season": season, "saved": float(delta)})
167
+
168
+ saved_headline = f"πŸ’§ {state['saved_liters']:.1f} liters saved"
169
+ saved_detail = (
170
+ "Drops become streams, streams become rivers. 🌊\n\n"
171
+ f"β‰ˆ {state['saved_liters']/12.0:.1f} showers Β· "
172
+ f"{state['saved_liters']/6.0:.1f} dishwasher loads Β· "
173
+ f"{state['saved_liters']/10.0:.1f} watering cans"
174
+ )
175
 
176
+ return summary_md, tips_md, saved_headline, saved_detail, state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ except Exception as e:
179
+ import traceback; traceback.print_exc()
180
+ err = f"Sorry β€” something went wrong: {e}"
181
+ return err, "", "πŸ’§ 0.0 liters saved", "", state or {"saved_liters": 0.0, "history": []}
182
 
183
+ def on_clear(state: dict | None):
184
+ state = {"saved_liters": 0.0, "history": []}
185
+ return "πŸ’§ 0.0 liters saved", "", state
186
+
187
+ def set_example(text: str, season: str):
188
+ return text, season
189
+
190
+ # -----------------------
191
+ # UI (light/dark safe)
192
+ # -----------------------
193
+ CUSTOM_CSS = """
194
+ :root {
195
+ --water-blue: #1570ef;
196
+ --water-blue-2: #2e90fa;
197
+ --card-bg: #ffffff;
198
+ }
199
+ body { background: linear-gradient(180deg, #dfefff 0%, #0c2d4a 800px, #0c2d4a 100%); }
200
+ .gradio-container { max-width: 980px !important; margin: 0 auto !important; }
201
+ #title-card { background: #ffffff; border-radius: 24px; padding: 22px; box-shadow: 0 10px 30px rgba(16,56,112,0.12); }
202
+ #subtitle { color: #1f3b67; }
203
+ label, .gr-text, .gr-markdown { color: #0b2c56 !important; }
204
+ button { border-radius: 999px !important; }
205
+ .gr-button { background: var(--water-blue) !important; border: none !important; color: white !important; }
206
+ .gr-button:hover { background: var(--water-blue-2) !important; }
207
+ .input-card, .output-card { background: #1f2a36; color: #e6f1ff; border-radius: 16px; padding: 18px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); }
208
+ .centered { display: flex; justify-content: center; align-items: center; text-align: center; }
209
+
210
+ /* ---- Force readable look in dark theme ---- */
211
+ [data-theme="dark"] body { background: linear-gradient(180deg, #dfefff 0%, #0c2d4a 800px, #0c2d4a 100%); }
212
+ [data-theme="dark"] #title-card { background: #ffffff !important; }
213
+ [data-theme="dark"] label,
214
+ [data-theme="dark"] .gr-text,
215
+ [data-theme="dark"] .gr-markdown { color: #e6f1ff !important; }
216
+ [data-theme="dark"] .gr-button { background: var(--water-blue) !important; color: #ffffff !important; }
217
+ """
218
+
219
+ theme = gr.themes.Soft(primary_hue="blue", neutral_hue="slate")
220
+
221
+ with gr.Blocks(css=CUSTOM_CSS, theme=theme, title="WaterWise Home β€” Your smart water assistant") as demo:
222
+ # Header
223
+ with gr.Column(elem_id="title-card"):
224
+ gr.Markdown("# πŸ’§ WaterWise Home", elem_classes=["centered"])
225
+ gr.Markdown(
226
+ "I'm your smart chip! Tell me how you used water today, and I'll track your usage, give tips, "
227
+ "and challenge you to save more.",
228
+ elem_id="subtitle",
229
+ elem_classes=["centered"],
230
  )
 
231
 
232
+ # Input / Output
233
  with gr.Row():
234
+ with gr.Column(scale=1, elem_classes=["input-card"]):
235
+ user_text = gr.Textbox(
236
+ label="Log your water use for today",
237
+ placeholder="e.g., Took a 12-minute shower, ran the dishwasher twice, watered the lawn",
238
+ lines=2,
239
+ value=""
240
+ )
241
+ season = gr.Radio(
242
+ label="🌀️ What season is it?",
243
+ choices=["Winter", "Spring", "Summer", "Fall"],
244
+ value="Winter",
245
+ interactive=True,
246
+ )
247
+
248
+ with gr.Column(scale=1, elem_classes=["output-card"]):
249
+ summary_md = gr.Markdown()
250
+ tips_md = gr.Markdown()
251
+
252
+ # Examples row
253
+ gr.Markdown("πŸ”Ž **Try an example**")
254
+ with gr.Row():
255
+ ex1 = gr.Button("πŸ› 10-min shower β€” example (Summer)")
256
+ ex2 = gr.Button("🍽️ Full dishwasher β€” example (Winter)")
257
+ ex3 = gr.Button("🌱 Watered garden at noon β€” example (Summer)")
258
 
259
+ gr.Markdown("**Selected:** ❄️ Winter")
260
 
261
  with gr.Row():
262
+ submit = gr.Button("πŸ’‘ Get My Water-Saving Tips", scale=1)
263
+ clear_btn = gr.Button("🧹 Clear History", scale=1)
264
+
265
+ # Savings section
266
+ gr.Markdown("πŸ’§ **Lifetime Water Saved**")
267
+ saved_headline = gr.Markdown("πŸ’§ 0.0 liters saved")
268
+ saved_detail = gr.Markdown("Drops become streams, streams become rivers. 🌊\n\nβ‰ˆ 0.0 showers Β· 0.0 dishwasher loads Β· 0.0 watering cans")
269
+
270
+ # App state
271
+ state = gr.State({"saved_liters": 0.0, "history": []})
272
+
273
+ # Wiring
274
+ submit.click(
275
+ fn=on_submit,
276
+ inputs=[user_text, season, state],
277
+ outputs=[summary_md, tips_md, saved_headline, saved_detail, state],
278
+ )
279
+
280
+ clear_btn.click(
281
+ fn=on_clear,
282
+ inputs=[state],
283
+ outputs=[saved_headline, saved_detail, state],
284
+ )
285
+
286
+ ex1.click(lambda: set_example("10 minute shower", "Summer"), None, [user_text, season])
287
+ ex2.click(lambda: set_example("Full dishwasher", "Winter"), None, [user_text, season])
288
+ ex3.click(lambda: set_example("Watered garden at noon", "Summer"), None, [user_text, season])
289
 
290
+ # Local dev
291
  if __name__ == "__main__":
292
  demo.launch()