Spaces:
Sleeping
Sleeping
Commit ·
ffb4f53
1
Parent(s): f8c1b4a
Replace gibberish image artifacts with legible HTML timeline/world cards
Browse filesDiffusion models can't render readable text, so the newspaper/document
artifacts came out as garbled noise. Both tabs now render the structured
JSON the model already returns as clean HTML cards (ripples, branches,
strengths, reflection question). Drops image generation from the UI;
image_client.py retained but unused.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README.md
CHANGED
|
@@ -29,8 +29,9 @@ One shared reasoning engine, two lenses on the lives and worlds we didn't live.
|
|
| 29 |
- **Reasoning:** [`Qwen/Qwen2.5-7B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct)
|
| 30 |
— open weights, **well under the 32B cap**, served via the HF Inference API
|
| 31 |
(`chat_completion`). Configurable through the `MODEL_NAME_TEXT` env var.
|
| 32 |
-
- **
|
| 33 |
-
|
|
|
|
| 34 |
- **No GPU required to run** — all inference goes through the Inference API, so the
|
| 35 |
Space stays light and within Zero GPU limits.
|
| 36 |
|
|
|
|
| 29 |
- **Reasoning:** [`Qwen/Qwen2.5-7B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct)
|
| 30 |
— open weights, **well under the 32B cap**, served via the HF Inference API
|
| 31 |
(`chat_completion`). Configurable through the `MODEL_NAME_TEXT` env var.
|
| 32 |
+
- **Rendering:** the model returns structured JSON, which the app renders as clean,
|
| 33 |
+
legible HTML timeline/world cards (no generated-image artifacts — diffusion
|
| 34 |
+
models can't render readable text, so we render the structure directly).
|
| 35 |
- **No GPU required to run** — all inference goes through the Inference API, so the
|
| 36 |
Space stays light and within Zero GPU limits.
|
| 37 |
|
STATUS.md
CHANGED
|
@@ -22,6 +22,10 @@ _Hugging Face "Build Small" hackathon submission. Last updated: 2026-06-14._
|
|
| 22 |
2. **Gradio 5 (pinned `>=5,<6`).** The original Gradio 4.44 pin broke against the
|
| 23 |
current dependency stack; Gradio 5.50 is verified working. README `sdk_version`
|
| 24 |
pinned to `5.50.0` to match.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
## Architecture
|
| 27 |
|
|
|
|
| 22 |
2. **Gradio 5 (pinned `>=5,<6`).** The original Gradio 4.44 pin broke against the
|
| 23 |
current dependency stack; Gradio 5.50 is verified working. README `sdk_version`
|
| 24 |
pinned to `5.50.0` to match.
|
| 25 |
+
3. **Dropped generated-image artifacts; render HTML cards instead.** Diffusion
|
| 26 |
+
models render gibberish "text" inside newspaper/document artifacts, which looked
|
| 27 |
+
broken. Both tabs now render the structured JSON the model already returns as
|
| 28 |
+
legible HTML timeline/world cards. `image_client.py` is retained but unused.
|
| 29 |
|
| 30 |
## Architecture
|
| 31 |
|
app.py
CHANGED
|
@@ -1,23 +1,20 @@
|
|
| 1 |
"""
|
| 2 |
Reality Divergence + Elsewhere
|
| 3 |
AI-native platform for exploring counterfactual personal and historical timelines.
|
| 4 |
-
Uses Qwen2.5-7B-Instruct
|
| 5 |
-
|
| 6 |
"""
|
| 7 |
|
|
|
|
| 8 |
import json
|
| 9 |
import os
|
| 10 |
-
from typing import
|
| 11 |
-
from datetime import datetime
|
| 12 |
-
from io import BytesIO
|
| 13 |
|
| 14 |
import gradio as gr
|
| 15 |
-
from PIL import Image
|
| 16 |
|
| 17 |
import config
|
| 18 |
import prompts
|
| 19 |
from model_client import infer_text, infer_json
|
| 20 |
-
from image_client import generate_artifact, get_image_client
|
| 21 |
|
| 22 |
|
| 23 |
# ============================================================================
|
|
@@ -210,36 +207,83 @@ def generate_world_divergence(scenario: str) -> Dict:
|
|
| 210 |
|
| 211 |
|
| 212 |
# ============================================================================
|
| 213 |
-
#
|
| 214 |
# ============================================================================
|
| 215 |
|
| 216 |
-
def
|
| 217 |
-
"""
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
|
| 245 |
# ============================================================================
|
|
@@ -378,6 +422,61 @@ body, .gradio-container {
|
|
| 378 |
grid-template-columns: 1fr;
|
| 379 |
}
|
| 380 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
"""
|
| 382 |
|
| 383 |
|
|
@@ -412,29 +511,19 @@ with gr.Blocks(css=CSS, title="Reality Divergence") as demo:
|
|
| 412 |
)
|
| 413 |
submit_btn = gr.Button("Reflect", variant="primary", size="lg")
|
| 414 |
|
| 415 |
-
# Output
|
| 416 |
with gr.Group():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
with gr.Row():
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
# Path selection
|
| 424 |
-
gr.Markdown("#### Choose a Path")
|
| 425 |
-
with gr.Row():
|
| 426 |
-
upward_btn = gr.Button("↑ What if it went better?", variant="primary")
|
| 427 |
-
downward_btn = gr.Button("↓ What if it went worse?")
|
| 428 |
-
|
| 429 |
-
# Right: potential artifacts
|
| 430 |
-
with gr.Column(scale=1):
|
| 431 |
-
gr.Markdown("#### Artifacts from Alternate Lives")
|
| 432 |
-
artifact_1 = gr.Image(label="Artifact 1", type="pil", scale=1)
|
| 433 |
-
artifact_2 = gr.Image(label="Artifact 2", type="pil", scale=1)
|
| 434 |
-
artifact_3 = gr.Image(label="Artifact 3", type="pil", scale=1)
|
| 435 |
-
|
| 436 |
-
# Full timeline result (hidden until path chosen)
|
| 437 |
-
timeline_result = gr.Markdown(visible=False, elem_classes=["timeline-card"])
|
| 438 |
return_button = gr.Button("↩ Return to Your Actual Life", visible=False, variant="primary")
|
| 439 |
|
| 440 |
# ========== REALITY DIVERGENCE TAB ==========
|
|
@@ -454,26 +543,10 @@ with gr.Blocks(css=CSS, title="Reality Divergence") as demo:
|
|
| 454 |
)
|
| 455 |
world_submit = gr.Button("Generate World", variant="primary", size="lg")
|
| 456 |
|
| 457 |
-
# Output:
|
| 458 |
with gr.Group():
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
with gr.Column(scale=1):
|
| 462 |
-
gr.Markdown("#### The Alternate World")
|
| 463 |
-
world_title = gr.Markdown("", elem_classes=["world-card"])
|
| 464 |
-
world_divergence = gr.Markdown("", elem_classes=["world-card"])
|
| 465 |
-
world_ripples = gr.Markdown("", elem_classes=["world-card"])
|
| 466 |
-
world_outcome = gr.Markdown("", elem_classes=["world-card"])
|
| 467 |
-
|
| 468 |
-
# Right: world artifacts (museum)
|
| 469 |
-
with gr.Column(scale=1):
|
| 470 |
-
gr.Markdown("#### Museum Artifacts")
|
| 471 |
-
with gr.Row():
|
| 472 |
-
world_artifact_1 = gr.Image(label="Economic Impact", type="pil", scale=1)
|
| 473 |
-
world_artifact_2 = gr.Image(label="Cultural Shift", type="pil", scale=1)
|
| 474 |
-
with gr.Row():
|
| 475 |
-
world_artifact_3 = gr.Image(label="Technology", type="pil", scale=1)
|
| 476 |
-
world_artifact_4 = gr.Image(label="Daily Life", type="pil", scale=1)
|
| 477 |
|
| 478 |
|
| 479 |
# ============================================================================
|
|
@@ -483,13 +556,13 @@ with gr.Blocks(css=CSS, title="Reality Divergence") as demo:
|
|
| 483 |
def handle_submit_story(story: str):
|
| 484 |
"""Process personal story submission.
|
| 485 |
|
| 486 |
-
Returns: (mirror_md,
|
| 487 |
"""
|
| 488 |
if not story or len(story.strip()) < config.MIN_STORY_LENGTH:
|
| 489 |
-
return "Please share at least 100 characters.",
|
| 490 |
|
| 491 |
if len(story) > config.MAX_STORY_LENGTH:
|
| 492 |
-
return "Please keep under 5000 characters.",
|
| 493 |
|
| 494 |
# Analyze
|
| 495 |
analysis = analyze_story(story)
|
|
@@ -502,100 +575,31 @@ def handle_submit_story(story: str):
|
|
| 502 |
"- US: 988 (Suicide & Crisis Lifeline)\n"
|
| 503 |
"- Or contact local emergency services."
|
| 504 |
)
|
| 505 |
-
return safety_msg,
|
| 506 |
|
| 507 |
# Mirror
|
| 508 |
mirror = generate_mirror(story, analysis)
|
| 509 |
|
| 510 |
-
|
| 511 |
-
try:
|
| 512 |
-
artifacts = generate_artifacts({"title": "Your Alternate Life", "divergence": story})
|
| 513 |
-
artifact_imgs = [artifacts[i] if i < len(artifacts) else None for i in range(3)]
|
| 514 |
-
except Exception as e:
|
| 515 |
-
print(f"[warn]Artifact generation failed: {e}")
|
| 516 |
-
artifact_imgs = [None, None, None]
|
| 517 |
-
|
| 518 |
-
return (
|
| 519 |
-
mirror,
|
| 520 |
-
artifact_imgs[0],
|
| 521 |
-
artifact_imgs[1],
|
| 522 |
-
artifact_imgs[2],
|
| 523 |
-
story,
|
| 524 |
-
analysis,
|
| 525 |
-
)
|
| 526 |
|
| 527 |
|
| 528 |
def handle_choose_path(direction: str, story: str, analysis: Dict):
|
| 529 |
"""Generate timeline for chosen path."""
|
| 530 |
if not story or len(story.strip()) < config.MIN_STORY_LENGTH:
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
visible=True,
|
| 534 |
-
), gr.update(visible=False)
|
| 535 |
|
| 536 |
timeline = generate_timeline(story, analysis or FALLBACK_ANALYSIS, direction)
|
| 537 |
-
|
| 538 |
-
result_md = f"""
|
| 539 |
-
### {direction.upper()} PATH: {timeline.get('title', 'Alternate Timeline')}
|
| 540 |
-
|
| 541 |
-
**Divergence Point:** {timeline.get('divergence', '')}
|
| 542 |
-
|
| 543 |
-
**Ripples:**
|
| 544 |
-
"""
|
| 545 |
-
for ripple in timeline.get('ripples', []):
|
| 546 |
-
result_md += f"\n- {ripple}"
|
| 547 |
-
|
| 548 |
-
result_md += f"\n\n**Who You Might Have Become:** {timeline.get('the_you', '')}"
|
| 549 |
-
|
| 550 |
-
result_md += f"\n\n**The Return**\n\n{timeline.get('return', {}).get('opening', '')}"
|
| 551 |
-
result_md += f"\n\n**Strengths in Your Actual Timeline:**\n"
|
| 552 |
-
for strength in timeline.get('return', {}).get('strengths', []):
|
| 553 |
-
result_md += f"\n- {strength}"
|
| 554 |
-
|
| 555 |
-
result_md += f"\n\n**Lesson:** {timeline.get('return', {}).get('lesson', '')}"
|
| 556 |
-
result_md += f"\n\n**Reflection Question:** {timeline.get('return', {}).get('reflection_question', '')}"
|
| 557 |
-
result_md += f"\n\n> *\"{timeline.get('quote', '')}\"*"
|
| 558 |
-
|
| 559 |
-
return gr.update(value=result_md, visible=True), gr.update(visible=True)
|
| 560 |
|
| 561 |
|
| 562 |
def handle_world_divergence(scenario: str):
|
| 563 |
-
"""Generate alternate world."""
|
| 564 |
if not scenario or len(scenario.strip()) < 20:
|
| 565 |
-
return "
|
| 566 |
-
|
| 567 |
world = generate_world_divergence(scenario)
|
| 568 |
-
|
| 569 |
-
title_md = f"## {world.get('title', 'Alternate World')}"
|
| 570 |
-
divergence_md = f"**Year {world.get('year_divergence', 'N/A')}:** {world.get('divergence_point', '')}"
|
| 571 |
-
|
| 572 |
-
ripples_md = "**Ripples:**\n"
|
| 573 |
-
for ripple in world.get('ripples', []):
|
| 574 |
-
ripples_md += f"\n- {ripple}"
|
| 575 |
-
|
| 576 |
-
outcome_md = f"**World Outcome:** {world.get('world_outcome', '')}\n\n"
|
| 577 |
-
outcome_md += f"*Economy:* {world.get('economy', '')}\n\n"
|
| 578 |
-
outcome_md += f"*Culture:* {world.get('culture', '')}\n\n"
|
| 579 |
-
outcome_md += f"*Technology:* {world.get('technology', '')}"
|
| 580 |
-
|
| 581 |
-
# Artifacts
|
| 582 |
-
try:
|
| 583 |
-
artifacts = generate_artifacts(world)
|
| 584 |
-
artifact_imgs = [artifacts[i] if i < len(artifacts) else None for i in range(4)]
|
| 585 |
-
except Exception as e:
|
| 586 |
-
print(f"[warn]Artifact generation failed: {e}")
|
| 587 |
-
artifact_imgs = [None, None, None, None]
|
| 588 |
-
|
| 589 |
-
return (
|
| 590 |
-
title_md,
|
| 591 |
-
divergence_md,
|
| 592 |
-
ripples_md,
|
| 593 |
-
outcome_md,
|
| 594 |
-
artifact_imgs[0] if len(artifact_imgs) > 0 else None,
|
| 595 |
-
artifact_imgs[1] if len(artifact_imgs) > 1 else None,
|
| 596 |
-
artifact_imgs[2] if len(artifact_imgs) > 2 else None,
|
| 597 |
-
artifact_imgs[3] if len(artifact_imgs) > 3 else None,
|
| 598 |
-
)
|
| 599 |
|
| 600 |
|
| 601 |
# Wire events (re-enter the Blocks context so handlers attach correctly)
|
|
@@ -603,7 +607,7 @@ with demo:
|
|
| 603 |
submit_btn.click(
|
| 604 |
fn=handle_submit_story,
|
| 605 |
inputs=[story_input],
|
| 606 |
-
outputs=[mirror_output,
|
| 607 |
)
|
| 608 |
|
| 609 |
upward_btn.click(
|
|
@@ -626,7 +630,7 @@ with demo:
|
|
| 626 |
world_submit.click(
|
| 627 |
fn=handle_world_divergence,
|
| 628 |
inputs=[world_input],
|
| 629 |
-
outputs=[
|
| 630 |
)
|
| 631 |
|
| 632 |
|
|
|
|
| 1 |
"""
|
| 2 |
Reality Divergence + Elsewhere
|
| 3 |
AI-native platform for exploring counterfactual personal and historical timelines.
|
| 4 |
+
Uses Qwen2.5-7B-Instruct (via the Hugging Face Inference API) for reasoning, and
|
| 5 |
+
renders results as structured, legible HTML cards.
|
| 6 |
"""
|
| 7 |
|
| 8 |
+
import html
|
| 9 |
import json
|
| 10 |
import os
|
| 11 |
+
from typing import Optional, Dict
|
|
|
|
|
|
|
| 12 |
|
| 13 |
import gradio as gr
|
|
|
|
| 14 |
|
| 15 |
import config
|
| 16 |
import prompts
|
| 17 |
from model_client import infer_text, infer_json
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
# ============================================================================
|
|
|
|
| 207 |
|
| 208 |
|
| 209 |
# ============================================================================
|
| 210 |
+
# HTML Card Rendering
|
| 211 |
# ============================================================================
|
| 212 |
|
| 213 |
+
def _list_items(items) -> str:
|
| 214 |
+
"""Render a list of strings as escaped <li> elements."""
|
| 215 |
+
return "".join(f"<li>{html.escape(str(i))}</li>" for i in items if str(i).strip())
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def render_timeline_html(timeline: Dict, direction: str) -> str:
|
| 219 |
+
"""Render a personal counterfactual timeline as structured cards."""
|
| 220 |
+
is_up = direction.lower() == "upward"
|
| 221 |
+
arrow = "↑" if is_up else "↓"
|
| 222 |
+
label = "The path where it went better" if is_up else "The path where it went harder"
|
| 223 |
+
ret = timeline.get("return", {}) or {}
|
| 224 |
+
return f"""
|
| 225 |
+
<div class="rd-stack">
|
| 226 |
+
<div class="rd-card rd-fork">
|
| 227 |
+
<div class="rd-eyebrow">{arrow} {label}</div>
|
| 228 |
+
<h2 class="rd-title">{html.escape(timeline.get('title', 'Alternate Timeline'))}</h2>
|
| 229 |
+
<p class="rd-lead">{html.escape(timeline.get('divergence', ''))}</p>
|
| 230 |
+
</div>
|
| 231 |
+
<div class="rd-card">
|
| 232 |
+
<div class="rd-eyebrow">Ripples through the years</div>
|
| 233 |
+
<ul class="rd-list">{_list_items(timeline.get('ripples', []))}</ul>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="rd-card">
|
| 236 |
+
<div class="rd-eyebrow">Who you might have become</div>
|
| 237 |
+
<p>{html.escape(timeline.get('the_you', ''))}</p>
|
| 238 |
+
</div>
|
| 239 |
+
<div class="rd-card rd-return">
|
| 240 |
+
<div class="rd-eyebrow">↩ Returning to your actual life</div>
|
| 241 |
+
<p>{html.escape(ret.get('opening', ''))}</p>
|
| 242 |
+
<div class="rd-sublabel">Strengths you actually built</div>
|
| 243 |
+
<ul class="rd-list">{_list_items(ret.get('strengths', []))}</ul>
|
| 244 |
+
<p class="rd-lesson"><strong>Lesson:</strong> {html.escape(ret.get('lesson', ''))}</p>
|
| 245 |
+
</div>
|
| 246 |
+
<div class="rd-card rd-question">
|
| 247 |
+
<div class="rd-eyebrow">A question to sit with</div>
|
| 248 |
+
<p class="rd-q">{html.escape(ret.get('reflection_question', ''))}</p>
|
| 249 |
+
</div>
|
| 250 |
+
<blockquote class="rd-quote">“{html.escape(timeline.get('quote', ''))}”</blockquote>
|
| 251 |
+
</div>
|
| 252 |
+
"""
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def render_world_html(world: Dict) -> str:
|
| 256 |
+
"""Render an alternate-world divergence as structured cards."""
|
| 257 |
+
return f"""
|
| 258 |
+
<div class="rd-stack">
|
| 259 |
+
<div class="rd-card rd-fork">
|
| 260 |
+
<div class="rd-eyebrow">Year {html.escape(str(world.get('year_divergence', '')))} · Point of divergence</div>
|
| 261 |
+
<h2 class="rd-title">{html.escape(world.get('title', 'Alternate World'))}</h2>
|
| 262 |
+
<p class="rd-lead">{html.escape(world.get('divergence_point', ''))}</p>
|
| 263 |
+
</div>
|
| 264 |
+
<div class="rd-card">
|
| 265 |
+
<div class="rd-eyebrow">Ripples</div>
|
| 266 |
+
<ul class="rd-list">{_list_items(world.get('ripples', []))}</ul>
|
| 267 |
+
</div>
|
| 268 |
+
<div class="rd-card">
|
| 269 |
+
<div class="rd-eyebrow">How this world evolved</div>
|
| 270 |
+
<p>{html.escape(world.get('world_outcome', ''))}</p>
|
| 271 |
+
</div>
|
| 272 |
+
<div class="rd-grid">
|
| 273 |
+
<div class="rd-card"><div class="rd-sublabel">Economy</div><p>{html.escape(world.get('economy', ''))}</p></div>
|
| 274 |
+
<div class="rd-card"><div class="rd-sublabel">Culture</div><p>{html.escape(world.get('culture', ''))}</p></div>
|
| 275 |
+
<div class="rd-card"><div class="rd-sublabel">Technology</div><p>{html.escape(world.get('technology', ''))}</p></div>
|
| 276 |
+
</div>
|
| 277 |
+
<div class="rd-grid-2">
|
| 278 |
+
<div class="rd-card rd-winners"><div class="rd-sublabel">Who thrived</div><ul class="rd-list">{_list_items(world.get('winners', []))}</ul></div>
|
| 279 |
+
<div class="rd-card rd-losers"><div class="rd-sublabel">Who struggled</div><ul class="rd-list">{_list_items(world.get('losers', []))}</ul></div>
|
| 280 |
+
</div>
|
| 281 |
+
<div class="rd-card rd-question">
|
| 282 |
+
<div class="rd-eyebrow">What it teaches us</div>
|
| 283 |
+
<p class="rd-q">{html.escape(world.get('reflection', ''))}</p>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
"""
|
| 287 |
|
| 288 |
|
| 289 |
# ============================================================================
|
|
|
|
| 422 |
grid-template-columns: 1fr;
|
| 423 |
}
|
| 424 |
}
|
| 425 |
+
|
| 426 |
+
/* Structured result cards (timeline / world) */
|
| 427 |
+
.rd-stack { display: flex; flex-direction: column; gap: 1rem; margin-top: 0.5rem; }
|
| 428 |
+
|
| 429 |
+
.rd-card {
|
| 430 |
+
background: linear-gradient(180deg, rgba(17, 24, 39, 0.6), rgba(11, 16, 32, 0.35));
|
| 431 |
+
border: 1px solid var(--border);
|
| 432 |
+
border-radius: 16px;
|
| 433 |
+
padding: 20px 24px;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.rd-fork { border-left: 3px solid var(--accent-blue); }
|
| 437 |
+
.rd-return { border-left: 3px solid var(--accent-purple); }
|
| 438 |
+
.rd-question {
|
| 439 |
+
border: 1px solid var(--accent-purple);
|
| 440 |
+
background: rgba(139, 92, 246, 0.08);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.rd-eyebrow {
|
| 444 |
+
font-size: 0.8rem;
|
| 445 |
+
font-weight: 700;
|
| 446 |
+
letter-spacing: 0.05em;
|
| 447 |
+
text-transform: uppercase;
|
| 448 |
+
color: var(--accent-blue);
|
| 449 |
+
margin-bottom: 0.5rem;
|
| 450 |
+
}
|
| 451 |
+
.rd-sublabel {
|
| 452 |
+
font-size: 0.85rem;
|
| 453 |
+
font-weight: 600;
|
| 454 |
+
color: var(--text-secondary);
|
| 455 |
+
margin: 0.75rem 0 0.35rem;
|
| 456 |
+
}
|
| 457 |
+
.rd-title { font-size: 1.6rem; font-weight: 800; margin: 0.2rem 0 0.5rem; color: var(--text-primary); }
|
| 458 |
+
.rd-lead { color: var(--text-secondary); font-size: 1.05rem; }
|
| 459 |
+
.rd-card p { color: var(--text-primary); line-height: 1.6; margin: 0.25rem 0; }
|
| 460 |
+
.rd-list { margin: 0.25rem 0 0; padding-left: 1.2rem; }
|
| 461 |
+
.rd-list li { color: var(--text-primary); line-height: 1.6; margin-bottom: 0.35rem; }
|
| 462 |
+
.rd-lesson { margin-top: 0.75rem; color: var(--text-secondary); }
|
| 463 |
+
.rd-q { font-size: 1.1rem; font-style: italic; color: var(--text-primary); }
|
| 464 |
+
.rd-quote {
|
| 465 |
+
border-left: 3px solid var(--accent-blue);
|
| 466 |
+
margin: 0.25rem 0 0;
|
| 467 |
+
padding: 0.5rem 0 0.5rem 1rem;
|
| 468 |
+
font-size: 1.2rem;
|
| 469 |
+
font-style: italic;
|
| 470 |
+
color: var(--text-primary);
|
| 471 |
+
}
|
| 472 |
+
.rd-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
| 473 |
+
.rd-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
| 474 |
+
.rd-winners { border-left: 3px solid #34D399; }
|
| 475 |
+
.rd-losers { border-left: 3px solid #F87171; }
|
| 476 |
+
|
| 477 |
+
@media (max-width: 1024px) {
|
| 478 |
+
.rd-grid, .rd-grid-2 { grid-template-columns: 1fr; }
|
| 479 |
+
}
|
| 480 |
"""
|
| 481 |
|
| 482 |
|
|
|
|
| 511 |
)
|
| 512 |
submit_btn = gr.Button("Reflect", variant="primary", size="lg")
|
| 513 |
|
| 514 |
+
# Output
|
| 515 |
with gr.Group():
|
| 516 |
+
gr.Markdown("#### Your Reflection")
|
| 517 |
+
mirror_output = gr.Markdown(FALLBACK_MIRROR, elem_classes=["timeline-card"])
|
| 518 |
+
|
| 519 |
+
# Path selection
|
| 520 |
+
gr.Markdown("#### Choose a Path")
|
| 521 |
with gr.Row():
|
| 522 |
+
upward_btn = gr.Button("↑ What if it went better?", variant="primary")
|
| 523 |
+
downward_btn = gr.Button("↓ What if it went worse?")
|
| 524 |
+
|
| 525 |
+
# Full timeline result, rendered as cards (hidden until a path is chosen)
|
| 526 |
+
timeline_result = gr.HTML(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
return_button = gr.Button("↩ Return to Your Actual Life", visible=False, variant="primary")
|
| 528 |
|
| 529 |
# ========== REALITY DIVERGENCE TAB ==========
|
|
|
|
| 543 |
)
|
| 544 |
world_submit = gr.Button("Generate World", variant="primary", size="lg")
|
| 545 |
|
| 546 |
+
# Output: structured world cards
|
| 547 |
with gr.Group():
|
| 548 |
+
gr.Markdown("#### The Alternate World")
|
| 549 |
+
world_output = gr.HTML("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
|
| 551 |
|
| 552 |
# ============================================================================
|
|
|
|
| 556 |
def handle_submit_story(story: str):
|
| 557 |
"""Process personal story submission.
|
| 558 |
|
| 559 |
+
Returns: (mirror_md, story_state, analysis_state)
|
| 560 |
"""
|
| 561 |
if not story or len(story.strip()) < config.MIN_STORY_LENGTH:
|
| 562 |
+
return "Please share at least 100 characters.", "", None
|
| 563 |
|
| 564 |
if len(story) > config.MAX_STORY_LENGTH:
|
| 565 |
+
return "Please keep under 5000 characters.", "", None
|
| 566 |
|
| 567 |
# Analyze
|
| 568 |
analysis = analyze_story(story)
|
|
|
|
| 575 |
"- US: 988 (Suicide & Crisis Lifeline)\n"
|
| 576 |
"- Or contact local emergency services."
|
| 577 |
)
|
| 578 |
+
return safety_msg, "", None
|
| 579 |
|
| 580 |
# Mirror
|
| 581 |
mirror = generate_mirror(story, analysis)
|
| 582 |
|
| 583 |
+
return mirror, story, analysis
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
|
| 585 |
|
| 586 |
def handle_choose_path(direction: str, story: str, analysis: Dict):
|
| 587 |
"""Generate timeline for chosen path."""
|
| 588 |
if not story or len(story.strip()) < config.MIN_STORY_LENGTH:
|
| 589 |
+
msg = "<div class='rd-card'>Please share your moment above and click <strong>Reflect</strong> first.</div>"
|
| 590 |
+
return gr.update(value=msg, visible=True), gr.update(visible=False)
|
|
|
|
|
|
|
| 591 |
|
| 592 |
timeline = generate_timeline(story, analysis or FALLBACK_ANALYSIS, direction)
|
| 593 |
+
return gr.update(value=render_timeline_html(timeline, direction), visible=True), gr.update(visible=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
|
| 595 |
|
| 596 |
def handle_world_divergence(scenario: str):
|
| 597 |
+
"""Generate alternate world, rendered as structured cards."""
|
| 598 |
if not scenario or len(scenario.strip()) < 20:
|
| 599 |
+
return "<div class='rd-card'>Describe a divergence point in at least 20 characters.</div>"
|
| 600 |
+
|
| 601 |
world = generate_world_divergence(scenario)
|
| 602 |
+
return render_world_html(world)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
|
| 604 |
|
| 605 |
# Wire events (re-enter the Blocks context so handlers attach correctly)
|
|
|
|
| 607 |
submit_btn.click(
|
| 608 |
fn=handle_submit_story,
|
| 609 |
inputs=[story_input],
|
| 610 |
+
outputs=[mirror_output, story_state, analysis_state]
|
| 611 |
)
|
| 612 |
|
| 613 |
upward_btn.click(
|
|
|
|
| 630 |
world_submit.click(
|
| 631 |
fn=handle_world_divergence,
|
| 632 |
inputs=[world_input],
|
| 633 |
+
outputs=[world_output]
|
| 634 |
)
|
| 635 |
|
| 636 |
|