prashantAI Claude Opus 4.8 (1M context) commited on
Commit
ffb4f53
·
1 Parent(s): f8c1b4a

Replace gibberish image artifacts with legible HTML timeline/world cards

Browse files

Diffusion 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>

Files changed (3) hide show
  1. README.md +3 -2
  2. STATUS.md +4 -0
  3. app.py +162 -158
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
- - **Artifacts:** [`black-forest-labs/FLUX.1-schnell`](https://huggingface.co/black-forest-labs/FLUX.1-schnell)
33
- via the HF Inference API (`text_to_image`), with SDXL as fallback.
 
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 for reasoning + FLUX.1-schnell for artifact generation,
5
- all served through the Hugging Face Inference API.
6
  """
7
 
 
8
  import json
9
  import os
10
- from typing import Tuple, Optional, List, Dict, Any
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
- # Artifact Generation
214
  # ============================================================================
215
 
216
- def generate_artifacts(timeline_data: Dict, num_artifacts: int = 3) -> List[Optional[Image.Image]]:
217
- """Generate 3-6 artifact images for timeline."""
218
- artifacts = []
219
-
220
- try:
221
- # Newspaper
222
- headline = timeline_data.get("title", "Alternate Reality")
223
- artifacts.append(generate_artifact("newspaper", headline=headline, subheading=timeline_data.get("divergence", "")))
224
- except Exception as e:
225
- print(f"[warn]Newspaper artifact failed: {e}")
226
- artifacts.append(None)
227
-
228
- try:
229
- # Product/Ad
230
- artifacts.append(generate_artifact("product", product="Alternate World Artifact", context=timeline_data.get("divergence", "")))
231
- except Exception as e:
232
- print(f"[warn]Product artifact failed: {e}")
233
- artifacts.append(None)
234
-
235
- try:
236
- # Document
237
- artifacts.append(generate_artifact("document", doc_type="diary entry", context=timeline_data.get("divergence", "")))
238
- except Exception as e:
239
- print(f"[warn]Document artifact failed: {e}")
240
- artifacts.append(None)
241
-
242
- return [a for a in artifacts if a is not None]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: parallel layout
416
  with gr.Group():
 
 
 
 
 
417
  with gr.Row():
418
- # Left: narrative
419
- with gr.Column(scale=1):
420
- gr.Markdown("#### Your Reflection")
421
- mirror_output = gr.Markdown(FALLBACK_MIRROR, elem_classes=["timeline-card"])
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: parallel layout
458
  with gr.Group():
459
- with gr.Row():
460
- # Left: world narrative
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, artifact_1, artifact_2, artifact_3, story_state, analysis_state)
487
  """
488
  if not story or len(story.strip()) < config.MIN_STORY_LENGTH:
489
- return "Please share at least 100 characters.", None, None, None, "", None
490
 
491
  if len(story) > config.MAX_STORY_LENGTH:
492
- return "Please keep under 5000 characters.", None, None, None, "", None
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, None, None, None, "", None
506
 
507
  # Mirror
508
  mirror = generate_mirror(story, analysis)
509
 
510
- # Artifacts (gracefully handle if image gen fails)
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
- return gr.update(
532
- value="Please share your moment above and click **Reflect** first.",
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 "", "", "", "", None, None, None, None
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, artifact_1, artifact_2, artifact_3, story_state, analysis_state]
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=[world_title, world_divergence, world_ripples, world_outcome, world_artifact_1, world_artifact_2, world_artifact_3, world_artifact_4]
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