""" echo/app.py ----------- The Gradio front-end: plant a seed, watch the tree of alternate lives grow, and walk it. UI-only — it drives the orchestrator and renders LifeTree.to_graph(). First cut runs entirely on MockLLM + mock tools, so it launches offline with no GPU and no ML deps. Swapping in the real model later is a one-line change in `_new_orchestrator()`. Run it: python -m echo.app Design notes: * The tree visual is a read-only vis-network embedded via ' ) _EMPTY_TREE = ( "
Plant the tree to begin.
" ) # ------------------------------------------------------------------ rendering def _render(orch: Orchestrator, active_id: str): """Compute every panel + tree output for a given active node (7 outputs).""" tree = orch.tree node = tree.nodes[active_id] html = _render_tree_html(orch.graph(), active_id) # dropdown: depth · occupation · dominant_feeling (read from node, not graph) choices = [ (f"{n.depth} · {n.facts.occupation} · {n.tone.dominant_feeling}", n.node_id) for n in sorted(tree.nodes.values(), key=lambda x: (x.depth, x.node_id)) ] summary = f"### {node.facts.constraints_text()}\n\n{node.summary}" if node.voice_line: summary += f"\n\n> *“{node.voice_line}”*" forks = node.pending_forks fa = (gr.update(value=forks[0], visible=True) if len(forks) > 0 else gr.update(visible=False)) fb = (gr.update(value=forks[1], visible=True) if len(forks) > 1 else gr.update(visible=False)) path = node.voice_audio_path audio_val = path if (path and path.lower().endswith(_AUDIO_EXTS)) else None branch = "```\n" + tree.branch_narrative(active_id) + "\n```" return ( gr.update(value=html), gr.update(choices=choices, value=active_id), gr.update(value=summary), gr.update(value=audio_val), fa, fb, gr.update(value=branch), ) _BLANK = (gr.update(),) * 7 # -------------------------------------------------------------------- handlers def plant(seed: str, base_age): orch = _new_orchestrator() root = orch.seed((seed or "").strip() or _DEFAULT_SEED, base_age=int(base_age or 24)) return (orch, root.node_id, *_render(orch, root.node_id)) def select_node(orch, selected_id): if orch is None or not selected_id: return (selected_id, *_BLANK) return (selected_id, *_render(orch, selected_id)) def choose(orch, active_id, i: int): if orch is None or not active_id: return (orch, active_id, *_BLANK) node = orch.tree.nodes.get(active_id) if node is None or i >= len(node.pending_forks): return (orch, active_id, *_BLANK) child = orch.choose_fork(active_id, i) return (orch, child.node_id, *_render(orch, child.node_id)) def show_final(orch): if orch is None: return gr.update(value="*Plant the tree first.*") return gr.update(value=orch.final_map_summary()) # ------------------------------------------------------------------ blocks/app def build_demo() -> gr.Blocks: with gr.Blocks(title="The Echo") as demo: gr.Markdown("# The Echo\n*the lives you didn't live*") orch_state = gr.State() active_state = gr.State() with gr.Row(): with gr.Column(scale=3): tree_html = gr.HTML(_EMPTY_TREE) with gr.Column(scale=2): seed_in = gr.Textbox(label="The fork in your life", value=_DEFAULT_SEED, lines=2) age_in = gr.Number(label="Starting age", value=24, precision=0) plant_btn = gr.Button("Plant the tree", variant="primary") node_dd = gr.Dropdown(label="You are here", choices=[], interactive=True) summary_md = gr.Markdown() audio = gr.Audio(label="This echo's voice", interactive=False) with gr.Row(): fork_a_btn = gr.Button("…", visible=False) fork_b_btn = gr.Button("…", visible=False) branch_md = gr.Markdown() with gr.Accordion("The final map", open=False): final_btn = gr.Button("See the final map") final_md = gr.Markdown() common = [tree_html, node_dd, summary_md, audio, fork_a_btn, fork_b_btn, branch_md] plant_btn.click(plant, [seed_in, age_in], [orch_state, active_state, *common]) node_dd.change(select_node, [orch_state, node_dd], [active_state, *common]) fork_a_btn.click(lambda o, a: choose(o, a, 0), [orch_state, active_state], [orch_state, active_state, *common]) fork_b_btn.click(lambda o, a: choose(o, a, 1), [orch_state, active_state], [orch_state, active_state, *common]) final_btn.click(show_final, [orch_state], [final_md]) return demo def main() -> None: build_demo().launch(theme=gr.themes.Base()) if __name__ == "__main__": main()