Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| from transformers import pipeline | |
| import random | |
| from gtts import gTTS | |
| import os | |
| print("Loading classification model...", flush=True) | |
| classifier = pipeline("zero-shot-classification", model="facebook/bart-large-mnli") | |
| CATEGORIES = [ | |
| "simple factual question", | |
| "mathematical calculation", | |
| "creative content generation", | |
| "complex research query" | |
| ] | |
| # --- 1. EMBEDDED DATA & LOGIC --- | |
| MODELS = { | |
| "Mistral Large 3": {"energy_wh": 1.98, "water_ml": 3.56, "co2_g": 0.95}, | |
| "Mistral Medium 3": {"energy_wh": 6.76, "water_ml": 12.17, "co2_g": 3.24}, | |
| "Claude Sonnet 4.6": {"energy_wh": 19.32, "water_ml": 34.78, "co2_g": 9.27}, | |
| "Gemini 3 Pro": {"energy_wh": 72.45, "water_ml": 130.41, "co2_g": 34.78}, | |
| "Gemini 3.1 Pro": {"energy_wh": 77.28, "water_ml": 139.10, "co2_g": 37.09}, | |
| "Claude Opus 4.6": {"energy_wh": 96.60, "water_ml": 173.88, "co2_g": 46.37}, | |
| "GPT-5.2": {"energy_wh": 144.90, "water_ml": 260.82, "co2_g": 69.55}, | |
| "GPT-5.4": {"energy_wh": 169.05, "water_ml": 304.29, "co2_g": 81.14} | |
| } | |
| CATEGORY_MULTIPLIERS = { | |
| "simple factual question": 0.05, | |
| "mathematical calculation": 0.10, | |
| "creative content generation": 0.80, | |
| "complex research query": 1.50 | |
| } | |
| DIALOGUES = { | |
| "energetic": { | |
| "simple factual question": "You used a supercomputer for a simple fact? I'm full right now, but that's just lazy. Open a book.", | |
| "mathematical calculation": "A basic calculator could do this with zero footprint. But sure, burn my energy for basic math.", | |
| "creative content generation": "Generating artificial creativity... I'll allow it, but watch my water levels drop. Your poetry is expensive.", | |
| "complex research query": "A legitimate research query! Finally, a worthy use of my massive architecture. Drain away!" | |
| }, | |
| "tired": { | |
| "simple factual question": "Look at me. I'm sweating. All because you couldn't be bothered to use a standard search engine.", | |
| "mathematical calculation": "Did you skip basic math class? I am literally evaporating to do arithmetic for you.", | |
| "creative content generation": "Your 'creativity' is draining my lifeblood. Make it quick, I don't have much cooling left.", | |
| "complex research query": "I respect the complexity, but this is taking a severe toll on my cooling systems. I hope this research saves the world." | |
| }, | |
| "raspy": { | |
| "simple factual question": "*Cough* Read an encyclopedia... I'm dying for your trivia...", | |
| "mathematical calculation": "Use an abacus... you are boiling my last drops for numbers...", | |
| "creative content generation": "My final breath... wasted on generating a fictional story... how poetic... and cruel.", | |
| "complex research query": "Critical system failure imminent... I am giving my life for your research... make it count." | |
| } | |
| } | |
| ALTERNATIVES = { | |
| "simple factual question": [ | |
| {"text": "Search Engine (Google)", "url": "https://www.google.com"}, | |
| {"text": "Wikipedia Encyclopedia", "url": "https://www.wikipedia.org"}, | |
| {"text": "Ask a Human Expert (Quora)", "url": "https://www.quora.com"} | |
| ], | |
| "mathematical calculation": [ | |
| {"text": "Wolfram Alpha Engine", "url": "https://www.wolframalpha.com"}, | |
| {"text": "Geogebra Calculator", "url": "https://www.geogebra.org"}, | |
| {"text": "Desmos Graphing", "url": "https://www.desmos.com"} | |
| ], | |
| "creative content generation": [ | |
| {"text": "Writing Prompts (Reddit)", "url": "https://www.reddit.com/r/WritingPrompts/"}, | |
| {"text": "Thesaurus/Dictionary", "url": "https://www.thesaurus.com"}, | |
| {"text": "Project Gutenberg Books", "url": "https://www.gutenberg.org"} | |
| ], | |
| "complex research query": [ | |
| {"text": "Google Scholar Search", "url": "https://scholar.google.com"}, | |
| {"text": "University Library Database", "url": "https://www.worldcat.org/"}, | |
| {"text": "Open Access Journals (DOAJ)", "url": "https://doaj.org/"} | |
| ] | |
| } | |
| def calculate_impact(category, confidence_score, query_text, model_name): | |
| model_cost = MODELS.get(model_name, MODELS["Mistral Large 3"]) | |
| cat_mult = CATEGORY_MULTIPLIERS.get(category, 0.05) | |
| conf_mult = 1.0 if confidence_score > 0.8 else (1.2 if confidence_score > 0.5 else 1.5) | |
| word_count = len(query_text.split()) | |
| len_factor = max(0.5, 1.0 + ((word_count - 15) * 0.015)) | |
| water_ml = model_cost["water_ml"] * cat_mult * conf_mult * len_factor | |
| energy_wh = model_cost["energy_wh"] * cat_mult * conf_mult * len_factor | |
| co2_g = model_cost["co2_g"] * cat_mult * conf_mult * len_factor | |
| return { | |
| "water_l": water_ml / 1000.0, "energy_kwh": energy_wh / 1000.0, | |
| "water_ml": round(water_ml, 1), "energy_wh": round(energy_wh, 2), "co2_g": round(co2_g, 2) | |
| } | |
| def get_dialogue(water_level, category): | |
| # Adjusted dialogue thresholds back to 10L max (7.0 and 3.0) | |
| state = "energetic" if water_level > 7.0 else ("tired" if water_level >= 3.0 else "raspy") | |
| return DIALOGUES[state].get(category, "I have no words left for this.") | |
| def generate_audio(text): | |
| tts = gTTS(text, lang='en') | |
| filepath = "temp_drip_voice.mp3" | |
| tts.save(filepath) | |
| return filepath | |
| # --- 2. UI GENERATORS & DASHBOARDS --- | |
| def get_relatable_translations(impact): | |
| bottles = impact['water_ml'] / 500.0 | |
| w_ex = f"{bottles:.1f} water bottles" if bottles >= 0.1 else "A few sips" | |
| bulb_hours = impact['energy_wh'] / 10.0 | |
| e_ex = f"{bulb_hours:.1f} hours of 1 LED bulb" if bulb_hours >= 0.1 else f"{impact['energy_wh']*12:.1f} mins of 1 LED bulb" | |
| charges = impact['co2_g'] / 5.0 | |
| c_ex = f"{charges:.1f} phone charges" if charges >= 0.1 else "Negligible emissions" | |
| return w_ex, e_ex, c_ex | |
| def get_stats_html(water, energy, co2, points): | |
| # Updated progress bar logic for a 10.0L maximum | |
| water_percent = max(0, min(100, (water / 10.0) * 100)) | |
| return f""" | |
| <div class="stats-container"> | |
| <div class="stat-card water-card"><div class="stat-label">WATER TANK (10L MAX)</div><div class="stat-value">{water:.2f}L</div><div class="progress-bar-bg"><div class="progress-bar-fill" style="width: {water_percent}%;"></div></div></div> | |
| <div class="stat-card energy-card"><div class="stat-label">ENERGY WASTED</div><div class="stat-value">{energy:.4f} <span style="font-size: 0.5em;">kWh</span></div></div> | |
| <div class="stat-card co2-card"><div class="stat-label">CARBON (CO₂e)</div><div class="stat-value">{co2:.2f}g</div></div> | |
| <div class="stat-card points-card"><div class="stat-label">SCORE</div><div class="stat-value">{points}</div></div> | |
| </div> | |
| """ | |
| def get_drip_visuals(water_level): | |
| # Adjusted visual state thresholds back to 10.0L maximum (7.0 and 3.0) | |
| if water_level > 7.0: | |
| g_start, g_end = "#7dd3fc", "#0369a1" | |
| mouth_d = "M43 112 Q60 122 77 112" | |
| cracks_op, sweat_op, tear_op = "0", "0", "0" | |
| mood, mood_color, anim = "Hydrated and coping", "rgb(34, 211, 238)", "bounce 2s infinite ease-in-out" | |
| elif 3.0 <= water_level <= 7.0: | |
| g_start, g_end = "#38bdf8", "#0284c7" | |
| mouth_d = "M43 112 Q60 110 77 112" | |
| cracks_op, sweat_op, tear_op = "0", "1", "0" | |
| mood, mood_color, anim = "Shrinking & Sweaty", "#0ea5e9", "float 4s infinite ease-in-out" | |
| elif water_level > 0: | |
| g_start, g_end = "#fca5a5", "#b91c1c" | |
| mouth_d = "M43 116 Q60 102 77 116" | |
| cracks_op, sweat_op, tear_op = "1", "0", "1" | |
| mood, mood_color, anim = "Overheating & Raspy", "#ef4444", "shake 0.5s infinite" | |
| else: | |
| g_start, g_end = "#94a3b8", "#334155" | |
| mouth_d = "M40 112 Q45 105 50 112 T60 112 T70 112 T80 112" | |
| cracks_op, sweat_op, tear_op = "0", "0", "0" | |
| mood, mood_color, anim = "Evaporated!", "#94a3b8", "floatUp 3s forwards" | |
| return f""" | |
| <div class="drip-panel" style="animation: {anim}; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%;"> | |
| <div class="drip-lbl" style="font-weight: 800; color: #94a3b8; letter-spacing: 3px; font-size: 14px; margin-bottom: 5px;">DRIP</div> | |
| <svg id="drip-svg" viewBox="0 0 120 148" style="width: 140px; height: 160px; overflow: visible;" xmlns="http://www.w3.org/2000/svg"> | |
| <defs><radialGradient id="dg" cx="38%" cy="32%"><stop id="dg1" offset="0%" stop-color="{g_start}"></stop><stop id="dg2" offset="100%" stop-color="{g_end}"></stop></radialGradient></defs> | |
| <path id="dbody" d="M60 8 C60 8,102 66,102 90 C102 118,83 137,60 137 C37 137,18 118,18 90 C18 66,60 8,60 8Z" fill="url(#dg)" stroke="rgba(34,211,238,0.35)" stroke-width="1.5" opacity="1"></path> | |
| <ellipse cx="42" cy="70" rx="9" ry="13" fill="rgba(255,255,255,0.22)" transform="rotate(-18,42,70)"></ellipse> | |
| <circle cx="44" cy="90" r="10" fill="white"></circle><circle cx="76" cy="90" r="10" fill="white"></circle> | |
| <circle id="pl" cx="45" cy="91" r="6" fill="#0f172a"></circle><circle id="pr" cx="77" cy="91" r="6" fill="#0f172a"></circle> | |
| <circle cx="47" cy="88" r="2.5" fill="white"></circle><circle cx="79" cy="88" r="2.5" fill="white"></circle> | |
| <path id="dm" d="{mouth_d}" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round" fill="none"></path> | |
| <g id="cracks" opacity="{cracks_op}" style="transition: opacity 0.5s;"> | |
| <path d="M80 46 L86 58 L79 65 L85 77" stroke="#bae6fd" stroke-width="1.3" stroke-linecap="round" fill="none" opacity=".7"></path><path d="M37 82 L31 93 L39 99" stroke="#bae6fd" stroke-width="1.3" stroke-linecap="round" fill="none" opacity=".7"></path> | |
| </g> | |
| <path id="sweat" d="M108 76 C110 70,116 70,116 78 C116 84,110 88,108 82Z" fill="#7dd3fc" opacity="{sweat_op}" style="transition: opacity 0.5s;"></path> | |
| <path id="tear" d="M38 104 C37 108,33 108,33 112 C33 115,36 117,38 114 C40 117,43 115,43 112 C43 108,39 108,38 104Z" fill="#93c5fd" opacity="{tear_op}" style="transition: opacity 0.5s;"></path> | |
| </svg> | |
| <div class="drip-mood" id="drip-mood" style="color: {mood_color}; font-weight: 500; font-size: 1.1rem; margin-top: 15px;">{mood}</div> | |
| <div style="text-align:center; margin-top:10px;"><div style="font-size:10px; color:#94a3b8; text-transform:uppercase; letter-spacing:1px;">Water Left</div><div style="font-size:26px; font-weight:700; color:{mood_color};" id="d-water">{water_level:.2f}L</div></div> | |
| </div> | |
| """ | |
| def get_impact_cards(impact, category): | |
| w_ex, e_ex, c_ex = get_relatable_translations(impact) | |
| return f""" | |
| <div class="impact-container"> | |
| <div class="impact-card bad-impact"> | |
| <h3 style="margin: 0 0 10px 0; color: #fca5a5;">🔥 LLM Impact</h3> | |
| <p style="margin:4px 0; font-size:1.1em;">💧 <strong>{impact['water_ml']} mL</strong><br><small>({w_ex})</small></p> | |
| <p style="margin:4px 0; font-size:1.1em;">⚡ <strong>{impact['energy_wh']} Wh</strong><br><small>({e_ex})</small></p> | |
| <p style="margin:4px 0; font-size:1.1em;">☁️ <strong>{impact['co2_g']} g</strong><br><small>({c_ex})</small></p> | |
| </div> | |
| <div class="impact-card good-impact"> | |
| <h3 style="margin: 0 0 10px 0; color: #86efac;">🌿 Eco-Alternative</h3> | |
| <p style="margin:4px 0; font-size:1.1em;"><strong>~0.0 mL</strong> Water</p> | |
| <p style="margin:4px 0; font-size:1.1em;"><strong>~0.001 Wh</strong> Energy</p> | |
| <p style="margin:4px 0; font-size:1.1em;"><strong>~0.01 g</strong> CO₂e</p> | |
| </div> | |
| </div> | |
| """ | |
| def format_alt_button(text, impact): | |
| icon = "💡" | |
| if "Search" in text or "Google" in text: icon = "🔍" | |
| elif "Wikipedia" in text or "Encyclopedia" in text: icon = "📚" | |
| elif "Expert" in text or "Quora" in text: icon = "👤" | |
| elif "Wolfram" in text or "Geogebra" in text or "Desmos" in text: icon = "🧮" | |
| elif "Reddit" in text or "Gutenberg" in text or "Thesaurus" in text: icon = "📝" | |
| elif "Scholar" in text or "Database" in text or "Journals" in text: icon = "🎓" | |
| return f"{icon} {text}\n\n🏆 Score: +10 Pts\n💧 Save: {impact['water_ml']} mL\n⚡ Save: {impact['energy_wh']} Wh\n☁️ Save: {impact['co2_g']} g" | |
| def generate_victory_dashboard(state): | |
| try: | |
| total_w_saved, total_e_saved, total_c_saved = 0.0, 0.0, 0.0 | |
| rows = "" | |
| for item in state.get("history", []): | |
| alt_name = item.get('alt_name', 'None') | |
| saved_w = item.get('water_ml', 0.0) | |
| saved_e = max(0.0, item.get('energy_wh', 0.0) - 0.001) | |
| saved_c = max(0.0, item.get('co2_g', 0.0) - 0.01) | |
| total_w_saved += saved_w | |
| total_e_saved += saved_e | |
| total_c_saved += saved_c | |
| rows += f""" | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2);"> | |
| <td style="padding: 15px; font-style: italic;">"{item.get('query', '')}"</td> | |
| <td style="padding: 15px; color: #fca5a5; line-height: 1.5;">💧 {item.get('water_ml', 0):.1f} mL<br>⚡ {item.get('energy_wh', 0):.2f} Wh<br>☁️ {item.get('co2_g', 0):.2f} g</td> | |
| <td style="padding: 15px;"><span style='color: #34d399; font-weight: bold;'>{alt_name}</span></td> | |
| <td style="padding: 15px; color: #60a5fa; font-weight: bold; line-height: 1.5;">💧 {saved_w:.1f} mL<br>⚡ {saved_e:.2f} Wh<br>☁️ {saved_c:.2f} g</td> | |
| </tr> | |
| """ | |
| confetti_elements = "".join([f'<div class="confetti-piece" style="left: {random.randint(0, 100)}%; background-color: hsl({random.randint(0, 360)}, 100%, 60%); animation-duration: {random.uniform(2.5, 5)}s; animation-delay: {random.uniform(0, 2)}s;"></div>' for _ in range(60)]) | |
| # Updated "Total Wasted" formula to 10.0 to reflect the 10L tank | |
| return f""" | |
| <div class="endgame-wrapper"> | |
| {confetti_elements} | |
| <div class="victory-dashboard"> | |
| <h1 class="victory-title">🎉 PLANET SAVED! 🎉</h1> | |
| <p style="text-align: center; font-size: 1.3em; color: #a7f3d0; margin-bottom: 30px;">Congratulations! You reached {state.get('points', 0)} points!</p> | |
| <div class="victory-stats-row"> | |
| <div class="v-stat-box" style="border-top: 4px solid #34d399;"> | |
| <h4 style="color: #34d399;">Planetary Savings</h4> | |
| <p style="color: #34d399; font-size: 1.3em; font-weight: bold; margin: 8px 0;">💧 {total_w_saved:.1f} mL Saved</p> | |
| <p style="color: #facc15; font-size: 1.3em; font-weight: bold; margin: 8px 0;">⚡ {total_e_saved:.2f} Wh Saved</p> | |
| <p style="color: #a8a29e; font-size: 1.3em; font-weight: bold; margin: 8px 0;">☁️ {total_c_saved:.2f} g Saved</p> | |
| </div> | |
| <div class="v-stat-box" style="border-top: 4px solid #fca5a5;"> | |
| <h4 style="color: #fca5a5;">Total Wasted</h4> | |
| <p style="color: #fca5a5; font-size: 1.1em; margin: 8px 0;">💧 {10.0 - state.get('water', 10.0):.2f} L</p> | |
| <p style="color: #fca5a5; font-size: 1.1em; margin: 8px 0;">⚡ {state.get('energy', 0):.4f} kWh</p> | |
| <p style="color: #fca5a5; font-size: 1.1em; margin: 8px 0;">☁️ {state.get('co2', 0):.2f} g</p> | |
| </div> | |
| </div> | |
| <h3 style="margin-top: 40px; color: #38bdf8; font-size: 1.8em; text-align: center;">📊 AI Usage Audit Log</h3> | |
| <div style="max-height: 500px; overflow-y: auto; background: rgba(15, 23, 42, 0.9); border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); padding: 10px;"> | |
| <table style="width: 100%; text-align: left; border-collapse: collapse; font-size: 1.05em;"> | |
| <tr style="background: rgba(56, 189, 248, 0.1); border-bottom: 2px solid #38bdf8;"> | |
| <th style="padding: 15px; color: #e2e8f0;">User Query</th><th style="padding: 15px; color: #fca5a5;">LLM Cost</th><th style="padding: 15px; color: #34d399;">Alternative Selected</th><th style="padding: 15px; color: #60a5fa;">Resources Saved</th> | |
| </tr> | |
| {rows} | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| except Exception as e: | |
| return f"<div style='color:red;'>Error generating dashboard: {str(e)}</div>" | |
| def generate_defeat_dashboard(state): | |
| try: | |
| rows = "" | |
| for item in state.get("history", []): | |
| rows += f""" | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2);"> | |
| <td style="padding: 15px; font-style: italic;">"{item.get('query', '')}"</td> | |
| <td style="padding: 15px; color: #fca5a5; line-height: 1.5;">💧 {item.get('water_ml', 0):.1f} mL<br>⚡ {item.get('energy_wh', 0):.2f} Wh<br>☁️ {item.get('co2_g', 0):.2f} g</td> | |
| </tr> | |
| """ | |
| ash_elements = "".join([f'<div class="ash-piece" style="left: {random.randint(0, 100)}%; animation-duration: {random.uniform(3, 6)}s; animation-delay: {random.uniform(0, 2)}s;"></div>' for _ in range(60)]) | |
| # Updated "Total Wasted" to reflect the empty 10L tank | |
| return f""" | |
| <div class="endgame-wrapper"> | |
| {ash_elements} | |
| <div class="defeat-dashboard"> | |
| <h1 class="defeat-title">💀 DRIP EVAPORATED! 💀</h1> | |
| <p style="text-align: center; font-size: 1.3em; color: #fca5a5; margin-bottom: 30px;">You ran out of water before reaching 500 points.</p> | |
| <div class="victory-stats-row"> | |
| <div class="v-stat-box" style="border-top: 4px solid #ef4444; background: rgba(239, 68, 68, 0.1);"> | |
| <h4 style="color: #ef4444;">Total Resources Wasted</h4> | |
| <p style="color: #fca5a5; font-size: 1.3em; font-weight: bold; margin: 8px 0;">💧 10.0 L (Tank Empty)</p> | |
| <p style="color: #fca5a5; font-size: 1.3em; font-weight: bold; margin: 8px 0;">⚡ {state.get('energy', 0):.4f} kWh</p> | |
| <p style="color: #fca5a5; font-size: 1.3em; font-weight: bold; margin: 8px 0;">☁️ {state.get('co2', 0):.2f} g</p> | |
| </div> | |
| <div class="v-stat-box" style="border-top: 4px solid #94a3b8;"> | |
| <h4 style="color: #94a3b8;">Final Score</h4> | |
| <p style="color: white; font-size: 3.5em; font-weight: bold; margin: 10px 0;">{state.get('points', 0)}</p> | |
| </div> | |
| </div> | |
| <h3 style="margin-top: 40px; color: #fca5a5; font-size: 1.8em; text-align: center;">🔥 Wasted Resources Log</h3> | |
| <div style="max-height: 400px; overflow-y: auto; background: rgba(15, 23, 42, 0.9); border-radius: 12px; border: 1px solid rgba(239, 68, 68, 0.3); padding: 10px;"> | |
| <table style="width: 100%; text-align: left; border-collapse: collapse; font-size: 1.05em;"> | |
| <tr style="background: rgba(239, 68, 68, 0.15); border-bottom: 2px solid #ef4444;"> | |
| <th style="padding: 15px; color: #fca5a5;">User Query</th><th style="padding: 15px; color: #fca5a5;">Resources Drained</th> | |
| </tr> | |
| {rows} | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| except Exception as e: | |
| return f"<div style='color:red;'>Error generating dashboard: {str(e)}</div>" | |
| # --- 3. EVENT HANDLERS --- | |
| def process_query(user_input, selected_model, state): | |
| if state["game_over"] or not user_input.strip(): | |
| return [state] + [gr.update()]*13 | |
| result = classifier(user_input, CATEGORIES) | |
| category = result['labels'][0] | |
| confidence = result['scores'][0] | |
| impact = calculate_impact(category, confidence, user_input, selected_model) | |
| state["queries"] += 1 | |
| state["water"] = max(0, state["water"] - impact["water_l"]) | |
| state["energy"] += impact["energy_kwh"] | |
| state["co2"] += impact["co2_g"] | |
| if category == "complex research query": | |
| state["points"] += 50 | |
| raw_alts = ALTERNATIVES.get(category, ALTERNATIVES["simple factual question"]) | |
| best_alt_name = raw_alts[0]["text"] | |
| shuffled_alts = list(raw_alts) | |
| random.shuffle(shuffled_alts) | |
| state["current_shuffled_alts"] = shuffled_alts | |
| state["current_best_alt"] = raw_alts[0] | |
| alt_choices = [format_alt_button(a["text"], impact) for a in shuffled_alts] | |
| while len(alt_choices) < 3: alt_choices.append("") | |
| dialogue = get_dialogue(state["water"], category) | |
| state["history"].append({ | |
| "query": user_input, "water_ml": impact["water_ml"], "energy_wh": impact["energy_wh"], | |
| "co2_g": impact["co2_g"], "alt_name": best_alt_name | |
| }) | |
| stats_ui = get_stats_html(state["water"], state["energy"], state["co2"], state["points"]) | |
| drip_ui = get_drip_visuals(state["water"]) | |
| impact_ui = get_impact_cards(impact, category) | |
| audio_path = generate_audio(dialogue) | |
| feedback_text = f"<div class='feedback-box'>🧠 <strong>{category.upper()}</strong> Detected!<br><br><em>\"{dialogue}\"</em></div>" | |
| # WIN CONDITION | |
| if state["points"] >= 500: | |
| state["game_over"] = True | |
| return (state, stats_ui, drip_ui, audio_path, impact_ui, gr.update(), gr.update(visible=False), | |
| gr.update(), gr.update(), gr.update(), gr.update(), gr.update(value=""), | |
| gr.update(visible=False), generate_victory_dashboard(state)) | |
| # LOSS CONDITION | |
| elif state["water"] <= 0.001: | |
| state["game_over"] = True | |
| return (state, stats_ui, drip_ui, audio_path, impact_ui, gr.update(), gr.update(visible=False), | |
| gr.update(), gr.update(), gr.update(), gr.update(), gr.update(value=""), | |
| gr.update(visible=False), generate_defeat_dashboard(state)) | |
| # REGULAR GAMEPLAY RETURN | |
| return (state, stats_ui, drip_ui, audio_path, impact_ui, gr.update(value=feedback_text), gr.update(visible=True), | |
| gr.update(value=alt_choices[0], visible=True), gr.update(value=alt_choices[1], visible=True), gr.update(value=alt_choices[2], visible=True), gr.update(value=""), gr.update(value=""), | |
| gr.update(visible=True), gr.update()) | |
| def select_alternative(choice_text, state): | |
| if state["game_over"] or not state.get("current_best_alt"): | |
| return [state] + [gr.update()]*5 | |
| first_line = choice_text.split("\n")[0] | |
| clean_choice = first_line.split(" ", 1)[1].strip() if " " in first_line else first_line | |
| selected_dict = next((item for item in state["current_shuffled_alts"] if item["text"] == clean_choice), None) | |
| url = selected_dict['url'] if selected_dict else "#" | |
| if clean_choice == state["current_best_alt"]["text"]: | |
| state["points"] += 10 | |
| msg = f"✅ **Excellent!** +10 Points.\n\n<a href='{url}' target='_blank' style='display:inline-block; margin-top:10px; padding: 10px 20px; background:#10b981; color:white; text-decoration:none; border-radius:8px; font-weight:bold;'>👉 Click here to use {clean_choice}</a>" | |
| else: | |
| msg = f"⚠️ **Okay choice.** Better than an LLM, but there was a slightly more optimal tool.\n\n<a href='{url}' target='_blank' style='display:inline-block; margin-top:10px; padding: 10px 20px; background:#38bdf8; color:white; text-decoration:none; border-radius:8px; font-weight:bold;'>👉 Click here to use {clean_choice}</a>" | |
| stats_ui = get_stats_html(state["water"], state["energy"], state["co2"], state["points"]) | |
| # WIN CONDITION FROM ALTERNATIVE CLICK | |
| if state["points"] >= 500: | |
| state["game_over"] = True | |
| return state, stats_ui, gr.update(), gr.update(visible=False), gr.update(visible=False), generate_victory_dashboard(state) | |
| return state, stats_ui, gr.update(value=msg), gr.update(visible=False), gr.update(visible=True), gr.update() | |
| # --- 4. CSS STYLING --- | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;500;700&display=swap'); | |
| body, .gradio-container { background-color: #0B1120 !important; font-family: 'Outfit', sans-serif !important; color: #e2e8f0 !important; } | |
| .gradio-container { max-width: 1000px !important; margin: auto; padding-top: 20px; } | |
| h1 { text-align: center; color: #38bdf8; text-shadow: 0 0 15px rgba(56, 189, 248, 0.5); font-size: 2.5em !important; margin-bottom: 5px !important;} | |
| .subtitle { text-align: center; color: #94a3b8; margin-bottom: 30px; font-weight: 300; } | |
| .stats-container { display: flex; gap: 10px; justify-content: space-between; margin-bottom: 20px; } | |
| .stat-card { flex: 1; padding: 15px 10px; border-radius: 12px; text-align: center; background: rgba(30, 41, 59, 0.6); border: 1px solid rgba(255,255,255,0.1); } | |
| .water-card { box-shadow: 0 4px 20px rgba(56, 189, 248, 0.15); border-top: 2px solid #38bdf8; } | |
| .energy-card { box-shadow: 0 4px 20px rgba(250, 204, 21, 0.15); border-top: 2px solid #facc15; } | |
| .co2-card { box-shadow: 0 4px 20px rgba(168, 162, 158, 0.15); border-top: 2px solid #a8a29e; } | |
| .points-card { box-shadow: 0 4px 20px rgba(52, 211, 153, 0.15); border-top: 2px solid #34d399; } | |
| .stat-label { font-size: 0.75em; letter-spacing: 1px; opacity: 0.8; margin-bottom: 5px; } | |
| .stat-value { font-size: 1.8em; font-weight: 700; color: white; } | |
| .progress-bar-bg { background: #1e293b; height: 8px; border-radius: 4px; margin-top: 10px; overflow: hidden; } | |
| .progress-bar-fill { background: linear-gradient(90deg, #0284c7, #38bdf8); height: 100%; transition: width 0.5s ease; } | |
| @keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-15px); } } | |
| @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } | |
| @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } } | |
| @keyframes floatUp { to { transform: translateY(-30px); opacity: 0; } } | |
| .drip-pod { background: rgba(15, 23, 42, 0.6); border-radius: 24px; border: 1px solid rgba(56, 189, 248, 0.2); padding: 30px 20px; display: flex; flex-direction: column; align-items: center; box-shadow: inset 0 0 40px rgba(0,0,0,0.5); } | |
| .hidden-audio { display: none !important; height: 0 !important; width: 0 !important; overflow: hidden !important; } | |
| .feedback-box { background: rgba(56, 189, 248, 0.1); border-left: 4px solid #38bdf8; padding: 15px; border-radius: 8px; font-size: 1.05em; color: #e2e8f0; width: 100%; text-align: left; line-height: 1.4; box-sizing: border-box;} | |
| .input-row { align-items: stretch !important; margin: 10px 0; } | |
| .custom-textbox textarea { background: rgba(30, 41, 59, 0.8) !important; border: 1px solid #38bdf8 !important; color: white !important; font-size: 1.1em !important; border-radius: 12px !important; box-shadow: inset 0 2px 10px rgba(0,0,0,0.3) !important; padding: 15px !important; resize: none !important; } | |
| .custom-btn { background: linear-gradient(135deg, #0284c7, #0ea5e9) !important; border: none !important; color: white !important; font-weight: bold !important; font-size: 1.1em !important; border-radius: 12px !important; transition: all 0.3s !important; height: 100%; min-height: 85px; } | |
| .custom-btn:hover { box-shadow: 0 0 20px rgba(56, 189, 248, 0.6) !important; transform: scale(1.02); } | |
| .impact-container { display: flex; gap: 20px; width: 100%; } | |
| .impact-card { flex: 1; padding: 20px; border-radius: 16px; border: 1px solid rgba(255,255,255,0.1); } | |
| .bad-impact { background: linear-gradient(180deg, rgba(127, 29, 29, 0.3), rgba(69, 10, 10, 0.5)); border-top: 3px solid #ef4444; } | |
| .good-impact { background: linear-gradient(180deg, rgba(20, 83, 45, 0.3), rgba(6, 78, 59, 0.5)); border-top: 3px solid #10b981; } | |
| .alt-group { padding: 25px; background: linear-gradient(145deg, rgba(16, 185, 129, 0.05), rgba(6, 95, 70, 0.1)); border-radius: 20px; border: 1px solid rgba(16, 185, 129, 0.3); margin-top: 25px; box-shadow: 0 10px 30px rgba(0,0,0,0.2);} | |
| .alt-card { background: linear-gradient(145deg, #1e293b, #0f172a) !important; border: 1px solid #10b981 !important; color: #a7f3d0 !important; height: auto !important; min-height: 180px !important; border-radius: 16px !important; font-size: 1.05em !important; font-weight: 500 !important; white-space: pre-wrap !important; line-height: 1.5 !important; padding: 15px !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.4) !important;} | |
| .alt-card:hover { background: linear-gradient(145deg, #064e3b, #065f46) !important; transform: translateY(-8px) !important; box-shadow: 0 12px 30px rgba(16, 185, 129, 0.3) !important; color: white !important; border-color: #34d399 !important;} | |
| .custom-dropdown { background: rgba(30, 41, 59, 0.8) !important; border: 1px solid #38bdf8 !important; border-radius: 12px !important; } | |
| /* End Game Dashboards & Animations */ | |
| .endgame-wrapper { position: relative; width: 100%; min-height: 600px; overflow: hidden; border-radius: 24px; padding-top: 10px; } | |
| .victory-dashboard { background: linear-gradient(135deg, #0f172a, #1e293b); padding: 40px; border-radius: 24px; border: 2px solid #34d399; animation: pulseGlow 2s infinite; color: white; position: relative; z-index: 5; margin-top: 20px;} | |
| .victory-title { text-align: center; color: #34d399; font-size: 3.5em !important; text-shadow: 0 0 20px rgba(52, 211, 153, 0.6); margin-bottom: 5px; } | |
| .victory-stats-row { display: flex; gap: 20px; margin-top: 30px; } | |
| .v-stat-box { flex: 1; background: rgba(0,0,0,0.4); padding: 25px; border-radius: 16px; text-align: center; border: 1px solid rgba(52, 211, 153, 0.3); } | |
| .v-stat-box h4 { color: #94a3b8; font-size: 0.9em; text-transform: uppercase; letter-spacing: 1px; margin: 0 0 10px 0; } | |
| @keyframes pulseGlow { 0% { box-shadow: 0 0 20px rgba(52, 211, 153, 0.4); } 50% { box-shadow: 0 0 50px rgba(52, 211, 153, 0.8); } 100% { box-shadow: 0 0 20px rgba(52, 211, 153, 0.4); } } | |
| .confetti-piece { position: absolute; width: 10px; height: 15px; top: -20px; opacity: 0; animation: fall linear forwards infinite; z-index: 10; border-radius: 2px;} | |
| @keyframes fall { 0% { transform: translateY(-20px) rotate(0deg); opacity: 1; } 100% { transform: translateY(1000px) rotate(720deg); opacity: 1; } } | |
| .defeat-dashboard { background: linear-gradient(135deg, #2b1010, #450a0a); padding: 40px; border-radius: 24px; border: 2px solid #ef4444; animation: pulseRed 2s infinite; color: white; position: relative; z-index: 5; margin-top: 20px; } | |
| .defeat-title { text-align: center; color: #ef4444; font-size: 3.5em !important; text-shadow: 0 0 20px rgba(239, 68, 68, 0.8); margin-bottom: 5px; } | |
| @keyframes pulseRed { 0% { box-shadow: 0 0 20px rgba(239, 68, 68, 0.4); } 50% { box-shadow: 0 0 60px rgba(239, 68, 68, 0.8); } 100% { box-shadow: 0 0 20px rgba(239, 68, 68, 0.4); } } | |
| .ash-piece { position: absolute; width: 8px; height: 8px; background-color: rgba(156, 163, 175, 0.6); border-radius: 50%; opacity: 0; animation: floatAsh linear forwards infinite; z-index: 10; filter: blur(1px); } | |
| @keyframes floatAsh { 0% { transform: translateY(600px) scale(1); opacity: 0; } 20% { opacity: 0.8; } 100% { transform: translateY(-100px) scale(2); opacity: 0; } } | |
| """ | |
| with gr.Blocks(css=custom_css, title="EcoQueryQuest") as demo: | |
| # 1. State initialized with 10.0L | |
| game_state = gr.State({ | |
| "water": 10.0, "energy": 0.0, "co2": 0.0, "points": 0, "queries": 0, | |
| "history": [], "game_over": False, "current_best_alt": None, "current_shuffled_alts": [] | |
| }) | |
| gr.Markdown("<h1>🌍 EcoQueryQuest</h1>") | |
| gr.Markdown("<div class='subtitle'>Select a model, type a query, and watch Drip react. Reach 500 points to win!</div>") | |
| # 2. Stats initialized with 10.0L | |
| stats_html = gr.HTML(get_stats_html(10.0, 0.0, 0.0, 0)) | |
| with gr.Column(visible=True) as main_game_ui: | |
| with gr.Row(): | |
| with gr.Column(scale=1, elem_classes=["drip-pod"]): | |
| # 3. Drip visual initialized with 10.0L | |
| drip_html = gr.HTML(get_drip_visuals(10.0)) | |
| drip_audio = gr.Audio(label="Drip's Voice", autoplay=True, interactive=False, elem_classes=["hidden-audio"]) | |
| drip_feedback = gr.HTML("<div class='feedback-box'>Waiting for your first query...</div>") | |
| with gr.Column(scale=2): | |
| model_selector = gr.Dropdown(choices=list(MODELS.keys()), value="GPT-5.4", label="Select Target LLM Backend", elem_classes=["custom-dropdown"], interactive=True, allow_custom_value=False, filterable=False) | |
| with gr.Row(elem_classes=["input-row"]): | |
| user_input = gr.Textbox(show_label=False, placeholder="Type your query here...", elem_classes=["custom-textbox"], scale=4, lines=3) | |
| submit_btn = gr.Button("Send Query", elem_classes=["custom-btn"], scale=1) | |
| impact_display = gr.HTML() | |
| with gr.Group(visible=False, elem_classes=["alt-group"]) as alternatives_group: | |
| gr.Markdown("<h3 style='text-align:center; color:#34d399; margin-bottom: 20px;'>🌱 Drip says: 'Quick! Pick a greener tool to earn points!'</h3>") | |
| with gr.Row(): | |
| alt_btn_1 = gr.Button("", elem_classes=["alt-card"]) | |
| alt_btn_2 = gr.Button("", elem_classes=["alt-card"]) | |
| alt_btn_3 = gr.Button("", elem_classes=["alt-card"]) | |
| alt_feedback = gr.Markdown() | |
| endgame_display = gr.HTML("") | |
| user_input.submit( | |
| fn=process_query, | |
| inputs=[user_input, model_selector, game_state], | |
| outputs=[game_state, stats_html, drip_html, drip_audio, impact_display, drip_feedback, alternatives_group, alt_btn_1, alt_btn_2, alt_btn_3, alt_feedback, user_input, main_game_ui, endgame_display] | |
| ) | |
| submit_btn.click( | |
| fn=process_query, | |
| inputs=[user_input, model_selector, game_state], | |
| outputs=[game_state, stats_html, drip_html, drip_audio, impact_display, drip_feedback, alternatives_group, alt_btn_1, alt_btn_2, alt_btn_3, alt_feedback, user_input, main_game_ui, endgame_display] | |
| ) | |
| for btn in [alt_btn_1, alt_btn_2, alt_btn_3]: | |
| btn.click( | |
| fn=select_alternative, | |
| inputs=[btn, game_state], | |
| outputs=[game_state, stats_html, alt_feedback, alternatives_group, main_game_ui, endgame_display] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(css=custom_css) |