Spaces:
Running
Running
| """ | |
| Choice — Check what you chose. | |
| Built for the Build Small Hackathon 2026. | |
| Log your decisions. Pick your path. Come back later. | |
| The Oracle watches your patterns and tells you what you | |
| already know but haven't admitted yet. | |
| "Your cautious choices work out better." | |
| "You do better when you decide quickly." | |
| "Most of your decisions are about money. That's where your attention lives." | |
| The past isn't fixed. It's a pattern. And patterns can be read. | |
| Everything stays in your browser. Nothing leaves your device. | |
| """ | |
| import gradio as gr | |
| import json | |
| import time | |
| import os | |
| import re | |
| from openai import OpenAI | |
| # --- NVIDIA Nemotron --- | |
| NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "") | |
| NEMOTRON_MODEL = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning" | |
| nvidia_client = None | |
| if NVIDIA_API_KEY: | |
| nvidia_client = OpenAI( | |
| base_url="https://integrate.api.nvidia.com/v1", | |
| api_key=NVIDIA_API_KEY, | |
| ) | |
| print("NVIDIA Nemotron connected.") | |
| else: | |
| print("No NVIDIA_API_KEY — running in fallback mode.") | |
| # --- Oracle Engine (ported from choice/src/lib/oracle.js) --- | |
| DOMAIN_KEYWORDS = { | |
| "money": ["buy", "sell", "spend", "invest", "pay", "loan", "debt", "price", "cost", "rent", "salary", "save", "budget", "finance"], | |
| "work": ["job", "career", "boss", "hire", "quit", "project", "deadline", "client", "meeting", "promotion", "work", "business", "startup"], | |
| "people": ["friend", "family", "partner", "date", "relationship", "talk", "tell", "trust", "text", "call", "forgive", "apologize", "mom", "dad", "kid"], | |
| "health": ["eat", "sleep", "exercise", "gym", "doctor", "medicine", "therapy", "mental", "stress", "drink", "smoke", "sober"], | |
| "risk": ["risk", "safe", "gamble", "chance", "bet", "try", "leap", "bold", "scared", "afraid", "comfortable"], | |
| "home": ["move", "apartment", "house", "rent", "lease", "renovate", "fix", "clean"], | |
| } | |
| POSITIVE_WORDS = ["good", "great", "worked", "glad", "happy", "right", "best", "perfect", "worth", "amazing", "yes"] | |
| NEGATIVE_WORDS = ["bad", "wrong", "regret", "mistake", "terrible", "worse", "failed", "awful", "wish", "disaster"] | |
| def detect_domains(text): | |
| lower = text.lower() | |
| found = [dom for dom, kws in DOMAIN_KEYWORDS.items() if any(kw in lower for kw in kws)] | |
| return found if found else ["general"] | |
| def outcome_sentiment(actual): | |
| if not actual: | |
| return "unknown" | |
| lower = actual.lower() | |
| pos = sum(1 for w in POSITIVE_WORDS if w in lower) | |
| neg = sum(1 for w in NEGATIVE_WORDS if w in lower) | |
| if pos > neg: return "positive" | |
| if neg > pos: return "negative" | |
| return "neutral" | |
| def choice_style(chosen_index, num_options): | |
| if chosen_index == 0: return "safe" | |
| if num_options > 2 and chosen_index == num_options - 1: return "bold" | |
| return "moderate" | |
| def consult_oracle(decisions): | |
| """The Oracle: find patterns in resolved decisions.""" | |
| resolved = [d for d in decisions if d.get("resolved")] | |
| if len(resolved) < 3: | |
| return { | |
| "ready": False, | |
| "message": f"The Oracle needs at least 3 resolved decisions. You have {len(resolved)}.", | |
| "insights": [] | |
| } | |
| domain_counts = {} | |
| domain_outcomes = {} | |
| style_outcomes = {"safe": {"positive": 0, "negative": 0, "neutral": 0}, | |
| "moderate": {"positive": 0, "negative": 0, "neutral": 0}, | |
| "bold": {"positive": 0, "negative": 0, "neutral": 0}} | |
| timings = [] | |
| for d in resolved: | |
| domains = detect_domains(d["question"]) | |
| style = choice_style(d.get("chosen_index", 0), len(d.get("options", ["a", "b"]))) | |
| sentiment = outcome_sentiment(d.get("actual", "")) | |
| for dom in domains: | |
| domain_counts[dom] = domain_counts.get(dom, 0) + 1 | |
| if dom not in domain_outcomes: | |
| domain_outcomes[dom] = {"positive": 0, "negative": 0, "neutral": 0} | |
| domain_outcomes[dom][sentiment] += 1 | |
| style_outcomes[style][sentiment] += 1 | |
| if d.get("created_at") and d.get("resolved_at"): | |
| days = (d["resolved_at"] - d["created_at"]) / 86400 | |
| timings.append({"days": days, "sentiment": sentiment, "style": style}) | |
| insights = [] | |
| # Domain insights | |
| for dom, outcomes in domain_outcomes.items(): | |
| total = sum(outcomes.values()) | |
| if total >= 2: | |
| pos_rate = round(outcomes["positive"] / total * 100) | |
| neg_rate = round(outcomes["negative"] / total * 100) | |
| if outcomes["positive"] > outcomes["negative"]: | |
| insights.append(f"Your {dom} decisions have gone well {pos_rate}% of the time ({total} resolved).") | |
| elif outcomes["negative"] > outcomes["positive"]: | |
| insights.append(f"{neg_rate}% of your {dom} decisions had negative outcomes ({total} resolved). Slow down on this one.") | |
| # Style insights | |
| bold_total = sum(style_outcomes["bold"].values()) | |
| safe_total = sum(style_outcomes["safe"].values()) | |
| if bold_total >= 2 and safe_total >= 2: | |
| bold_pos = style_outcomes["bold"]["positive"] / bold_total if bold_total else 0 | |
| safe_pos = style_outcomes["safe"]["positive"] / safe_total if safe_total else 0 | |
| if bold_pos > safe_pos + 0.15: | |
| insights.append(f"When you take the bolder path, outcomes are better — {round(bold_pos*100)}% vs {round(safe_pos*100)}% for safe choices.") | |
| elif safe_pos > bold_pos + 0.15: | |
| insights.append(f"Your cautious choices work out better — {round(safe_pos*100)}% positive vs {round(bold_pos*100)}% for bold moves.") | |
| # Timing insights | |
| fast = [t for t in timings if t["days"] < 3] | |
| slow = [t for t in timings if t["days"] >= 7] | |
| if len(fast) >= 2 and len(slow) >= 2: | |
| fast_pos = sum(1 for t in fast if t["sentiment"] == "positive") / len(fast) | |
| slow_pos = sum(1 for t in slow if t["sentiment"] == "positive") / len(slow) | |
| if fast_pos > slow_pos + 0.15: | |
| insights.append(f"You do better when you decide quickly — {round(fast_pos*100)}% positive vs {round(slow_pos*100)}% when you deliberate.") | |
| elif slow_pos > fast_pos + 0.15: | |
| insights.append(f"Sleeping on it works for you — {round(slow_pos*100)}% positive when you take time vs {round(fast_pos*100)}% for snap decisions.") | |
| # Volume insight | |
| if domain_counts: | |
| top_dom = max(domain_counts, key=domain_counts.get) | |
| if domain_counts[top_dom] >= 3: | |
| insights.append(f"Most of your decisions are about {top_dom} ({domain_counts[top_dom]} of {len(resolved)}). That's where your attention lives.") | |
| return { | |
| "ready": True, | |
| "message": f"Based on {len(resolved)} resolved decisions:", | |
| "insights": insights | |
| } | |
| def ai_reflect(question, options, decisions): | |
| """AI-generated reflection on a new decision, given history.""" | |
| if not nvidia_client: | |
| return "" | |
| history_summary = "" | |
| resolved = [d for d in decisions if d.get("resolved")] | |
| if resolved: | |
| recent = resolved[-5:] | |
| lines = [] | |
| for d in recent: | |
| sent = outcome_sentiment(d.get("actual", "")) | |
| lines.append(f"- \"{d['question']}\" → chose \"{d.get('chosen', '?')}\" → {sent}") | |
| history_summary = f"\n\nRecent decisions:\n" + "\n".join(lines) | |
| try: | |
| response = nvidia_client.chat.completions.create( | |
| model=NEMOTRON_MODEL, | |
| messages=[ | |
| {"role": "system", "content": "You are a wise, direct advisor. Given someone's decision and their history of past choices, offer ONE brief insight (2-3 sentences max). Be honest. No fluff. If you see a pattern, name it."}, | |
| {"role": "user", "content": f"I'm deciding: {question}\nOptions: {', '.join(options)}{history_summary}"} | |
| ], | |
| max_tokens=400, | |
| temperature=0.5, | |
| ) | |
| return response.choices[0].message.content.strip() | |
| except: | |
| return "" | |
| # --- State Management --- | |
| def add_decision(question, option_a, option_b, option_c, state): | |
| """Add a new decision fork.""" | |
| decisions = state or [] | |
| options = [o for o in [option_a, option_b, option_c] if o and o.strip()] | |
| if not question or len(options) < 2: | |
| return state, render_decisions(decisions), "Need a question and at least 2 options." | |
| decision = { | |
| "id": str(int(time.time() * 1000)), | |
| "question": question.strip(), | |
| "options": options, | |
| "chosen": None, | |
| "chosen_index": None, | |
| "actual": None, | |
| "resolved": False, | |
| "created_at": time.time(), | |
| "resolved_at": None, | |
| } | |
| decisions.insert(0, decision) | |
| reflection = ai_reflect(question, options, decisions) | |
| return decisions, render_decisions(decisions), reflection if reflection else "Fork logged. Choose when you're ready." | |
| def make_choice(decision_id, choice_text, state): | |
| """Record which option was chosen.""" | |
| decisions = state or [] | |
| for d in decisions: | |
| if d["id"] == decision_id: | |
| if choice_text in d["options"]: | |
| d["chosen"] = choice_text | |
| d["chosen_index"] = d["options"].index(choice_text) | |
| return decisions, render_decisions(decisions) | |
| def resolve_decision(decision_id, actual_outcome, state): | |
| """Record what actually happened.""" | |
| decisions = state or [] | |
| for d in decisions: | |
| if d["id"] == decision_id: | |
| d["actual"] = actual_outcome | |
| d["resolved"] = True | |
| d["resolved_at"] = time.time() | |
| return decisions, render_decisions(decisions), render_oracle(decisions) | |
| # --- Rendering --- | |
| def render_decisions(decisions): | |
| if not decisions: | |
| return """<div class="ch-empty">No forks yet. Log your first decision above.</div>""" | |
| html = "" | |
| for d in decisions: | |
| status_class = "ch-resolved" if d["resolved"] else "ch-chosen" if d.get("chosen") else "ch-open" | |
| status_icon = "✅" if d["resolved"] else "→" if d.get("chosen") else "⑂" | |
| sentiment = "" | |
| if d["resolved"] and d.get("actual"): | |
| s = outcome_sentiment(d["actual"]) | |
| sentiment = {"positive": "🟢", "negative": "🔴", "neutral": "🟡"}.get(s, "") | |
| options_html = "" | |
| for i, opt in enumerate(d["options"]): | |
| is_chosen = d.get("chosen") == opt | |
| opt_class = "ch-opt-chosen" if is_chosen else "ch-opt" | |
| options_html += f'<span class="{opt_class}">{opt}</span>' | |
| html += f""" | |
| <div class="ch-card {status_class}" data-id="{d['id']}"> | |
| <div class="ch-question">{status_icon} {d['question']}</div> | |
| <div class="ch-options">{options_html}</div> | |
| {f'<div class="ch-actual">{sentiment} {d["actual"]}</div>' if d.get("actual") else ''} | |
| </div> | |
| """ | |
| return f'<div class="ch-list">{html}</div>' | |
| def render_oracle(decisions): | |
| oracle = consult_oracle(decisions or []) | |
| if not oracle["ready"]: | |
| return f"""<div class="ch-oracle-waiting">{oracle['message']}</div>""" | |
| insights_html = "".join(f'<div class="ch-insight">→ {i}</div>' for i in oracle["insights"]) | |
| return f""" | |
| <div class="ch-oracle"> | |
| <div class="ch-oracle-title">The Oracle</div> | |
| <div class="ch-oracle-subtitle">{oracle['message']}</div> | |
| {insights_html} | |
| </div> | |
| """ | |
| # --- CSS --- | |
| CUSTOM_CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap'); | |
| .ch-empty { | |
| text-align: center; color: #666; padding: 40px; | |
| font-family: 'Space Grotesk', sans-serif; font-style: italic; | |
| } | |
| .ch-list { display: flex; flex-direction: column; gap: 12px; } | |
| .ch-card { | |
| font-family: 'Space Grotesk', sans-serif; | |
| background: #ffffff; border-radius: 12px; padding: 16px 20px; | |
| border-left: 4px solid #ccc; | |
| } | |
| .ch-open { border-left-color: #F2CC8F; } | |
| .ch-chosen { border-left-color: #81B29A; } | |
| .ch-resolved { border-left-color: #9370DB; opacity: 0.8; } | |
| .ch-question { | |
| font-size: 1.05em; font-weight: 600; color: #1a1a2e; margin-bottom: 8px; | |
| } | |
| .ch-options { display: flex; gap: 8px; flex-wrap: wrap; } | |
| .ch-opt { | |
| font-size: 0.85em; padding: 4px 12px; border-radius: 20px; | |
| background: #f0f0f5; color: #666; border: 1px solid #ddd; | |
| } | |
| .ch-opt-chosen { | |
| font-size: 0.85em; padding: 4px 12px; border-radius: 20px; | |
| background: #81B29A22; color: #81B29A; border: 1px solid #81B29A; | |
| font-weight: 600; | |
| } | |
| .ch-actual { | |
| margin-top: 8px; font-size: 0.9em; color: #555; | |
| border-top: 1px solid #eee; padding-top: 8px; font-style: italic; | |
| } | |
| .ch-oracle { | |
| font-family: 'Space Grotesk', sans-serif; | |
| background: linear-gradient(135deg, #f8f6ff, #f0ecff); | |
| border-radius: 16px; padding: 24px; border: 1px solid #9370DB33; | |
| } | |
| .ch-oracle-title { | |
| font-size: 1.2em; font-weight: 700; color: #9370DB; | |
| letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 4px; | |
| } | |
| .ch-oracle-subtitle { font-size: 0.8em; color: #666; margin-bottom: 16px; } | |
| .ch-oracle-waiting { | |
| font-family: 'Space Grotesk', sans-serif; | |
| text-align: center; color: #888; padding: 20px; font-style: italic; | |
| } | |
| .ch-insight { | |
| font-size: 0.95em; color: #333; padding: 8px 0; | |
| border-bottom: 1px solid #e0e0e0; line-height: 1.5; | |
| } | |
| .ch-insight:last-child { border-bottom: none; } | |
| .gradio-container { background: #f5f5f8 !important; max-width: 800px !important; margin: 0 auto !important; } | |
| .gr-button-primary { | |
| background: #9370DB !important; border: none !important; | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| } | |
| .gr-button-primary:hover { background: #7B5EC2 !important; } | |
| footer { display: none !important; } | |
| """ | |
| # --- App --- | |
| with gr.Blocks(css=CUSTOM_CSS, title="Choice", theme=gr.themes.Base()) as app: | |
| DEMO_DECISIONS = [ | |
| { | |
| "id": "demo-1", | |
| "question": "Should I quit my stable job to build something of my own?", | |
| "options": ["Stay safe and keep the paycheck", "Take the leap"], | |
| "chosen": "Take the leap", | |
| "chosen_index": 1, | |
| "actual": "Terrifying for six months but I built something real. Best decision I ever made.", | |
| "resolved": True, | |
| "resolved_at": 1718000000, | |
| "created_at": 1717900000, | |
| }, | |
| { | |
| "id": "demo-2", | |
| "question": "Do I tell my friend the truth about their partner?", | |
| "options": ["Keep quiet and let it play out", "Be honest and risk the friendship"], | |
| "chosen": "Be honest and risk the friendship", | |
| "chosen_index": 1, | |
| "actual": "They didn't talk to me for two weeks. Then they called and said I was right. We're closer now.", | |
| "resolved": True, | |
| "resolved_at": 1717500000, | |
| "created_at": 1717400000, | |
| }, | |
| { | |
| "id": "demo-3", | |
| "question": "Should I spend the money on the trip or save it?", | |
| "options": ["Save it for rent", "Book the flight"], | |
| "chosen": "Book the flight", | |
| "chosen_index": 1, | |
| "actual": "Best week of my life. The money came back. The memories wouldn't have.", | |
| "resolved": True, | |
| "resolved_at": 1718100000, | |
| "created_at": 1718000000, | |
| }, | |
| { | |
| "id": "demo-4", | |
| "question": "Apply for the promotion or stay where I'm comfortable?", | |
| "options": ["Stay comfortable", "Apply and risk rejection"], | |
| "chosen": "Apply and risk rejection", | |
| "chosen_index": 1, | |
| "actual": "Got it. Turned out I was the only one who applied. Everyone else was too scared.", | |
| "resolved": True, | |
| "resolved_at": 1718200000, | |
| "created_at": 1718100000, | |
| }, | |
| { | |
| "id": "demo-5", | |
| "question": "Invest in my friend's startup or keep the cash safe?", | |
| "options": ["Keep it in savings", "Invest and believe in them"], | |
| "chosen": "Keep it in savings", | |
| "chosen_index": 0, | |
| "actual": "They folded in 8 months. Glad I trusted my gut on that one.", | |
| "resolved": True, | |
| "resolved_at": 1717800000, | |
| "created_at": 1717000000, | |
| }, | |
| { | |
| "id": "demo-6", | |
| "question": "Call my dad or wait for him to call me?", | |
| "options": ["Wait for him", "Pick up the phone"], | |
| "chosen": "Wait for him", | |
| "chosen_index": 0, | |
| "actual": "He never called. I should have called. That one still sits with me.", | |
| "resolved": True, | |
| "resolved_at": 1717600000, | |
| "created_at": 1717000000, | |
| }, | |
| ] | |
| state = gr.State(DEMO_DECISIONS) | |
| gr.HTML(""" | |
| <div style="text-align:center; padding: 20px 0 8px;"> | |
| <h1 style="font-family: 'Space Grotesk', sans-serif; color: #1a1a2e; font-size: 2.4em; font-weight: 700;"> | |
| Ch<span style="color:#9370DB">o</span>ice | |
| </h1> | |
| <p style="font-family: 'Space Grotesk', sans-serif; color: #666; font-size: 0.95em;"> | |
| Check what you chose. | |
| </p> | |
| </div> | |
| """) | |
| with gr.Accordion("New Fork", open=True): | |
| question = gr.Textbox(label="The decision", placeholder="Should I quit my job and start the thing?") | |
| with gr.Row(): | |
| opt_a = gr.Textbox(label="Option A", placeholder="Stay safe") | |
| opt_b = gr.Textbox(label="Option B", placeholder="Take the leap") | |
| opt_c = gr.Textbox(label="Option C (optional)", placeholder="") | |
| add_btn = gr.Button("Log Fork", variant="primary") | |
| reflection = gr.Textbox(label="AI Reflection", interactive=False, visible=True, lines=6) | |
| decisions_html = gr.HTML(value=render_decisions(DEMO_DECISIONS), label="Your Forks") | |
| oracle_html = gr.HTML(value=render_oracle(DEMO_DECISIONS), label="The Oracle") | |
| with gr.Accordion("Resolve a Decision", open=False): | |
| resolve_id = gr.Textbox(label="Decision ID (from the card)") | |
| with gr.Row(): | |
| choice_text = gr.Textbox(label="What you chose", placeholder="The option text") | |
| choose_btn = gr.Button("Lock In Choice") | |
| actual = gr.Textbox(label="What actually happened?", placeholder="It worked out great / It was a disaster / Somewhere in between") | |
| resolve_btn = gr.Button("Resolve", variant="primary") | |
| add_btn.click( | |
| fn=add_decision, | |
| inputs=[question, opt_a, opt_b, opt_c, state], | |
| outputs=[state, decisions_html, reflection], | |
| ) | |
| choose_btn.click( | |
| fn=make_choice, | |
| inputs=[resolve_id, choice_text, state], | |
| outputs=[state, decisions_html], | |
| ) | |
| resolve_btn.click( | |
| fn=resolve_decision, | |
| inputs=[resolve_id, actual, state], | |
| outputs=[state, decisions_html, oracle_html], | |
| ) | |
| gr.HTML(""" | |
| <div style="text-align:center; padding: 16px 0 4px; color: #444; font-size: 0.7em; font-family: 'Space Grotesk', sans-serif;"> | |
| Your decisions stay on your device. Choice stores nothing.<br> | |
| <span style="color:#9370DB;">Heuremen — Build Small Hackathon 2026</span> | |
| </div> | |
| """) | |
| if __name__ == "__main__": | |
| app.launch() | |