| """ |
| Reality Divergence + Elsewhere |
| AI-native platform for exploring counterfactual personal and historical timelines. |
| Uses Qwen2.5-7B-Instruct (via the Hugging Face Inference API) for reasoning, and |
| renders results as structured, legible HTML cards. |
| """ |
|
|
| import base64 |
| import html |
| import json |
| import os |
| from io import BytesIO |
| from typing import Dict, Optional |
|
|
| import gradio as gr |
|
|
| import config |
| import prompts |
| from model_client import infer_text, infer_json |
| from image_client import get_image_client |
|
|
|
|
| |
| |
| |
|
|
| FALLBACK_ANALYSIS = { |
| "emotion_score": 6, |
| "regret_score": 7, |
| "self_blame_score": 3, |
| "category": "Career", |
| "risk_level": "low", |
| "risk_markers": [] |
| } |
|
|
| FALLBACK_MIRROR = ( |
| "You're carrying a moment where one choice—perhaps a risk declined or accepted—" |
| "changed the shape of what came next. A part of you may still be wondering who you'd have become " |
| "if that moment had unfolded differently. That curiosity deserves a careful frame." |
| ) |
|
|
| FALLBACK_TIMELINE = { |
| "title": "The Path Not Taken", |
| "divergence": "At the moment you made your choice, everything branched.", |
| "ripples": [ |
| "Your confidence would have grown in a different direction.", |
| "New relationships might have formed, others would have stayed distant.", |
| "Your skills would have developed along an alternate path.", |
| "The people around you would have seen a different version of you.", |
| "Your understanding of yourself would be fundamentally different." |
| ], |
| "the_you": ( |
| "You might have become more outwardly bold or more introspective, depending on the path. " |
| "But you would have carried different lessons, different wounds, different strengths." |
| ), |
| "return": { |
| "opening": "In the life you actually lived, you learned something you could not have learned elsewhere.", |
| "strengths": [ |
| "You learned to make choices under incomplete information.", |
| "You carried responsibility even when the answer was unclear.", |
| "You kept enough curiosity to revisit this moment with honesty." |
| ], |
| "lesson": "A path can be meaningful without being the only path that could have mattered.", |
| "reflection_question": "What did your actual timeline make possible that the alternate one might have cost?" |
| }, |
| "quote": "Another life may have opened doors, but this one taught you how to stand." |
| } |
|
|
| FALLBACK_WORLD_DIVERGENCE = { |
| "title": "Apple Never Launches the iPhone", |
| "divergence_point": "2007: Apple decides to focus on computers instead of mobile", |
| "year_divergence": 2007, |
| "ripples": [ |
| "Business phones and BlackBerrys dominate mobile computing.", |
| "Apps arrive later and through different distribution channels.", |
| "Mobile photography remains less central to daily life.", |
| "Touchscreen adoption slows without the iPhone's refinement.", |
| "Tech culture evolves around portable computing differently." |
| ], |
| "world_outcome": ( |
| "A world where mobile computing exists but remains fragmented. " |
| "Productivity-focused devices outnumber entertainment devices. " |
| "The social media landscape never reaches its current scale." |
| ), |
| "economy": "Business hardware companies thrive. Software consolidation never happens.", |
| "culture": "Always-connected culture develops slower. Social media remains desktop-first.", |
| "technology": "Cloud services evolve differently. AR/VR developments diverge.", |
| "winners": ["Microsoft", "BlackBerry", "Desktop computing"], |
| "losers": ["App-based startups", "Mobile creators", "Social media explosion"], |
| "reflection": "One device shaped a culture. What else in our world hinges on a single innovation?" |
| } |
|
|
|
|
| |
| |
| |
|
|
| def safe_int(val, default=0, min_val=0, max_val=10): |
| """Safely convert and clamp integer values.""" |
| try: |
| return max(min_val, min(max_val, int(val))) |
| except (ValueError, TypeError): |
| return default |
|
|
|
|
| def safe_str(val, default=""): |
| """Safely convert to string.""" |
| try: |
| return str(val).strip() if val else default |
| except Exception: |
| return default |
|
|
|
|
| def safe_list(val, default=None): |
| """Safely extract list values.""" |
| try: |
| if isinstance(val, list): |
| return [safe_str(v) for v in val] |
| return default or [] |
| except Exception: |
| return default or [] |
|
|
|
|
| def merge_analysis_with_fallback(parsed: Optional[Dict]) -> Dict: |
| """Merge parsed analysis with fallback; one-way upgrade (risk only escalates).""" |
| if not parsed: |
| return FALLBACK_ANALYSIS |
| |
| fallback = FALLBACK_ANALYSIS.copy() |
| |
| |
| result = { |
| "emotion_score": safe_int(parsed.get("emotion_score"), fallback["emotion_score"]), |
| "regret_score": safe_int(parsed.get("regret_score"), fallback["regret_score"]), |
| "self_blame_score": safe_int(parsed.get("self_blame_score"), fallback["self_blame_score"]), |
| "category": safe_str(parsed.get("category"), fallback["category"]), |
| "risk_level": "high" if (parsed.get("risk_level") == "high" or fallback["risk_level"] == "high") else safe_str(parsed.get("risk_level"), fallback["risk_level"]), |
| "risk_markers": safe_list(parsed.get("risk_markers"), fallback["risk_markers"]) |
| } |
| |
| return result |
|
|
|
|
| |
| |
| |
|
|
| def analyze_story(story: str) -> Dict: |
| """Analyze personal story for emotion, regret, and risk.""" |
| if not story or len(story.strip()) < config.MIN_STORY_LENGTH: |
| return FALLBACK_ANALYSIS |
| |
| |
| parsed = infer_json(prompts.ANALYSIS_PROMPT, story, temperature=0.1, max_tokens=1024) |
| return merge_analysis_with_fallback(parsed) |
|
|
|
|
| def generate_mirror(story: str, analysis: Dict) -> str: |
| """Generate compassionate mirror reflection.""" |
| prompt = f"Story:\n{story}\n\nAnalysis:\n{json.dumps(analysis)}" |
| mirror = infer_text(prompts.MIRROR_PROMPT, prompt, temperature=0.55, max_tokens=150) |
| return mirror if mirror else FALLBACK_MIRROR |
|
|
|
|
| def generate_timeline(story: str, analysis: Dict, direction: str) -> Dict: |
| """Generate counterfactual timeline (Upward/Downward).""" |
| prompt = f"""Story:\n{story}\n\nAnalysis:\n{json.dumps(analysis)}\n\nDirection: {direction}""" |
| parsed = infer_json(prompts.TIMELINE_PROMPT, prompt, temperature=0.7, max_tokens=1024) |
| |
| if not parsed: |
| return FALLBACK_TIMELINE |
| |
| |
| return { |
| "title": safe_str(parsed.get("title"), FALLBACK_TIMELINE["title"]), |
| "divergence": safe_str(parsed.get("divergence"), FALLBACK_TIMELINE["divergence"]), |
| "ripples": safe_list(parsed.get("ripples"), FALLBACK_TIMELINE["ripples"]), |
| "the_you": safe_str(parsed.get("the_you"), FALLBACK_TIMELINE["the_you"]), |
| "return": { |
| "opening": safe_str(parsed.get("return", {}).get("opening"), FALLBACK_TIMELINE["return"]["opening"]), |
| "strengths": safe_list(parsed.get("return", {}).get("strengths"), FALLBACK_TIMELINE["return"]["strengths"]), |
| "lesson": safe_str(parsed.get("return", {}).get("lesson"), FALLBACK_TIMELINE["return"]["lesson"]), |
| "reflection_question": safe_str(parsed.get("return", {}).get("reflection_question"), FALLBACK_TIMELINE["return"]["reflection_question"]) |
| }, |
| "quote": safe_str(parsed.get("quote"), FALLBACK_TIMELINE["quote"]) |
| } |
|
|
|
|
| def generate_world_divergence(scenario: str) -> Dict: |
| """Generate world/historical divergence.""" |
| prompt = f"Divergence scenario: {scenario}" |
| parsed = infer_json(prompts.WORLD_DIVERGENCE_PROMPT, prompt, temperature=0.7, max_tokens=1024) |
| |
| if not parsed: |
| return FALLBACK_WORLD_DIVERGENCE |
| |
| return { |
| "title": safe_str(parsed.get("title"), FALLBACK_WORLD_DIVERGENCE["title"]), |
| "divergence_point": safe_str(parsed.get("divergence_point"), FALLBACK_WORLD_DIVERGENCE["divergence_point"]), |
| "year_divergence": safe_int(parsed.get("year_divergence"), 2000, 1800, 2050), |
| "ripples": safe_list(parsed.get("ripples"), FALLBACK_WORLD_DIVERGENCE["ripples"]), |
| "world_outcome": safe_str(parsed.get("world_outcome"), FALLBACK_WORLD_DIVERGENCE["world_outcome"]), |
| "economy": safe_str(parsed.get("economy"), FALLBACK_WORLD_DIVERGENCE["economy"]), |
| "culture": safe_str(parsed.get("culture"), FALLBACK_WORLD_DIVERGENCE["culture"]), |
| "technology": safe_str(parsed.get("technology"), FALLBACK_WORLD_DIVERGENCE["technology"]), |
| "winners": safe_list(parsed.get("winners"), FALLBACK_WORLD_DIVERGENCE["winners"]), |
| "losers": safe_list(parsed.get("losers"), FALLBACK_WORLD_DIVERGENCE["losers"]), |
| "reflection": safe_str(parsed.get("reflection"), FALLBACK_WORLD_DIVERGENCE["reflection"]) |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| _NO_TEXT = "no text, no words, no letters, no typography, no captions, no signage" |
|
|
|
|
| def scene_data_uri(prompt: str) -> str: |
| """Generate a text-free atmospheric image and return it as a base64 data URI. |
| |
| Returns "" on any failure so the cards still render without an image. |
| """ |
| if not config.ENABLE_IMAGE_GEN: |
| return "" |
| try: |
| img = get_image_client().generate(prompt) |
| if img is None: |
| return "" |
| buf = BytesIO() |
| img.convert("RGB").save(buf, format="JPEG", quality=85) |
| b64 = base64.b64encode(buf.getvalue()).decode("ascii") |
| return f"data:image/jpeg;base64,{b64}" |
| except Exception as e: |
| print(f"[warn]scene image failed: {e}") |
| return "" |
|
|
|
|
| def timeline_scene_prompt(timeline: Dict, direction: str) -> str: |
| """Build a text-free image prompt for a personal timeline.""" |
| mood = "warm golden hopeful luminous sunrise" if direction.lower() == "upward" else "moody cool somber dramatic dusk" |
| title = timeline.get("title", "an alternate life") |
| return ( |
| f"Atmospheric cinematic symbolic landscape representing the theme '{title}', " |
| f"{mood}, dreamlike concept art, soft volumetric light, painterly, {_NO_TEXT}" |
| ) |
|
|
|
|
| def world_scene_prompt(world: Dict) -> str: |
| """Build a text-free image prompt for an alternate world.""" |
| title = world.get("title", "an alternate world") |
| return ( |
| f"Epic cinematic establishing shot of an alternate world: '{title}', " |
| f"atmospheric concept art, dramatic lighting, highly detailed, {_NO_TEXT}" |
| ) |
|
|
|
|
| def _list_items(items) -> str: |
| """Render a list of strings as escaped <li> elements.""" |
| return "".join(f"<li>{html.escape(str(i))}</li>" for i in items if str(i).strip()) |
|
|
|
|
| def _timeline_nodes(items) -> str: |
| """Render a list of strings as glowing timeline-spine nodes.""" |
| return "".join( |
| f"<div class='rd-node'><p>{html.escape(str(i))}</p></div>" |
| for i in items if str(i).strip() |
| ) |
|
|
|
|
| def _check_items(items) -> str: |
| """Render a list of strings as checkmark list items.""" |
| return "".join(f"<li>{html.escape(str(i))}</li>" for i in items if str(i).strip()) |
|
|
|
|
| def render_timeline_html(timeline: Dict, direction: str, image_uri: str = "") -> str: |
| """Render a personal counterfactual timeline as structured cards.""" |
| is_up = direction.lower() == "upward" |
| arrow = "↑" if is_up else "↓" |
| label = "The path where it went better" if is_up else "The path where it went harder" |
| ret = timeline.get("return", {}) or {} |
| img = f"<img class='rd-hero-img' src='{image_uri}' alt=''>" if image_uri else "" |
| return f""" |
| <div class="rd-stack"> |
| <div class="rd-card rd-fork"> |
| {img} |
| <div class="rd-spec">⚗ One possible path — modeled, not predicted</div> |
| <div class="rd-badge">{arrow}</div> |
| <div class="rd-eyebrow">{label}</div> |
| <h2 class="rd-title">{html.escape(timeline.get('title', 'Alternate Timeline'))}</h2> |
| <p class="rd-lead">{html.escape(timeline.get('divergence', ''))}</p> |
| </div> |
| <div class="rd-card"> |
| <div class="rd-eyebrow">⏳ Ripples through the years</div> |
| <div class="rd-timeline">{_timeline_nodes(timeline.get('ripples', []))}</div> |
| </div> |
| <div class="rd-card"> |
| <div class="rd-eyebrow">🌗 Who you might have become</div> |
| <p>{html.escape(timeline.get('the_you', ''))}</p> |
| </div> |
| <div class="rd-card rd-return"> |
| <div class="rd-eyebrow">↩ Returning to your actual life</div> |
| <p>{html.escape(ret.get('opening', ''))}</p> |
| <div class="rd-sublabel">Strengths you actually built</div> |
| <ul class="rd-checks">{_check_items(ret.get('strengths', []))}</ul> |
| <p class="rd-lesson"><strong>Lesson:</strong> {html.escape(ret.get('lesson', ''))}</p> |
| </div> |
| <div class="rd-card rd-question"> |
| <div class="rd-eyebrow">💭 A question to sit with</div> |
| <p class="rd-q">{html.escape(ret.get('reflection_question', ''))}</p> |
| </div> |
| <blockquote class="rd-quote">{html.escape(timeline.get('quote', ''))}</blockquote> |
| </div> |
| """ |
|
|
|
|
| def render_world_html(world: Dict, image_uri: str = "") -> str: |
| """Render an alternate-world divergence as structured cards.""" |
| img = f"<img class='rd-hero-img' src='{image_uri}' alt=''>" if image_uri else "" |
| return f""" |
| <div class="rd-stack"> |
| <div class="rd-card rd-fork"> |
| {img} |
| <div class="rd-spec">⚗ Speculative — causal chains are modeled, not known</div> |
| <div class="rd-badge">🌍</div> |
| <div class="rd-eyebrow">Year {html.escape(str(world.get('year_divergence', '')))} · Point of divergence</div> |
| <h2 class="rd-title">{html.escape(world.get('title', 'Alternate World'))}</h2> |
| <p class="rd-lead">{html.escape(world.get('divergence_point', ''))}</p> |
| </div> |
| <div class="rd-card"> |
| <div class="rd-eyebrow">⏳ Ripples</div> |
| <div class="rd-timeline">{_timeline_nodes(world.get('ripples', []))}</div> |
| </div> |
| <div class="rd-card"> |
| <div class="rd-eyebrow">🌐 How this world evolved</div> |
| <p>{html.escape(world.get('world_outcome', ''))}</p> |
| </div> |
| <div class="rd-grid"> |
| <div class="rd-card rd-stat"><span class="rd-ico">💰</span><div class="rd-sublabel">Economy</div><p>{html.escape(world.get('economy', ''))}</p></div> |
| <div class="rd-card rd-stat"><span class="rd-ico">🎭</span><div class="rd-sublabel">Culture</div><p>{html.escape(world.get('culture', ''))}</p></div> |
| <div class="rd-card rd-stat"><span class="rd-ico">⚙️</span><div class="rd-sublabel">Technology</div><p>{html.escape(world.get('technology', ''))}</p></div> |
| </div> |
| <div class="rd-grid-2"> |
| <div class="rd-card rd-winners"><span class="rd-ico">📈</span><div class="rd-sublabel">Who thrived</div><ul class="rd-list">{_list_items(world.get('winners', []))}</ul></div> |
| <div class="rd-card rd-losers"><span class="rd-ico">📉</span><div class="rd-sublabel">Who struggled</div><ul class="rd-list">{_list_items(world.get('losers', []))}</ul></div> |
| </div> |
| <div class="rd-card rd-question"> |
| <div class="rd-eyebrow">🧭 What it teaches us</div> |
| <p class="rd-q">{html.escape(world.get('reflection', ''))}</p> |
| </div> |
| </div> |
| """ |
|
|
|
|
| |
| |
| |
|
|
| CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); |
| |
| :root { |
| --bg-dark: #050816; |
| --bg-mid: #08111F; |
| --bg-light: #130A24; |
| --accent-blue: #38BDF8; |
| --accent-purple: #8B5CF6; |
| --text-primary: #F8FAFC; |
| --text-secondary: #94A3B8; |
| --border: #334155; |
| } |
| |
| body, .gradio-container { |
| background: linear-gradient(135deg, var(--bg-dark), var(--bg-mid), var(--bg-light)); |
| color: var(--text-primary); |
| font-family: Inter, system-ui, sans-serif; |
| } |
| |
| .gradio-container { |
| max-width: 1400px; |
| margin: auto; |
| } |
| |
| /* Headers */ |
| .hero-title { |
| display: inline-block; |
| font-size: 3.5rem; |
| font-weight: 800; |
| letter-spacing: -1px; |
| margin-top: 0.5rem; |
| color: #7DD3FC; /* solid, always-visible fallback */ |
| } |
| /* Only apply the transparent gradient-text effect where it is actually supported, |
| and force our background to win over Gradio's theme CSS. */ |
| @supports ((-webkit-background-clip: text) or (background-clip: text)) { |
| .hero-title { |
| background: linear-gradient(110deg, var(--accent-blue), var(--accent-purple), #F0ABFC, var(--accent-blue)) !important; |
| background-size: 250% auto !important; |
| -webkit-background-clip: text !important; |
| background-clip: text !important; |
| -webkit-text-fill-color: transparent !important; |
| animation: rdHero 7s ease infinite; |
| } |
| } |
| @keyframes rdHero { |
| 0% { background-position: 0% center; } |
| 50% { background-position: 100% center; } |
| 100% { background-position: 0% center; } |
| } |
| |
| .hero-subtitle { |
| font-size: 1.25rem; |
| color: var(--text-secondary); |
| margin-top: 1rem; |
| } |
| |
| /* Cards */ |
| .timeline-card, .artifact-container, .world-card { |
| background: linear-gradient(180deg, rgba(17, 24, 39, 0.5), rgba(11, 16, 32, 0.3)); |
| border: 1px solid var(--border); |
| border-radius: 16px; |
| padding: 24px; |
| backdrop-filter: blur(8px); |
| } |
| |
| .timeline-card:hover { |
| border-color: var(--accent-blue); |
| box-shadow: 0 0 20px rgba(56, 189, 248, 0.2); |
| } |
| |
| /* Buttons */ |
| .gr-button { |
| background: var(--accent-blue) !important; |
| color: white !important; |
| border: 0 !important; |
| border-radius: 12px !important; |
| font-weight: 700 !important; |
| transition: all 0.3s ease; |
| } |
| |
| .gr-button:hover { |
| background: var(--accent-purple) !important; |
| box-shadow: 0 0 20px rgba(139, 92, 246, 0.4); |
| } |
| |
| /* Tabs */ |
| .gr-tabs-nav { |
| border-bottom: 2px solid var(--border); |
| } |
| |
| .gr-tabs-nav .gr-tab-nav-item { |
| color: var(--text-secondary); |
| font-weight: 600; |
| } |
| |
| .gr-tabs-nav .gr-tab-nav-item.gr-tab-nav-item-selected { |
| color: var(--accent-blue); |
| border-bottom: 3px solid var(--accent-blue); |
| } |
| |
| /* Layout helpers */ |
| .parallel-layout { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 2rem; |
| margin-top: 2rem; |
| } |
| |
| .timeline-column, .image-column { |
| display: flex; |
| flex-direction: column; |
| gap: 1.5rem; |
| } |
| |
| .artifact-grid { |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 1rem; |
| margin-top: 1rem; |
| } |
| |
| .artifact-item { |
| border-radius: 12px; |
| overflow: hidden; |
| border: 1px solid var(--border); |
| } |
| |
| .artifact-item img { |
| width: 100%; |
| height: auto; |
| display: block; |
| } |
| |
| /* Mobile */ |
| @media (max-width: 1024px) { |
| .parallel-layout { |
| grid-template-columns: 1fr; |
| } |
| |
| .hero-title { |
| font-size: 2rem; |
| } |
| |
| .artifact-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| /* Structured result cards (timeline / world) */ |
| @keyframes rdFadeUp { |
| from { opacity: 0; transform: translateY(14px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .rd-stack { display: flex; flex-direction: column; gap: 1.1rem; margin-top: 0.5rem; } |
| |
| .rd-card { |
| position: relative; |
| overflow: hidden; |
| background: linear-gradient(180deg, rgba(17, 24, 39, 0.65), rgba(11, 16, 32, 0.4)); |
| border: 1px solid var(--border); |
| border-radius: 18px; |
| padding: 22px 26px; |
| animation: rdFadeUp 0.5s ease both; |
| transition: border-color .25s ease, box-shadow .25s ease, transform .25s ease; |
| } |
| .rd-card:hover { |
| border-color: rgba(56, 189, 248, 0.55); |
| box-shadow: 0 10px 34px rgba(5, 8, 22, 0.6), 0 0 24px rgba(56, 189, 248, 0.12); |
| transform: translateY(-2px); |
| } |
| |
| .rd-fork { |
| border: 1px solid rgba(56, 189, 248, 0.35); |
| background: |
| radial-gradient(1100px 200px at 0% 0%, rgba(56, 189, 248, 0.13), transparent 60%), |
| linear-gradient(180deg, rgba(17, 24, 39, 0.72), rgba(11, 16, 32, 0.45)); |
| } |
| .rd-fork::before, .rd-return::before { |
| content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; |
| background: linear-gradient(180deg, var(--accent-blue), var(--accent-purple)); |
| } |
| .rd-question { |
| border: 1px solid rgba(139, 92, 246, 0.5); |
| background: |
| radial-gradient(800px 130px at 100% 0%, rgba(139, 92, 246, 0.18), transparent 70%), |
| rgba(139, 92, 246, 0.06); |
| } |
| |
| .rd-hero-img { |
| display: block; width: 100%; height: 220px; object-fit: cover; |
| border-radius: 14px; margin: -2px 0 16px; |
| border: 1px solid var(--border); |
| box-shadow: 0 8px 30px rgba(5, 8, 22, 0.55); |
| } |
| |
| .rd-spec { |
| display: inline-flex; align-items: center; gap: 0.4rem; |
| font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; |
| text-transform: uppercase; |
| color: #FBBF24; |
| background: rgba(251, 191, 36, 0.12); |
| border: 1px solid rgba(251, 191, 36, 0.4); |
| border-radius: 999px; |
| padding: 4px 12px; |
| margin-bottom: 0.9rem; |
| } |
| |
| .rd-badge { |
| display: inline-flex; align-items: center; justify-content: center; |
| width: 46px; height: 46px; border-radius: 50%; |
| font-size: 1.4rem; font-weight: 800; color: #fff; |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); |
| box-shadow: 0 0 20px rgba(56, 189, 248, 0.5); |
| margin-bottom: 0.7rem; |
| } |
| |
| .rd-eyebrow { |
| font-size: 0.78rem; font-weight: 700; letter-spacing: 0.08em; |
| text-transform: uppercase; color: var(--accent-blue); margin-bottom: 0.55rem; |
| } |
| .rd-sublabel { font-size: 0.85rem; font-weight: 700; color: var(--text-secondary); margin: 0.9rem 0 0.4rem; letter-spacing: .03em; } |
| .rd-title { font-size: 1.7rem; font-weight: 800; margin: 0.1rem 0 0.5rem; color: var(--text-primary); letter-spacing: -0.5px; } |
| .rd-lead { color: var(--text-secondary); font-size: 1.08rem; line-height: 1.55; } |
| .rd-card p { color: var(--text-primary); line-height: 1.65; margin: 0.25rem 0; } |
| |
| /* Timeline spine */ |
| .rd-timeline { position: relative; margin: 0.3rem 0 0; padding-left: 30px; } |
| .rd-timeline::before { |
| content: ""; position: absolute; left: 7px; top: 6px; bottom: 6px; width: 2px; |
| background: linear-gradient(180deg, var(--accent-blue), var(--accent-purple)); |
| opacity: 0.55; |
| } |
| .rd-node { position: relative; padding-bottom: 1rem; } |
| .rd-node:last-child { padding-bottom: 0; } |
| .rd-node::before { |
| content: ""; position: absolute; left: -30px; top: 4px; |
| width: 14px; height: 14px; border-radius: 50%; |
| background: var(--accent-blue); border: 3px solid var(--bg-dark); |
| box-shadow: 0 0 12px rgba(56, 189, 248, 0.75); |
| } |
| .rd-node p { margin: 0; color: var(--text-primary); line-height: 1.6; } |
| |
| /* Strengths with check marks */ |
| .rd-checks { list-style: none; margin: 0.25rem 0 0; padding: 0; } |
| .rd-checks li { position: relative; padding-left: 1.7rem; margin-bottom: 0.45rem; color: var(--text-primary); line-height: 1.55; } |
| .rd-checks li::before { content: "✓"; position: absolute; left: 0; color: #34D399; font-weight: 800; } |
| |
| .rd-list { margin: 0.25rem 0 0; padding-left: 1.2rem; } |
| .rd-list li { color: var(--text-primary); line-height: 1.6; margin-bottom: 0.35rem; } |
| .rd-lesson { margin-top: 0.8rem; color: var(--text-secondary); } |
| .rd-q { font-size: 1.15rem; font-style: italic; color: var(--text-primary); line-height: 1.6; } |
| |
| .rd-quote { |
| position: relative; margin: 0.3rem 0 0; padding: 0.5rem 0 0.5rem 2.6rem; |
| font-size: 1.28rem; font-style: italic; color: var(--text-primary); line-height: 1.5; |
| } |
| .rd-quote::before { |
| content: "\201C"; position: absolute; left: 0; top: 0; |
| font-size: 3.4rem; line-height: 1; color: var(--accent-purple); font-family: Georgia, serif; |
| } |
| |
| /* Stat + win/lose grids */ |
| .rd-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; } |
| .rd-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } |
| .rd-stat { border-top: 3px solid var(--accent-blue); } |
| .rd-winners { border-left: 3px solid #34D399; } |
| .rd-losers { border-left: 3px solid #F87171; } |
| .rd-ico { display: block; font-size: 1.6rem; margin-bottom: 0.35rem; } |
| |
| @media (max-width: 1024px) { |
| .rd-grid, .rd-grid-2 { grid-template-columns: 1fr; } |
| } |
| """ |
|
|
|
|
| |
| |
| |
|
|
| with gr.Blocks(css=CSS, title="Reality Divergence") as demo: |
| |
| |
| story_state = gr.State("") |
| analysis_state = gr.State(None) |
| timeline_state = gr.State(None) |
| direction_state = gr.State("Upward") |
| |
| with gr.Tabs(): |
| |
| with gr.TabItem("🪞 Elsewhere (Your Timeline)", id="elsewhere"): |
|
|
| |
| gr.HTML("<div class='hero-title'>Explore the Life You Didn't Live</div>") |
| gr.HTML("<div class='hero-subtitle'>A compassionate reflection on one choice that changed everything.</div>") |
|
|
| |
| with gr.Group(): |
| gr.Markdown("#### Your Moment") |
| story_input = gr.Textbox( |
| label="Tell me about a decision, event, or conversation that changed the shape of what came next.", |
| placeholder="Share what happened, what you chose, and what you wonder about...", |
| lines=8, |
| max_lines=12 |
| ) |
| submit_btn = gr.Button("Reflect", variant="primary", size="lg") |
|
|
| |
| with gr.Group(): |
| gr.Markdown("#### Your Reflection") |
| mirror_output = gr.Markdown(FALLBACK_MIRROR, elem_classes=["timeline-card"]) |
|
|
| |
| gr.Markdown("#### Choose a Path") |
| with gr.Row(): |
| upward_btn = gr.Button("↑ What if it went better?", variant="primary") |
| downward_btn = gr.Button("↓ What if it went worse?") |
|
|
| |
| timeline_result = gr.HTML(visible=False) |
| return_button = gr.Button("↩ Return to Your Actual Life", visible=False, variant="primary") |
|
|
| |
| with gr.TabItem("🌍 Reality Divergence (The World)", id="reality"): |
|
|
| |
| gr.HTML("<div class='hero-title'>The Museum of Worlds That Never Happened</div>") |
| gr.HTML("<div class='hero-subtitle'>What if one decision shaped the entire world?</div>") |
|
|
| |
| with gr.Group(): |
| gr.Markdown("#### Divergence Point") |
| world_input = gr.Textbox( |
| label="Describe a historical or future decision that would reshape civilization.", |
| placeholder="e.g., 'What if the Renaissance never happened?' or 'What if AI was never invented?'", |
| lines=6 |
| ) |
| world_submit = gr.Button("Generate World", variant="primary", size="lg") |
|
|
| |
| with gr.Group(): |
| gr.Markdown("#### The Alternate World") |
| world_output = gr.HTML("") |
|
|
|
|
| |
| |
| |
|
|
| def handle_submit_story(story: str): |
| """Process personal story submission. |
| |
| Returns: (mirror_md, story_state, analysis_state) |
| """ |
| if not story or len(story.strip()) < config.MIN_STORY_LENGTH: |
| return "Please share at least 100 characters.", "", None |
|
|
| if len(story) > config.MAX_STORY_LENGTH: |
| return "Please keep under 5000 characters.", "", None |
|
|
| |
| analysis = analyze_story(story) |
|
|
| if analysis.get("risk_level") == "high": |
| safety_msg = ( |
| "### ⚠ Safety Notice\n\n" |
| "This memory touches on deep pain. Elsewhere is not designed for trauma or crisis reflection.\n\n" |
| "**If you're in danger, reach out:**\n" |
| "- US: 988 (Suicide & Crisis Lifeline)\n" |
| "- Or contact local emergency services." |
| ) |
| return safety_msg, "", None |
|
|
| |
| mirror = generate_mirror(story, analysis) |
|
|
| return mirror, story, analysis |
|
|
|
|
| def handle_choose_path(direction: str, story: str, analysis: Dict): |
| """Generate timeline for chosen path.""" |
| if not story or len(story.strip()) < config.MIN_STORY_LENGTH: |
| msg = "<div class='rd-card'>Please share your moment above and click <strong>Reflect</strong> first.</div>" |
| return gr.update(value=msg, visible=True), gr.update(visible=False) |
|
|
| timeline = generate_timeline(story, analysis or FALLBACK_ANALYSIS, direction) |
| image_uri = scene_data_uri(timeline_scene_prompt(timeline, direction)) |
| html_out = render_timeline_html(timeline, direction, image_uri) |
| return gr.update(value=html_out, visible=True), gr.update(visible=True) |
|
|
|
|
| def handle_world_divergence(scenario: str): |
| """Generate alternate world, rendered as structured cards.""" |
| if not scenario or len(scenario.strip()) < 20: |
| return "<div class='rd-card'>Describe a divergence point in at least 20 characters.</div>" |
|
|
| world = generate_world_divergence(scenario) |
| image_uri = scene_data_uri(world_scene_prompt(world)) |
| return render_world_html(world, image_uri) |
|
|
|
|
| |
| with demo: |
| submit_btn.click( |
| fn=handle_submit_story, |
| inputs=[story_input], |
| outputs=[mirror_output, story_state, analysis_state] |
| ) |
|
|
| upward_btn.click( |
| fn=lambda story, analysis: handle_choose_path("Upward", story, analysis), |
| inputs=[story_state, analysis_state], |
| outputs=[timeline_result, return_button] |
| ) |
|
|
| downward_btn.click( |
| fn=lambda story, analysis: handle_choose_path("Downward", story, analysis), |
| inputs=[story_state, analysis_state], |
| outputs=[timeline_result, return_button] |
| ) |
|
|
| return_button.click( |
| fn=lambda: (gr.update(visible=False), gr.update(visible=False)), |
| outputs=[timeline_result, return_button] |
| ) |
|
|
| world_submit.click( |
| fn=handle_world_divergence, |
| inputs=[world_input], |
| outputs=[world_output] |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch( |
| server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), |
| server_port=int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860"))), |
| ) |