Spaces:
Sleeping
Sleeping
| """ | |
| CASCADE Hyperlattice - Interactive 3D Agent Visualization | |
| L40S GPU-POWERED. Graph-first. Click to drill. Auto-evolve. | |
| """ | |
| # MUST BE FIRST - Patch cascade-lattice before any imports | |
| import cascade_patch # noqa: F401 | |
| import streamlit as st | |
| from streamlit_autorefresh import st_autorefresh | |
| import plotly.graph_objects as go | |
| import numpy as np | |
| import time | |
| import colorsys | |
| # Rich TUI for REAL terminal rendering | |
| from rich.console import Console | |
| from rich.table import Table | |
| from rich.panel import Panel | |
| from rich.text import Text | |
| from rich.tree import Tree | |
| from rich.style import Style | |
| from io import StringIO | |
| from lattice import Hyperlattice | |
| from swarm import QuineSwarm | |
| from ipfs_sync import CollectiveMemory, SyncManager | |
| from cascade_bridge import CascadeBridge | |
| from champion_loader import ( | |
| get_champion_info, load_champion_module, | |
| get_champion_diagnostics, get_replicated_agents_info, get_download_status | |
| ) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # GPU DETECTION | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| try: | |
| import torch | |
| TORCH_AVAILABLE = True | |
| GPU_AVAILABLE = torch.cuda.is_available() | |
| GPU_NAME = torch.cuda.get_device_name(0) if GPU_AVAILABLE else None | |
| GPU_MEMORY = torch.cuda.get_device_properties(0).total_memory // (1024**3) if GPU_AVAILABLE else 0 | |
| except ImportError: | |
| TORCH_AVAILABLE = False | |
| GPU_AVAILABLE = False | |
| GPU_NAME = None | |
| GPU_MEMORY = 0 | |
| try: | |
| from cascade import Tracer, CausationGraph, sdk_observe, init as cascade_init | |
| from cascade import store as cascade_store | |
| from cascade import discover_models, discover_datasets | |
| from cascade.hold import Hold, CausationHold, HoldSession | |
| from cascade.analysis import MetricsEngine | |
| from cascade.diagnostics import diagnose, BugDetector | |
| from cascade.forensics import DataForensics, GhostLog | |
| from cascade.viz import PlaybackBuffer, find_latest_tape, load_tape_file | |
| CASCADE_AVAILABLE = True | |
| # Initialize cascade observation layer | |
| cascade_init(project="hyperlattice") | |
| except Exception as e: | |
| print(f"[CASCADE] Import failed: {e}") | |
| CASCADE_AVAILABLE = False | |
| sdk_observe = None | |
| cascade_store = None | |
| Tracer = None | |
| CausationGraph = None | |
| Hold = None | |
| CausationHold = None | |
| HoldSession = None | |
| MetricsEngine = None | |
| discover_models = lambda: [] | |
| discover_datasets = lambda: [] | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # PAGE CONFIG | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| st.set_page_config( | |
| page_title="CASCADE Hyperlattice", | |
| page_icon="🌌", | |
| layout="wide", | |
| initial_sidebar_state="expanded" # Sidebar open by default for controls | |
| ) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # EMERGENCY STOP CHECK - Run FIRST before anything else | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| if st.session_state.get('stop_requested', 0) > 0: | |
| st.session_state.auto_run = False | |
| st.session_state.stop_requested = 0 | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # SESSION STATE | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| if 'initialized' not in st.session_state: | |
| st.session_state.initialized = False | |
| st.session_state.lattice = None | |
| st.session_state.swarm = None | |
| st.session_state.cascade = None | |
| st.session_state.step_count = 0 | |
| st.session_state.events = [] | |
| st.session_state.selected_agent = None | |
| st.session_state.selected_gate = None # Null gate selection | |
| st.session_state.selected_trail = None # Trail/movement selection | |
| st.session_state.selection_type = None # 'agent', 'gate', 'trail' | |
| st.session_state.lattice_size = 8 | |
| st.session_state.num_agents = 10 | |
| st.session_state.max_agents = 15 # Population cap | |
| st.session_state.num_gates = 6 | |
| st.session_state.auto_run = False | |
| st.session_state.stop_requested = 0 # Counter for aggressive stop | |
| st.session_state.experience_chain = [] | |
| st.session_state.camera_angle = 0 | |
| # CHEESE - The goal! | |
| st.session_state.cheese_position = None | |
| st.session_state.cheese_found = False | |
| st.session_state.cheese_finder = None | |
| # HOLD system - pause simulation for graph interaction | |
| st.session_state.hold_active = False | |
| st.session_state.hold_camera = None # Store camera state during hold | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # HOLD SYSTEM - cascade-lattice integration | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| if CASCADE_AVAILABLE and Hold is not None: | |
| hold_system = Hold.get() | |
| else: | |
| hold_system = None | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # SIMULATION | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def check_hold(): | |
| """Check if HOLD is active - if so, pause auto_run.""" | |
| if st.session_state.get('hold_active', False): | |
| st.session_state.auto_run = False | |
| return True | |
| return False | |
| def init_simulation(): | |
| try: | |
| size = st.session_state.lattice_size | |
| agents = st.session_state.num_agents | |
| gates = st.session_state.num_gates | |
| st.session_state.lattice = Hyperlattice(dimensions=(size, size, size), num_null_gates=gates) | |
| st.session_state.swarm = QuineSwarm(st.session_state.lattice, initial_agents=agents) | |
| st.session_state.cascade = CascadeBridge(session_id=f"stream_{int(time.time())}") | |
| st.session_state.step_count = 0 | |
| st.session_state.initialized = True | |
| st.session_state.events = [] | |
| # Place the CHEESE randomly in the lattice! | |
| import random | |
| all_nodes = list(st.session_state.lattice.nodes.keys()) | |
| if all_nodes: | |
| st.session_state.cheese_position = random.choice(all_nodes) | |
| st.session_state.cheese_found = False | |
| st.session_state.cheese_finder = None | |
| # Log genesis to cascade | |
| if CASCADE_AVAILABLE and cascade_store: | |
| try: | |
| receipt = cascade_store.observe( | |
| model_id="hyperlattice", | |
| data={ | |
| "event": "genesis", | |
| "lattice_size": size, | |
| "agents": agents, | |
| "gates": gates, | |
| "session_id": st.session_state.cascade.session_id | |
| } | |
| ) | |
| print(f"[CASCADE] Genesis logged: {receipt.cid}") | |
| except Exception as e: | |
| print(f"[CASCADE] Genesis log failed: {e}") | |
| except Exception as e: | |
| st.error(f"Init failed: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| def step_simulation(): | |
| if not st.session_state.initialized: | |
| init_simulation() | |
| events = st.session_state.swarm.step() | |
| st.session_state.step_count += 1 | |
| # Fork at null gates occasionally | |
| new_agents = st.session_state.swarm.fork_at_null_gates() | |
| # Prune if too many | |
| st.session_state.swarm.prune_weak(keep_top=st.session_state.max_agents) | |
| for event in events: | |
| st.session_state.cascade.log_event(event) | |
| # event is an Experience object, not a dict | |
| event_type = event.action if hasattr(event, 'action') else "move" | |
| agent_id = event.agent_id if hasattr(event, 'agent_id') else str(event)[:20] | |
| event_data = event.to_dict() if hasattr(event, 'to_dict') else {"raw": str(event)} | |
| st.session_state.events.append({ | |
| "step": st.session_state.step_count, | |
| "type": event_type, | |
| "agent": agent_id, | |
| "data": event_data | |
| }) | |
| # Track forks | |
| for agent in new_agents: | |
| st.session_state.events.append({ | |
| "step": st.session_state.step_count, | |
| "type": "fork", | |
| "agent": agent.agent_id, | |
| "data": {"parent": agent.parent_hash, "gen": agent.generation} | |
| }) | |
| # 🧀 CHEESE HUNT! Check if any agent found the cheese | |
| if st.session_state.cheese_position and not st.session_state.cheese_found: | |
| cheese_node = st.session_state.cheese_position # This is a node ID string | |
| # Check all agents - swarm.agents is a Dict[str, QuineAgent] | |
| for agent in st.session_state.swarm.agents.values(): | |
| # agent.position is the current node ID (string) | |
| if agent.position == cheese_node: | |
| # FOUND IT! 🎉 | |
| st.session_state.cheese_found = True | |
| st.session_state.cheese_finder = agent.agent_id | |
| st.session_state.auto_run = False # Stop the simulation! | |
| st.session_state.events.append({ | |
| "step": st.session_state.step_count, | |
| "type": "🧀 CHEESE FOUND!", | |
| "agent": agent.agent_id, | |
| "data": {"position": cheese_node[:12], "generation": agent.generation} | |
| }) | |
| break | |
| # Also check via 3D position proximity (fallback) | |
| if not st.session_state.cheese_found: | |
| try: | |
| cheese_3d = st.session_state.lattice.get_node_position(cheese_node) | |
| for agent in st.session_state.swarm.agents.values(): | |
| agent_3d = st.session_state.lattice.get_node_position(agent.position) | |
| dist = sum((a - b) ** 2 for a, b in zip(agent_3d, cheese_3d)) ** 0.5 | |
| if dist < 0.1: # Very close | |
| st.session_state.cheese_found = True | |
| st.session_state.cheese_finder = agent.agent_id | |
| st.session_state.auto_run = False | |
| st.session_state.events.append({ | |
| "step": st.session_state.step_count, | |
| "type": "🧀 CHEESE FOUND!", | |
| "agent": agent.agent_id, | |
| "data": {"position": cheese_node[:12], "generation": agent.generation} | |
| }) | |
| break | |
| except: | |
| pass | |
| # Keep ALL events - only limit to prevent memory issues in very long sessions | |
| if len(st.session_state.events) > 10000: | |
| st.session_state.events = st.session_state.events[-10000:] | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # RICH TUI RENDERER - Real terminal rendering with SVG export | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def render_rich_tui(events, cascade_stats, lineage_data, cascade_store=None): | |
| """ | |
| Render a REAL Rich TUI and export to SVG for Streamlit embedding. | |
| SVG embeds cleanly unlike HTML which gets corrupted. | |
| """ | |
| console = Console(record=True, force_terminal=True, width=120, color_system="truecolor") | |
| # Header Panel | |
| obs_count = cascade_stats.get('total_observations', 0) | |
| genesis = cascade_stats.get('genesis_root', '????????????????') | |
| header_text = Text() | |
| header_text.append("CASCADE HYPERLATTICE v0.6.2\n", style="bold cyan") | |
| header_text.append(f"Genesis: ", style="white") | |
| header_text.append(f"{genesis}\n", style="bold green") | |
| header_text.append(f"Lattice Observations: ", style="white") | |
| header_text.append(f"{obs_count:,}", style="bold yellow") | |
| console.print(Panel(header_text, title="🌌 HYPERLATTICE", border_style="cyan")) | |
| # Events Table - FULL HISTORY, NO TRUNCATION | |
| events_table = Table( | |
| title="🔥 LIVE EVENT STREAM", | |
| show_header=True, | |
| header_style="bold magenta", | |
| border_style="green", | |
| title_style="bold green" | |
| ) | |
| events_table.add_column("Step", style="cyan", justify="right", width=6) | |
| events_table.add_column("Agent", style="yellow", width=24) | |
| events_table.add_column("Event", style="magenta", width=14) | |
| events_table.add_column("Details", style="white") | |
| # ALL events - no truncation | |
| for evt in reversed(events): | |
| step = str(evt.get('step', '?')) | |
| agent = evt.get('agent', '??????') | |
| evt_type = evt.get('type', 'unknown').upper() | |
| data = evt.get('data', {}) | |
| # Format details based on event type | |
| if evt_type == 'MOVE': | |
| from_n = data.get('from_node', '?') | |
| to_n = data.get('to_node', '?') | |
| details = f"{from_n} → {to_n}" | |
| elif evt_type == 'NULL_TRANSIT': | |
| from_n = data.get('from_node', '?') | |
| to_n = data.get('to_node', '?') | |
| details = f"⚡ {from_n} ⇒ {to_n}" | |
| elif evt_type == 'FORK': | |
| child = data.get('child_id', '?') | |
| details = f"🧬 parent→{child}" | |
| elif evt_type == 'SPAWN': | |
| details = "✨ new agent" | |
| elif evt_type == 'PRUNE': | |
| fit = data.get('fitness', 0) | |
| details = f"💀 fitness={fit:.3f}" | |
| elif evt_type == 'DECISION': | |
| action = data.get('action', '?') | |
| details = f"🎮 {action}" | |
| elif evt_type == 'HOLD_RESOLUTION': | |
| override = data.get('was_override', False) | |
| details = f"{'🛑' if override else '✅'} override={override}" | |
| else: | |
| details = str(data)[:80] if data else "" | |
| events_table.add_row(step, agent, evt_type, details) | |
| console.print(events_table) | |
| # Agent Lineage Tree - FULL, NO TRUNCATION | |
| if lineage_data: | |
| tree = Tree("🧬 [bold cyan]AGENT LINEAGE[/bold cyan]", guide_style="dim") | |
| # Build tree structure | |
| roots = [] | |
| children_map = {} | |
| for aid, d in lineage_data.items(): | |
| parent = d.get('parent_hash') | |
| if not parent or parent == 'GENESIS': | |
| roots.append((aid, d)) | |
| else: | |
| if parent not in children_map: | |
| children_map[parent] = [] | |
| children_map[parent].append((aid, d)) | |
| def add_children(node, agent_id): | |
| for child_id, child_data in children_map.get(agent_id, []): | |
| gen = child_data.get('generation', 0) | |
| fit = child_data.get('fitness', 0) | |
| child_node = node.add(f"[yellow]{child_id}[/yellow] G{gen} F{fit:.2f}") | |
| add_children(child_node, child_id) | |
| for root_id, root_data in roots: | |
| gen = root_data.get('generation', 0) | |
| fit = root_data.get('fitness', 0) | |
| root_node = tree.add(f"[bold green]{root_id}[/bold green] G{gen} F{fit:.2f}") | |
| add_children(root_node, root_id) | |
| console.print(tree) | |
| # Cascade Store Observations - query if available | |
| if cascade_store: | |
| try: | |
| recent_obs = cascade_store.query(limit=50) | |
| if recent_obs: | |
| obs_table = Table( | |
| title="🔗 LATTICE OBSERVATIONS", | |
| show_header=True, | |
| header_style="bold blue", | |
| border_style="blue" | |
| ) | |
| obs_table.add_column("CID", style="cyan", width=18) | |
| obs_table.add_column("Model", style="yellow", width=30) | |
| obs_table.add_column("Timestamp", style="dim") | |
| for obs in recent_obs: | |
| cid = obs.cid if hasattr(obs, 'cid') and obs.cid else "N/A" | |
| model = obs.model_id if hasattr(obs, 'model_id') else "?" | |
| ts = str(obs.timestamp)[:19] if hasattr(obs, 'timestamp') else "?" | |
| obs_table.add_row(cid, model, ts) | |
| console.print(obs_table) | |
| except Exception as e: | |
| console.print(f"[dim red]Cascade query: {e}[/dim red]") | |
| # Export to SVG - embeds cleanly in Streamlit | |
| svg = console.export_svg(title="CASCADE TUI") | |
| return svg | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # CSS - Mobile-first responsive design | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| st.markdown(""" | |
| <style> | |
| /* ═══ ANTI-FLICKER: Prevent fade/flash on Streamlit reruns ═══ */ | |
| .stApp, [data-testid="stAppViewContainer"], [data-testid="stMain"], | |
| .main, .block-container, [data-testid="stVerticalBlock"], | |
| .element-container, [data-testid="stPlotlyChart"], .js-plotly-plot { | |
| animation: none !important; | |
| transition: none !important; | |
| opacity: 1 !important; | |
| visibility: visible !important; | |
| } | |
| /* Kill Streamlit's skeleton/loading states */ | |
| [data-testid="stSkeleton"], .stSpinner, [data-testid="stStatusWidget"] { | |
| display: none !important; | |
| } | |
| /* Prevent any fade animations on the app shell */ | |
| .stApp * { | |
| animation-duration: 0s !important; | |
| transition-duration: 0s !important; | |
| } | |
| /* ═══ BRUTALIST DARK THEME - NO BABY SHIT ═══ */ | |
| .stApp { background: #080808 !important; } | |
| [data-testid="stHeader"] { background: #080808 !important; border-bottom: 1px solid #333; } | |
| [data-testid="stSidebar"] { background: #0a0a0a !important; border-right: 1px solid #333; } | |
| .block-container { padding: 0.5rem !important; max-width: 100% !important; } | |
| /* Buttons - flat, sharp, no bullshit */ | |
| .stButton > button { | |
| background: #1a1a1a !important; | |
| border: 1px solid #444 !important; | |
| color: #ccc !important; | |
| font-weight: 500 !important; | |
| font-size: 0.9rem !important; | |
| padding: 10px 16px !important; | |
| border-radius: 0 !important; | |
| min-height: 40px !important; | |
| width: 100% !important; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .stButton > button:hover { | |
| background: #252525 !important; | |
| border-color: #666 !important; | |
| color: #fff !important; | |
| transform: none !important; | |
| box-shadow: none !important; | |
| } | |
| /* Mobile control bar */ | |
| .mobile-controls { | |
| position: fixed; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: #0a0a0a; | |
| border-top: 1px solid #333; | |
| padding: 8px; | |
| display: flex; | |
| gap: 4px; | |
| justify-content: center; | |
| z-index: 9999; | |
| } | |
| .mobile-btn { | |
| flex: 1; | |
| max-width: 100px; | |
| padding: 10px 6px; | |
| border-radius: 0; | |
| font-size: 0.8rem; | |
| font-weight: 500; | |
| border: 1px solid #444; | |
| background: #1a1a1a; | |
| color: #ccc; | |
| text-align: center; | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| } | |
| .mobile-btn:hover { background: #252525; border-color: #666; } | |
| .mobile-btn.active { background: #331111; border-color: #663333; } | |
| /* Agent scroll */ | |
| .agent-scroll { | |
| display: flex; | |
| overflow-x: auto; | |
| gap: 4px; | |
| padding: 8px 0; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| .agent-scroll::-webkit-scrollbar { height: 2px; } | |
| .agent-scroll::-webkit-scrollbar-track { background: #111; } | |
| .agent-scroll::-webkit-scrollbar-thumb { background: #444; } | |
| .agent-chip { | |
| flex-shrink: 0; | |
| background: #111; | |
| border: 1px solid #333; | |
| border-radius: 0; | |
| padding: 6px 12px; | |
| font-size: 0.75rem; | |
| color: #999; | |
| white-space: nowrap; | |
| cursor: pointer; | |
| font-family: monospace; | |
| } | |
| .agent-chip.selected { | |
| background: #1a1a0a; | |
| border-color: #666622; | |
| color: #cccc88; | |
| } | |
| /* Stats row */ | |
| .stats-row { | |
| display: flex; | |
| overflow-x: auto; | |
| gap: 4px; | |
| padding: 4px 0; | |
| } | |
| .stat-card { | |
| flex-shrink: 0; | |
| background: #111; | |
| border: 1px solid #222; | |
| border-radius: 0; | |
| padding: 6px 10px; | |
| text-align: center; | |
| min-width: 70px; | |
| } | |
| .stat-card .val { font-size: 1.1rem; font-weight: 500; color: #88cc88; font-family: monospace; } | |
| .stat-card .lbl { font-size: 0.6rem; color: #666; text-transform: uppercase; letter-spacing: 1px; } | |
| /* Metrics - flat */ | |
| [data-testid="stMetric"] { | |
| background: #0f0f0f; | |
| border: 1px solid #222; | |
| border-radius: 0; | |
| padding: 6px !important; | |
| } | |
| [data-testid="stMetricValue"] { | |
| color: #88cc88 !important; | |
| font-size: 1rem !important; | |
| font-family: monospace !important; | |
| } | |
| [data-testid="stMetricLabel"] { | |
| font-size: 0.65rem !important; | |
| color: #666 !important; | |
| text-transform: uppercase !important; | |
| letter-spacing: 1px !important; | |
| } | |
| /* Graph container */ | |
| .js-plotly-plot { | |
| touch-action: pan-x pan-y pinch-zoom !important; | |
| } | |
| /* DESKTOP: Force sidebar visible */ | |
| @media (min-width: 768px) { | |
| [data-testid="stSidebar"] { | |
| min-width: 280px !important; | |
| width: 280px !important; | |
| transform: none !important; | |
| visibility: visible !important; | |
| display: block !important; | |
| position: relative !important; | |
| left: 0 !important; | |
| margin-left: 0 !important; | |
| } | |
| [data-testid="stSidebar"] > div { | |
| transform: none !important; | |
| visibility: visible !important; | |
| } | |
| /* Hide collapse button on desktop */ | |
| [data-testid="collapsedControl"] { | |
| display: none !important; | |
| } | |
| } | |
| /* MOBILE: Fully collapsible sidebar */ | |
| @media (max-width: 767px) { | |
| /* When collapsed, hide completely */ | |
| [data-testid="stSidebar"][aria-expanded="false"] { | |
| transform: translateX(-100%) !important; | |
| width: 0 !important; | |
| min-width: 0 !important; | |
| visibility: hidden !important; | |
| } | |
| /* When expanded, full overlay */ | |
| [data-testid="stSidebar"][aria-expanded="true"] { | |
| position: fixed !important; | |
| left: 0 !important; | |
| top: 0 !important; | |
| height: 100vh !important; | |
| width: 85vw !important; | |
| max-width: 320px !important; | |
| z-index: 9999 !important; | |
| background: #0a0a0a !important; | |
| transform: translateX(0) !important; | |
| visibility: visible !important; | |
| box-shadow: 5px 0 20px rgba(0,0,0,0.5) !important; | |
| } | |
| /* ALWAYS show collapse button on mobile */ | |
| [data-testid="collapsedControl"] { | |
| display: flex !important; | |
| position: fixed !important; | |
| left: 10px !important; | |
| top: 10px !important; | |
| z-index: 10000 !important; | |
| background: #1a1a2e !important; | |
| border: 1px solid #8a2be2 !important; | |
| border-radius: 8px !important; | |
| padding: 8px !important; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.5) !important; | |
| } | |
| /* Main content gets full width when sidebar collapsed */ | |
| [data-testid="stSidebar"][aria-expanded="false"] ~ .main { | |
| margin-left: 0 !important; | |
| width: 100% !important; | |
| } | |
| } | |
| /* Make graphs fill available height */ | |
| .js-plotly-plot .plotly { | |
| width: 100% !important; | |
| } | |
| /* TUI Terminal - responsive sizing */ | |
| .tui-terminal { | |
| font-size: clamp(8px, 1.2vw, 11px) !important; | |
| overflow-x: auto; | |
| } | |
| /* Desktop */ | |
| @media (min-width: 1025px) { | |
| .mobile-controls { display: none; } | |
| } | |
| /* Selected agent panel */ | |
| .agent-panel { | |
| background: #0f0f0f; | |
| border: 1px solid #333; | |
| border-radius: 0; | |
| padding: 12px; | |
| margin-top: 8px; | |
| } | |
| .agent-panel h3 { | |
| color: #999; | |
| margin: 0 0 8px 0; | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .agent-panel .row { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 4px 0; | |
| border-bottom: 1px solid #1a1a1a; | |
| } | |
| .agent-panel .val { color: #88cc88; font-family: monospace; } | |
| /* Hide streamlit cruft */ | |
| footer { display: none !important; } | |
| [data-testid="stToolbar"] { display: none !important; } | |
| /* ═══ SCROLLABLE DATA PANELS ═══ */ | |
| .scroll-panel { | |
| max-height: 350px; | |
| overflow-y: auto; | |
| background: #0a0a0a; | |
| border: 1px solid #222; | |
| border-radius: 0; | |
| padding: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .scroll-panel::-webkit-scrollbar { width: 6px; } | |
| .scroll-panel::-webkit-scrollbar-track { background: #080808; } | |
| .scroll-panel::-webkit-scrollbar-thumb { background: #333; } | |
| .scroll-panel::-webkit-scrollbar-thumb:hover { background: #444; } | |
| .scroll-panel-wide { | |
| max-height: 400px; | |
| overflow-y: auto; | |
| background: #0a0a0a; | |
| border: 1px solid #222; | |
| border-radius: 0; | |
| padding: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .scroll-panel-wide::-webkit-scrollbar { width: 6px; } | |
| .scroll-panel-wide::-webkit-scrollbar-track { background: #080808; } | |
| .scroll-panel-wide::-webkit-scrollbar-thumb { background: #333; } | |
| .scroll-panel-wide::-webkit-scrollbar-thumb:hover { background: #444; } | |
| /* Panel headers */ | |
| .scroll-panel h4, .scroll-panel-wide h4 { | |
| color: #888; | |
| margin-bottom: 6px; | |
| border-bottom: 1px solid #222; | |
| padding-bottom: 4px; | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| /* Progress bars - flat */ | |
| .stProgress > div > div { | |
| background: #333 !important; | |
| border-radius: 0 !important; | |
| } | |
| .stProgress > div > div > div { | |
| background: #668866 !important; | |
| border-radius: 0 !important; | |
| } | |
| /* Expanders - flat */ | |
| .streamlit-expanderHeader { | |
| background: #111 !important; | |
| border: 1px solid #222 !important; | |
| border-radius: 0 !important; | |
| } | |
| /* Code blocks */ | |
| code { | |
| background: #111 !important; | |
| border: 1px solid #222 !important; | |
| border-radius: 0 !important; | |
| font-family: 'Consolas', 'Monaco', monospace !important; | |
| } | |
| /* Sliders - release on mouseup, prevent sticky drag */ | |
| .stSlider [data-baseweb="slider"] { | |
| pointer-events: auto !important; | |
| } | |
| .stSlider input[type="range"]:focus { | |
| outline: none !important; | |
| } | |
| </style> | |
| <script> | |
| // Force sliders to blur on mouseup so they don't stay selected | |
| document.addEventListener('mouseup', function(e) { | |
| var sliders = document.querySelectorAll('.stSlider input[type="range"], .stSlider [role="slider"]'); | |
| sliders.forEach(function(s) { s.blur(); }); | |
| }); | |
| document.addEventListener('touchend', function(e) { | |
| var sliders = document.querySelectorAll('.stSlider input[type="range"], .stSlider [role="slider"]'); | |
| sliders.forEach(function(s) { s.blur(); }); | |
| }); | |
| </script> | |
| """, unsafe_allow_html=True) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # GATHER DATA | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| diagnostics = get_champion_diagnostics() | |
| identity = diagnostics.get('identity', {}) | |
| arch = diagnostics.get('architecture', {}) | |
| caps = diagnostics.get('capabilities', {}) | |
| traits_data = diagnostics.get('traits', {}) | |
| lineage_data = {} | |
| swarm_stats = {} | |
| provenance_receipt = {} | |
| if st.session_state.initialized: | |
| lineage_data = st.session_state.swarm.get_agent_lineage_data() | |
| swarm_stats = st.session_state.swarm.get_swarm_stats() | |
| if st.session_state.cascade: | |
| provenance_receipt = st.session_state.cascade.get_session_receipt() | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # SIDEBAR - Controls & Settings (VISIBLE BY DEFAULT) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| with st.sidebar: | |
| st.markdown("# 🎮 CONTROLS") | |
| # BIG obvious buttons | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("🚀 START", key="sidebar_start"): | |
| with st.spinner("Initializing..."): | |
| init_simulation() | |
| st.rerun() | |
| with col2: | |
| if st.button("⚡ STEP", use_container_width=True): | |
| step_simulation() | |
| st.rerun() | |
| # Auto-run toggle - AGGRESSIVE STOP | |
| st.markdown("---") | |
| auto_label = "🛑 STOP AUTO" if st.session_state.auto_run else "▶️ AUTO RUN" | |
| auto_class = "auto-btn-active" if st.session_state.auto_run else "auto-btn" | |
| st.markdown(f'<div class="{auto_class}">', unsafe_allow_html=True) | |
| if st.button(auto_label, use_container_width=True): | |
| if st.session_state.auto_run: | |
| # AGGRESSIVE STOP - set flag immediately, no toggle | |
| st.session_state.auto_run = False | |
| st.session_state.stop_requested += 1 | |
| else: | |
| st.session_state.auto_run = True | |
| st.session_state.stop_requested = 0 | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # 🔒 HOLD TOGGLE - Uses cascade-lattice HOLD system | |
| st.markdown("---") | |
| hold_label = "🔓 RELEASE HOLD" if st.session_state.get('hold_active', False) else "🔒 HOLD GRAPH" | |
| hold_help = "HOLD pauses simulation & lets you rotate/pan the 3D graph freely" | |
| if st.button(hold_label, use_container_width=True, help=hold_help): | |
| if st.session_state.get('hold_active', False): | |
| # Release hold - RESUME auto if it was running before hold | |
| st.session_state.hold_active = False | |
| # Restore auto_run state from before hold | |
| if st.session_state.get('auto_run_before_hold', False): | |
| st.session_state.auto_run = True | |
| st.session_state.auto_run_before_hold = False | |
| if hold_system: | |
| hold_system.accept() # Accept/release the hold point | |
| else: | |
| # Engage hold - PAUSE auto_run (save state to restore later) | |
| st.session_state.hold_active = True | |
| # Save current auto_run state before pausing | |
| st.session_state.auto_run_before_hold = st.session_state.auto_run | |
| st.session_state.auto_run = False | |
| if hold_system: | |
| # Log hold engagement to cascade (action_probs must be numpy array) | |
| hold_system.yield_point( | |
| action_probs=np.array([1.0]), | |
| value=0.0, | |
| observation={"event": "graph_hold", "step": st.session_state.step_count}, | |
| brain_id="hyperlattice_ui" | |
| ) | |
| st.rerun() | |
| if st.session_state.get('hold_active', False): | |
| st.info("🔒 **HOLD ACTIVE** - Graph interaction enabled. Simulation paused.") | |
| # Check for accumulated stop requests | |
| if st.session_state.get('stop_requested', 0) > 0: | |
| st.session_state.auto_run = False | |
| # Check hold state - if active, force auto_run off | |
| check_hold() | |
| st.markdown("---") | |
| st.markdown("## ⚙️ Settings") | |
| st.session_state.lattice_size = st.slider("🌐 Lattice Size", 4, 12, st.session_state.lattice_size) | |
| st.session_state.num_agents = st.slider("👥 Agents", 3, 25, st.session_state.num_agents) | |
| st.session_state.max_agents = st.slider("💀 Max Pop (cull)", 5, 50, st.session_state.get('max_agents', 15)) | |
| st.session_state.num_gates = st.slider("🔮 Null Gates", 0, 15, st.session_state.num_gates) | |
| if st.button("🔄 REINIT", use_container_width=True): | |
| init_simulation() | |
| st.rerun() | |
| # System info | |
| st.markdown("---") | |
| st.markdown("## 🖥️ System") | |
| if GPU_AVAILABLE: | |
| st.success(f"✅ {GPU_NAME}") | |
| st.caption(f"{GPU_MEMORY}GB VRAM") | |
| else: | |
| st.warning("⚠️ CPU Mode") | |
| # Selected agent drill-down | |
| if st.session_state.selected_agent and st.session_state.selected_agent in lineage_data: | |
| st.markdown("---") | |
| st.markdown("## 👤 Selected Agent") | |
| d = lineage_data[st.session_state.selected_agent] | |
| st.code(st.session_state.selected_agent, language=None) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.metric("Gen", d['generation']) | |
| st.metric("Children", d['num_children']) | |
| with col2: | |
| st.metric("Fitness", f"{d['fitness']:.4f}") | |
| # Experience chain - FULL, NO TRUNCATION | |
| if st.session_state.experience_chain: | |
| with st.expander(f"📜 Experience Chain ({len(st.session_state.experience_chain)})", expanded=True): | |
| for i, exp in enumerate(st.session_state.experience_chain): | |
| st.caption(f"#{i+1} → {exp.get('to_node', '?')} (R:{exp.get('reward', 0):.3f})") | |
| if st.button("❌ Deselect", use_container_width=True): | |
| st.session_state.selected_agent = None | |
| st.session_state.experience_chain = [] | |
| st.rerun() | |
| # ═══════════════════════════════════════════════════════════════ | |
| # CASCADE-LATTICE STATS - Full package integration | |
| # ═══════════════════════════════════════════════════════════════ | |
| st.markdown("---") | |
| st.markdown("## 🔗 CASCADE LATTICE") | |
| if CASCADE_AVAILABLE and cascade_store: | |
| try: | |
| lattice_stats = cascade_store.stats() | |
| st.metric("📊 Observations", f"{lattice_stats.get('total_observations', 0):,}") | |
| st.metric("📌 Pinned", lattice_stats.get('pinned_observations', 0)) | |
| # Genesis root | |
| genesis = lattice_stats.get('genesis_root', 'unknown') | |
| st.code(f"Genesis: {genesis}", language=None) | |
| # Model count | |
| models = lattice_stats.get('models', {}) | |
| st.caption(f"{len(models)} model types tracked") | |
| # Show ALL models in expander - NO TRUNCATION | |
| with st.expander(f"📁 Models ({len(models)} total)", expanded=False): | |
| sorted_models = sorted(models.items(), key=lambda x: -x[1]) # ALL models | |
| for model_id, count in sorted_models: | |
| st.text(f"{model_id}: {count}") | |
| # HOLD stats | |
| if Hold: | |
| hold = Hold.get() | |
| hold_stats = hold.stats | |
| st.caption(f"HOLD: {hold_stats.get('total_holds', 0)} holds, {hold_stats.get('overrides', 0)} overrides") | |
| except Exception as e: | |
| st.caption(f"Stats error: {e}") | |
| else: | |
| st.caption("cascade-lattice not available") | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MAIN AREA - Graph Left (60%), Data Right (40%) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| st.markdown("# CASCADE Hyperlattice") | |
| st.markdown(f"**Step: {st.session_state.step_count}** | Agents: {len(lineage_data) if st.session_state.initialized else 0}") | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MAIN LAYOUT: Responsive - Graph (left 65%) | Data Panels (right 35%) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| graph_col, data_col = st.columns([2, 1], gap="medium") | |
| with graph_col: | |
| if not st.session_state.initialized: | |
| st.markdown(""" | |
| <div style="text-align:center; padding: 40px 16px; background: #0a0a0a; border: 1px dashed #333; margin: 8px 0;"> | |
| <div style="font-size: 2rem;">▶</div> | |
| <div style="font-size: 1rem; color: #666; margin: 12px 0;">Press START</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| # Build 3D graph | |
| def get_lineage_color(root_id, generation, fitness, max_gen): | |
| hue = (hash(root_id) % 360) / 360.0 | |
| saturation = max(0.3, 1.0 - (generation / max(max_gen, 1)) * 0.7) | |
| lightness = 0.3 + min(1.0, fitness / 5.0) * 0.5 | |
| r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation) | |
| return f'rgb({int(r*255)},{int(g*255)},{int(b*255)})' | |
| max_gen = max((d['generation'] for d in lineage_data.values()), default=1) | |
| x_vals, y_vals, z_vals = [], [], [] | |
| colors, sizes, texts = [], [], [] | |
| agent_ids = list(lineage_data.keys()) | |
| for aid, d in lineage_data.items(): | |
| pos = d['position'] | |
| x_vals.append(pos[0]) | |
| y_vals.append(pos[1]) | |
| z_vals.append(pos[2]) | |
| color = get_lineage_color(d['root_lineage'], d['generation'], d['fitness'], max_gen) | |
| colors.append(color) | |
| size = 20 if st.session_state.selected_agent == aid else 12 | |
| sizes.append(size) | |
| short_id = aid[-8:] if len(aid) > 8 else aid | |
| texts.append(f"<b>{short_id}</b><br>Gen: {d['generation']}<br>Fit: {d['fitness']:.4f}") | |
| fig = go.Figure() | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # QUINE CONFLUENCE TRAILS - Each agent has unique color from Merkle hash | |
| # Temporal gradient: solid at present → faded at origin | |
| # Direction cones show traversal direction | |
| # ═══════════════════════════════════════════════════════════════════ | |
| def hash_to_hsl(id_str: str) -> tuple: | |
| """Convert any string (agent_id) to unique HSL color via hashing.""" | |
| if not id_str: | |
| return (180, 70, 50) # Default cyan | |
| # Hash the string to get consistent hex representation | |
| import hashlib | |
| hex_hash = hashlib.md5(id_str.encode()).hexdigest() | |
| # Use hash chars for hue, saturation, lightness | |
| h = int(hex_hash[:4], 16) % 360 | |
| s = 60 + (int(hex_hash[4:6], 16) % 30) # 60-90% | |
| l = 45 + (int(hex_hash[6:8], 16) % 20) # 45-65% | |
| return (h, s, l) | |
| def hsl_to_rgb(h, s, l): | |
| """Convert HSL to RGB.""" | |
| s, l = s / 100, l / 100 | |
| c = (1 - abs(2 * l - 1)) * s | |
| x = c * (1 - abs((h / 60) % 2 - 1)) | |
| m = l - c / 2 | |
| if h < 60: r, g, b = c, x, 0 | |
| elif h < 120: r, g, b = x, c, 0 | |
| elif h < 180: r, g, b = 0, c, x | |
| elif h < 240: r, g, b = 0, x, c | |
| elif h < 300: r, g, b = x, 0, c | |
| else: r, g, b = c, 0, x | |
| return int((r + m) * 255), int((g + m) * 255), int((b + m) * 255) | |
| # Group movements by agent | |
| agent_trails = {} # agent_id -> list of (from_node, to_node, step, is_null_transit) | |
| max_step = st.session_state.step_count or 1 | |
| for e in st.session_state.events: | |
| if e["type"] in ["move", "null_transit"]: | |
| data = e.get("data", {}) | |
| agent_id = e.get("agent", "unknown") | |
| from_node = data.get("from_node") or "" | |
| to_node = data.get("to_node") or "" | |
| step = e.get("step", 0) | |
| is_null = e["type"] == "null_transit" | |
| if from_node and to_node: | |
| if agent_id not in agent_trails: | |
| agent_trails[agent_id] = [] | |
| agent_trails[agent_id].append((from_node, to_node, step, is_null)) | |
| # Get agent colors from STABLE identifier (agent_id, not quine_hash which evolves) | |
| # agent_id is the birth hash - it never changes | |
| agent_colors = {} | |
| for agent_id in agent_trails.keys(): | |
| # Use the agent_id itself - it's the stable birth hash | |
| agent_colors[agent_id] = hash_to_hsl(agent_id) | |
| # Draw each agent's trail - connect ALL segments in step order | |
| # The path should be continuous since to_node[n] == from_node[n+1] in normal movement | |
| for agent_id, moves in agent_trails.items(): | |
| if not moves: | |
| continue | |
| # Sort by step to ensure correct path order | |
| moves.sort(key=lambda m: m[2]) | |
| h, s, l = agent_colors.get(agent_id, (180, 70, 50)) | |
| r, g, b = hsl_to_rgb(h, s, l) | |
| # Build continuous path - just chain all positions together | |
| path_x, path_y, path_z = [], [], [] | |
| hover_texts = [] | |
| null_segment_indices = [] # Track segment start indices for null transits | |
| for i, (from_node, to_node, step, is_null) in enumerate(moves): | |
| if from_node not in st.session_state.lattice.nodes or to_node not in st.session_state.lattice.nodes: | |
| continue | |
| p1 = st.session_state.lattice.get_node_position(from_node) | |
| p2 = st.session_state.lattice.get_node_position(to_node) | |
| # Always add from_node for each segment (creates overlapping points but ensures continuity) | |
| path_x.append(p1[0]) | |
| path_y.append(p1[1]) | |
| path_z.append(p1[2]) | |
| hover_texts.append(f"{agent_id[:8]} step {step}") | |
| # Add destination point | |
| path_x.append(p2[0]) | |
| path_y.append(p2[1]) | |
| path_z.append(p2[2]) | |
| hover_texts.append(f"{agent_id[:8]} step {step}") | |
| # Track null transit segment for dashed overlay | |
| if is_null: | |
| null_segment_indices.append(len(path_x) - 2) # Index of from point | |
| # Draw the path with breaks | |
| if len(path_x) >= 2: | |
| opacity = 0.85 | |
| width = 2.5 | |
| fig.add_trace(go.Scatter3d( | |
| x=path_x, y=path_y, z=path_z, | |
| mode='lines+markers', | |
| line=dict(color=f'rgba({r},{g},{b},{opacity:.2f})', width=width), | |
| marker=dict(size=2, color=f'rgba({r},{g},{b},{opacity:.2f})'), | |
| hoverinfo='text', | |
| hovertext=hover_texts, | |
| showlegend=False, | |
| connectgaps=False # Don't connect across None values | |
| )) | |
| # Overlay dashed lines for null transit segments | |
| for seg_idx in null_segment_indices: | |
| if seg_idx + 1 < len(path_x) and path_x[seg_idx] is not None and path_x[seg_idx + 1] is not None: | |
| fig.add_trace(go.Scatter3d( | |
| x=[path_x[seg_idx], path_x[seg_idx + 1]], | |
| y=[path_y[seg_idx], path_y[seg_idx + 1]], | |
| z=[path_z[seg_idx], path_z[seg_idx + 1]], | |
| mode='lines', | |
| line=dict(color=f'rgba({r},{g},{b},0.9)', width=3, dash='dash'), | |
| hoverinfo='skip', | |
| showlegend=False | |
| )) | |
| # NULL GATE CONNECTIONS - Glowing wormhole tunnels | |
| null_pairs = st.session_state.lattice.get_null_gate_pairs() | |
| gate_conn_x, gate_conn_y, gate_conn_z = [], [], [] | |
| for n1, n2 in null_pairs: | |
| p1 = st.session_state.lattice.get_node_position(n1) | |
| p2 = st.session_state.lattice.get_node_position(n2) | |
| gate_conn_x.extend([p1[0], p2[0], None]) | |
| gate_conn_y.extend([p1[1], p2[1], None]) | |
| gate_conn_z.extend([p1[2], p2[2], None]) | |
| if gate_conn_x: | |
| # Outer glow | |
| fig.add_trace(go.Scatter3d( | |
| x=gate_conn_x, y=gate_conn_y, z=gate_conn_z, | |
| mode='lines', | |
| line=dict(color='rgba(255,100,255,0.3)', width=8), | |
| hoverinfo='none', | |
| showlegend=False | |
| )) | |
| # Core beam | |
| fig.add_trace(go.Scatter3d( | |
| x=gate_conn_x, y=gate_conn_y, z=gate_conn_z, | |
| mode='lines', | |
| line=dict(color='rgba(255,0,255,0.7)', width=3), | |
| hoverinfo='none', | |
| name='Wormholes' | |
| )) | |
| # AGENTS - 🐭 Mouse visualization to chase the cheese! | |
| # Outer glow ring (fitness-based intensity) | |
| glow_sizes = [] | |
| glow_colors = [] | |
| for aid, d in lineage_data.items(): | |
| fitness = d['fitness'] | |
| # Glow size based on fitness (more fit = bigger aura) | |
| glow_size = 25 + min(fitness * 10, 30) | |
| glow_sizes.append(glow_size) | |
| # Glow color with transparency | |
| base_color = colors[list(lineage_data.keys()).index(aid)] | |
| glow_colors.append(base_color.replace('rgb(', 'rgba(').replace(')', ',0.25)')) | |
| # Outer glow layer for mice | |
| fig.add_trace(go.Scatter3d( | |
| x=x_vals, y=y_vals, z=z_vals, | |
| mode='markers', | |
| marker=dict(size=glow_sizes, color=glow_colors, opacity=0.3, | |
| line=dict(width=0)), | |
| hoverinfo='none', | |
| showlegend=False | |
| )) | |
| # 🐭 Mouse emoji markers for quine agents! | |
| fig.add_trace(go.Scatter3d( | |
| x=x_vals, y=y_vals, z=z_vals, | |
| mode='markers+text', | |
| marker=dict(size=sizes, color=colors, opacity=0.9, | |
| symbol='circle', | |
| line=dict(width=2, color='#fff')), | |
| text=['🐭'] * len(x_vals), | |
| textposition='middle center', | |
| textfont=dict(size=14, color='#ffffff'), | |
| hovertext=texts, | |
| hoverinfo='text', | |
| customdata=agent_ids, | |
| name='Mice' | |
| )) | |
| # SELECTION HIGHLIGHT - Glowing ring around selected agent | |
| if st.session_state.selected_agent and st.session_state.selected_agent in lineage_data: | |
| sel_d = lineage_data[st.session_state.selected_agent] | |
| sel_pos = sel_d['position'] | |
| # Add multiple rings for glow effect | |
| for ring_size, ring_alpha in [(35, 0.3), (28, 0.5), (22, 0.8)]: | |
| fig.add_trace(go.Scatter3d( | |
| x=[sel_pos[0]], y=[sel_pos[1]], z=[sel_pos[2]], | |
| mode='markers', | |
| marker=dict( | |
| size=ring_size, | |
| color=f'rgba(0,255,255,{ring_alpha})', | |
| symbol='circle-open', | |
| line=dict(width=3, color='#00ffff') | |
| ), | |
| hoverinfo='none', | |
| name='Selection', | |
| showlegend=False | |
| )) | |
| # Lineage edges | |
| edge_x, edge_y, edge_z = [], [], [] | |
| for aid, d in lineage_data.items(): | |
| if d['parent_hash']: | |
| for pid, pd in lineage_data.items(): | |
| if pd['quine_hash'] == d['parent_hash']: | |
| pos1 = pd['position'] | |
| pos2 = d['position'] | |
| edge_x.extend([pos1[0], pos2[0], None]) | |
| edge_y.extend([pos1[1], pos2[1], None]) | |
| edge_z.extend([pos1[2], pos2[2], None]) | |
| break | |
| if edge_x: | |
| fig.add_trace(go.Scatter3d( | |
| x=edge_x, y=edge_y, z=edge_z, | |
| mode='lines', | |
| line=dict(color='rgba(80,80,80,0.5)', width=2), | |
| hoverinfo='none', | |
| name='Lineage' | |
| )) | |
| # Null gates - PORTAL visualization with glowing rings | |
| gate_ids = [] | |
| if st.session_state.lattice.null_gates: | |
| null_x, null_y, null_z = [], [], [] | |
| gate_hovers = [] | |
| for gate_key, node_id in st.session_state.lattice.null_gates.items(): | |
| try: | |
| pos = st.session_state.lattice.get_node_position(node_id) | |
| null_x.append(pos[0]) | |
| null_y.append(pos[1]) | |
| null_z.append(pos[2]) | |
| gate_ids.append(gate_key) | |
| # Find paired gate | |
| pair = st.session_state.lattice.get_gate_pair(gate_key) | |
| pair_str = pair[:8] if pair else "N/A" | |
| gate_hovers.append(f"⚡ PORTAL·Gate: {gate_key[:8]}·Pair: {pair_str}·Node: {node_id[:8]}") | |
| except (KeyError, AttributeError): | |
| pass | |
| if null_x: | |
| # Outer glow ring | |
| fig.add_trace(go.Scatter3d( | |
| x=null_x, y=null_y, z=null_z, | |
| mode='markers', | |
| marker=dict(size=28, color='rgba(255,100,255,0.2)', | |
| symbol='circle-open', | |
| line=dict(width=4, color='rgba(255,50,255,0.4)')), | |
| hoverinfo='none', | |
| showlegend=False | |
| )) | |
| # Middle ring | |
| fig.add_trace(go.Scatter3d( | |
| x=null_x, y=null_y, z=null_z, | |
| mode='markers', | |
| marker=dict(size=18, color='rgba(200,50,255,0.5)', | |
| symbol='circle-open', | |
| line=dict(width=3, color='#ff44ff')), | |
| hoverinfo='none', | |
| showlegend=False | |
| )) | |
| # Core portal marker - just the lightning bolt, no X | |
| fig.add_trace(go.Scatter3d( | |
| x=null_x, y=null_y, z=null_z, | |
| mode='markers+text', | |
| marker=dict(size=12, color='#ff00ff', opacity=0.8, | |
| symbol='circle', | |
| line=dict(width=2, color='#ff88ff')), | |
| text=['⚡' for _ in null_x], | |
| textposition='middle center', | |
| textfont=dict(size=18, color='#ffffff'), | |
| hovertext=gate_hovers, | |
| hoverinfo='text', | |
| customdata=gate_ids, | |
| name='Portals' | |
| )) | |
| # 🧀 THE CHEESE - Render LAST so it's on top of everything! | |
| if st.session_state.cheese_position and not st.session_state.cheese_found: | |
| try: | |
| cheese_pos = st.session_state.lattice.get_node_position(st.session_state.cheese_position) | |
| cx, cy, cz = cheese_pos[0], cheese_pos[1], cheese_pos[2] | |
| # Big glowing cheese marker - larger than gates, distinctive color | |
| fig.add_trace(go.Scatter3d( | |
| x=[cx], y=[cy], z=[cz], | |
| mode='markers+text', | |
| marker=dict(size=20, color='#FFD700', opacity=1.0, | |
| symbol='circle', | |
| line=dict(width=3, color='#FF8C00')), | |
| text=['🧀'], | |
| textposition='top center', | |
| textfont=dict(size=24, color='#FFD700'), | |
| hovertext='🧀 THE CHEESE! Find me!', | |
| hoverinfo='text', | |
| name='🧀 Cheese' | |
| )) | |
| except (KeyError, AttributeError): | |
| pass | |
| # Camera - use saved position if HOLD was used, otherwise default | |
| if st.session_state.get('hold_camera'): | |
| camera_eye = st.session_state.hold_camera | |
| else: | |
| camera_eye = dict(x=1.5, y=1.5, z=1.0) | |
| # Unique key for uirevision to preserve camera during HOLD | |
| ui_rev = 'hold_locked' if st.session_state.get('hold_active', False) else 'camera_lock' | |
| # Build scene config | |
| scene_config = dict( | |
| xaxis=dict(showbackground=False, showgrid=True, gridcolor='#222', zeroline=False, title='', showticklabels=False), | |
| yaxis=dict(showbackground=False, showgrid=True, gridcolor='#222', zeroline=False, title='', showticklabels=False), | |
| zaxis=dict(showbackground=False, showgrid=True, gridcolor='#222', zeroline=False, title='', showticklabels=False), | |
| bgcolor='#0a0a0a', | |
| camera=dict(eye=camera_eye), | |
| aspectmode='cube', # Lock aspect ratio | |
| dragmode='orbit' if st.session_state.get('hold_active', False) else 'pan' # Orbit when HOLD active | |
| ) | |
| fig.update_layout( | |
| scene=scene_config, | |
| paper_bgcolor='#0a0a0a', | |
| plot_bgcolor='#0a0a0a', | |
| showlegend=False, | |
| margin=dict(l=0, r=0, t=0, b=0), | |
| width=800, # FIXED width - no resize on settings change | |
| height=700, # FIXED height | |
| autosize=False, # DISABLE autosize to prevent resizing | |
| uirevision=ui_rev, # PRESERVE camera/zoom - different key during HOLD | |
| scene_camera=dict(projection=dict(type='perspective')) | |
| ) | |
| # 🔒 HOLD indicator overlay on graph | |
| if st.session_state.get('hold_active', False): | |
| st.markdown(""" | |
| <div style="position: relative; top: -720px; left: 10px; z-index: 1000; | |
| background: rgba(255,100,0,0.9); color: #fff; padding: 8px 16px; | |
| border-radius: 4px; font-weight: bold; width: fit-content; | |
| font-family: monospace; pointer-events: none;"> | |
| 🔒 HOLD ACTIVE - Drag to rotate/pan | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 🧀 CHEESE VICTORY CELEBRATION! | |
| if st.session_state.cheese_found: | |
| st.balloons() | |
| st.success(f"🧀🎉 **THE CHEESE HAS BEEN FOUND!** 🎉🧀\n\n**Champion:** `{st.session_state.cheese_finder}`\n\n*Press Reset Sim to play again!*") | |
| # Render graph - NO selection callback during HOLD (allows free interaction) | |
| if st.session_state.get('hold_active', False): | |
| # During HOLD: no on_select to prevent reruns, allow free rotation/pan | |
| event = st.plotly_chart(fig, key="main_graph_hold", width="content") | |
| else: | |
| # Normal mode: selection callback enabled | |
| event = st.plotly_chart(fig, key="main_graph", on_select="rerun", selection_mode="points", width="content") | |
| # Handle selection from graph click | |
| if event and hasattr(event, 'selection'): | |
| sel = event.selection | |
| # Streamlit selection is a dict with 'points' key when user clicks | |
| # Skip if it's not a dict (could be a method reference or other object) | |
| if isinstance(sel, dict) and sel.get('points'): | |
| point = sel['points'][0] | |
| trace_idx = point.get('curve_number', point.get('curveNumber', -1)) | |
| pt_idx = point.get('point_number', point.get('pointNumber', -1)) | |
| # Determine which trace was clicked | |
| # Trail = 0, Wormholes = 1, Agents = 2, Lineage = 3, Gates = 4 | |
| # (order depends on how many traces were added) | |
| trace_name = "" | |
| try: | |
| trace_name = fig.data[trace_idx].name | |
| except: | |
| pass | |
| if trace_name == 'Agents' and pt_idx >= 0 and pt_idx < len(agent_ids): | |
| clicked_agent = agent_ids[pt_idx] | |
| st.session_state.selected_agent = clicked_agent | |
| st.session_state.selection_type = 'agent' | |
| st.session_state.experience_chain = st.session_state.swarm.get_experience_chain(clicked_agent) | |
| st.rerun() | |
| elif trace_name == 'Gates' and pt_idx >= 0 and pt_idx < len(gate_ids): | |
| clicked_gate = gate_ids[pt_idx] | |
| st.session_state.selected_gate = clicked_gate | |
| st.session_state.selection_type = 'gate' | |
| st.rerun() | |
| # ═══════════════════════════════════════════════════════════════ | |
| # 🧬 LINEAGE SUNBURST - Interactive genealogical tree | |
| # ═══════════════════════════════════════════════════════════════ | |
| st.markdown("---") | |
| st.markdown("### 🧬 LINEAGE TREE") | |
| # Build sunburst data structure | |
| sb_ids = [] | |
| sb_parents = [] | |
| sb_values = [] | |
| sb_labels = [] | |
| sb_colors = [] | |
| # Map quine_hash -> agent_id for parent lookup | |
| hash_to_id = {d['quine_hash']: aid for aid, d in lineage_data.items()} | |
| for aid, d in lineage_data.items(): | |
| sb_ids.append(aid) | |
| # Find parent by quine_hash | |
| parent_hash = d.get('parent_hash') | |
| if parent_hash and parent_hash in hash_to_id: | |
| sb_parents.append(hash_to_id[parent_hash]) | |
| else: | |
| sb_parents.append("") # Root node | |
| # Value = fitness (minimum 0.1 for visibility) | |
| sb_values.append(max(0.1, d['fitness'])) | |
| # Label = short ID + generation | |
| short_id = aid[-6:] if len(aid) > 6 else aid | |
| sb_labels.append(f"{short_id}<br>G{d['generation']}") | |
| # Color by generation | |
| sb_colors.append(d['generation']) | |
| if sb_ids: | |
| # Create sunburst chart | |
| sunburst_fig = go.Figure(go.Sunburst( | |
| ids=sb_ids, | |
| labels=sb_labels, | |
| parents=sb_parents, | |
| values=sb_values, | |
| branchvalues="total", | |
| marker=dict( | |
| colors=sb_colors, | |
| colorscale='Viridis', | |
| line=dict(width=2, color='#111') | |
| ), | |
| hovertemplate="<b>%{label}</b><br>Fitness: %{value:.2f}<extra></extra>", | |
| textfont=dict(size=11, color='white'), | |
| insidetextorientation='radial' | |
| )) | |
| sunburst_fig.update_layout( | |
| margin=dict(t=10, l=10, r=10, b=10), | |
| paper_bgcolor='#0a0a0a', | |
| height=280, | |
| font=dict(color='white'), | |
| clickmode='event+select' # Enable click events | |
| ) | |
| # Render chart | |
| st.plotly_chart(sunburst_fig, key="lineage_sunburst", width="stretch") | |
| # Agent selector with radio buttons (guaranteed to work) | |
| sorted_agents = sorted(lineage_data.items(), key=lambda x: -x[1]['fitness']) | |
| # Radio button selector | |
| agent_labels = {aid: f"🧬 {aid[-6:]} G{d['generation']} F{d['fitness']:.1f}" for aid, d in sorted_agents} | |
| agent_labels[None] = "— none —" | |
| current_sel = st.session_state.selected_agent if st.session_state.selected_agent in agent_labels else None | |
| new_selection = st.radio( | |
| "Select Agent:", | |
| options=[None] + [aid for aid, _ in sorted_agents], | |
| format_func=lambda x: agent_labels.get(x, str(x)), | |
| index=0 if current_sel is None else list(agent_labels.keys()).index(current_sel), | |
| key="agent_radio", | |
| horizontal=True, | |
| label_visibility="collapsed" | |
| ) | |
| if new_selection != st.session_state.selected_agent: | |
| st.session_state.selected_agent = new_selection | |
| if new_selection: | |
| st.session_state.experience_chain = st.session_state.swarm.get_experience_chain(new_selection) | |
| st.toast(f"✅ {new_selection[-8:]}", icon="🧬") | |
| st.rerun() | |
| # ═══════════════════════════════════════════════════════════════ | |
| # 💻 REAL RICH TUI TERMINAL - Genuine Rich library rendering | |
| # ═══════════════════════════════════════════════════════════════ | |
| st.markdown("### 💻 RICH TUI TERMINAL") | |
| # Get cascade stats | |
| cascade_stats = {} | |
| if CASCADE_AVAILABLE and cascade_store: | |
| try: | |
| cascade_stats = cascade_store.stats() | |
| except: | |
| cascade_stats = {} | |
| # Render REAL Rich TUI and export to SVG | |
| rich_svg = render_rich_tui( | |
| events=st.session_state.events, # ALL events, no truncation | |
| cascade_stats=cascade_stats, | |
| lineage_data=lineage_data, | |
| cascade_store=cascade_store if CASCADE_AVAILABLE else None | |
| ) | |
| # Use st.components.html() which properly renders SVG with embedded <style> | |
| # st.markdown corrupts the CSS - shows it as raw text | |
| import streamlit.components.v1 as components | |
| # Wrap SVG in proper HTML document with dark background | |
| full_html = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <style> | |
| body {{ | |
| margin: 0; | |
| padding: 8px; | |
| background: #0a0a0a; | |
| overflow: auto; | |
| }} | |
| svg {{ | |
| width: 100%; | |
| height: auto; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| {rich_svg} | |
| </body> | |
| </html> | |
| """ | |
| # Render with components.html - properly handles embedded styles | |
| # Height extended to align with architecture panel on right | |
| components.html(full_html, height=670, scrolling=True) | |
| with data_col: | |
| if st.session_state.initialized: | |
| sel_agent = st.session_state.selected_agent | |
| sel_gate = st.session_state.selected_gate | |
| has_selection = sel_agent or sel_gate | |
| # ═══════════════════════════════════════════════════════════════ | |
| # SELECTED ITEM - Always visible at top | |
| # ═══════════════════════════════════════════════════════════════ | |
| if sel_agent and sel_agent in lineage_data: | |
| d = lineage_data[sel_agent] | |
| # Big glowing header for selected agent | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(135deg, #00ffff11, #00ff8811); | |
| border: 2px solid #0ff; border-radius: 12px; padding: 16px; | |
| margin-bottom: 16px; box-shadow: 0 0 20px #0ff3;"> | |
| <div style="color: #0ff; font-size: 1.5em; font-weight: bold;">🎯 SELECTED</div> | |
| <div style="color: #fff; font-size: 1.8em; font-family: monospace;">{sel_agent[-12:]}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| c1, c2, c3, c4 = st.columns(4) | |
| c1.metric("Gen", d['generation']) | |
| c2.metric("Fit", f"{d['fitness']:.2f}") | |
| c3.metric("Kids", d['num_children']) | |
| c4.metric("Hash", d['quine_hash'][:6]) | |
| # 🔥 FULL AGENT ACTIVITY PANEL - Everything this quine has done | |
| with st.expander("📊 **FULL ACTIVITY LOG**", expanded=True): | |
| # Get ALL events for this agent | |
| agent_events = [e for e in st.session_state.events if e.get('agent') == sel_agent] | |
| if agent_events: | |
| st.markdown(f"**{len(agent_events)} total actions**") | |
| # Group by type | |
| event_types = {} | |
| for e in agent_events: | |
| etype = e.get('type', 'unknown') | |
| if etype not in event_types: | |
| event_types[etype] = [] | |
| event_types[etype].append(e) | |
| # Show summary | |
| cols = st.columns(len(event_types) if event_types else 1) | |
| for i, (etype, events) in enumerate(event_types.items()): | |
| cols[i % len(cols)].metric(etype[:10], len(events)) | |
| st.markdown("---") | |
| # Full chronological log | |
| st.markdown("**Chronological Activity:**") | |
| for e in agent_events: | |
| step = e.get('step', '?') | |
| etype = e.get('type', '?') | |
| data = e.get('data', {}) | |
| # Format based on event type | |
| if etype == 'move': | |
| from_n = (data.get('from_node') or '')[:8] | |
| to_n = (data.get('to_node') or '')[:8] | |
| st.text(f"S{step} → MOVE {from_n} → {to_n}") | |
| elif etype == 'null_transit': | |
| from_n = (data.get('from_node') or '')[:8] | |
| to_n = (data.get('to_node') or '')[:8] | |
| st.text(f"S{step} ⚡ WARP {from_n} → {to_n}") | |
| elif etype == 'fork': | |
| parent = (data.get('parent') or '')[:8] | |
| st.text(f"S{step} 🧬 BORN from {parent} (G{data.get('gen', '?')})") | |
| elif '🧀' in etype: | |
| st.success(f"S{step} {etype}") | |
| else: | |
| st.text(f"S{step} {etype}: {str(data)[:30]}") | |
| else: | |
| st.caption("No recorded activity yet - run simulation!") | |
| # Experience chain (reward history) | |
| exp_chain = st.session_state.swarm.get_experience_chain(sel_agent) if st.session_state.swarm else [] | |
| if exp_chain: | |
| st.markdown("---") | |
| st.markdown(f"**Reward History ({len(exp_chain)} steps):**") | |
| rewards = [exp.get('reward', 0) for exp in exp_chain] | |
| total_reward = sum(rewards) | |
| avg_reward = total_reward / len(rewards) if rewards else 0 | |
| st.text(f"Total: {total_reward:.3f} | Avg: {avg_reward:.4f}") | |
| # Mini sparkline | |
| st.line_chart(rewards, height=80) | |
| if st.button("✖ CLEAR SELECTION", key="clr_agent", use_container_width=True, type="secondary"): | |
| st.session_state.selected_agent = None | |
| st.session_state.experience_chain = [] | |
| st.toast("Selection cleared", icon="🗑️") | |
| st.rerun() | |
| elif sel_gate: | |
| # Glowing header for selected gate | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(135deg, #ff444411, #ff880011); | |
| border: 2px solid #f44; border-radius: 12px; padding: 16px; | |
| margin-bottom: 16px; box-shadow: 0 0 20px #f443;"> | |
| <div style="color: #f44; font-size: 1.5em; font-weight: bold;">◆ NULL GATE</div> | |
| <div style="color: #fff; font-size: 1.8em; font-family: monospace;">{sel_gate[:10]}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if hasattr(st.session_state.lattice, 'get_gate_stats'): | |
| stats = st.session_state.lattice.get_gate_stats(sel_gate) | |
| if stats: | |
| c1, c2, c3 = st.columns(3) | |
| c1.metric("Node", stats.get('node_id', '?')[:6]) | |
| c2.metric("Dist", f"{stats.get('wormhole_distance', 0):.1f}") | |
| transit_count = sum(1 for e in st.session_state.events if e.get('type') == 'null_transit') | |
| c3.metric("Transits", transit_count) | |
| if st.button("✖ CLEAR SELECTION", key="clr_gate", use_container_width=True, type="secondary"): | |
| st.session_state.selected_gate = None | |
| st.toast("Gate selection cleared", icon="🗑️") | |
| st.rerun() | |
| else: | |
| st.markdown(f""" | |
| <div style="background: #111; border: 2px dashed #444; border-radius: 12px; | |
| padding: 24px; text-align: center; margin-bottom: 16px;"> | |
| <div style="font-size: 2em; margin-bottom: 8px;">🧬</div> | |
| <div style="color: #888; font-size: 1.2em;">SELECT AN AGENT</div> | |
| <div style="color: #555; font-size: 0.9em; margin-top: 8px;">Click sunburst segments or use dropdown</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.divider() | |
| # ═══════════════════════════════════════════════════════════════ | |
| # REACTIVE DATA PANELS - All visible, filter by selection | |
| # ═══════════════════════════════════════════════════════════════ | |
| # EXPERIENCE - Auto-refresh from swarm data | |
| st.markdown("### 📊 EXPERIENCE") | |
| if sel_agent: | |
| # Always get fresh experience data | |
| exp_chain = st.session_state.swarm.get_experience_chain(sel_agent) if st.session_state.swarm else [] | |
| if exp_chain: | |
| # FULL experience chain - NO TRUNCATION | |
| for i, exp in enumerate(exp_chain): | |
| cols = st.columns([1,2,2]) | |
| cols[0].text(f"#{i+1}") | |
| cols[1].text(f"A:{exp.get('action', '?')}") | |
| cols[2].text(f"R:{exp.get('reward', 0):.3f}") | |
| st.caption(f"Showing ALL {len(exp_chain)} experiences") | |
| else: | |
| st.caption("No experience data yet") | |
| else: | |
| st.caption("← Select agent to see experience") | |
| st.divider() | |
| # MOVEMENT LOG - Filters to selected agent | |
| st.markdown("### 🚶 MOVEMENTS") | |
| move_events = [e for e in st.session_state.events if e.get('type') in ['move', 'null_transit']] | |
| if sel_agent: | |
| move_events = [e for e in move_events if e.get('agent') == sel_agent] | |
| st.caption(f"Showing moves for {sel_agent[-8:]}") | |
| for e in reversed(move_events[-8:]): | |
| data = e.get('data', {}) | |
| from_n = (data.get('from_node') or '')[:10] | |
| to_n = (data.get('to_node') or '')[:10] | |
| icon = "⚡" if e.get('type') == 'null_transit' else "→" | |
| highlight = "**" if sel_agent and e.get('agent') == sel_agent else "" | |
| st.text(f"{highlight}S{e.get('step', '?')} {from_n} {icon} {to_n}{highlight}") | |
| if not move_events: | |
| st.caption("No moves yet") | |
| st.divider() | |
| # LINEAGE ICICLE - Vertical hierarchy view (complementary to sunburst) | |
| st.markdown("### 🌳 FAMILY TREE") | |
| if lineage_data: | |
| # Build icicle data | |
| ic_ids = [] | |
| ic_parents = [] | |
| ic_values = [] | |
| ic_labels = [] | |
| hash_to_id = {d['quine_hash']: aid for aid, d in lineage_data.items()} | |
| for aid, d in lineage_data.items(): | |
| ic_ids.append(aid) | |
| parent_hash = d.get('parent_hash') | |
| if parent_hash and parent_hash in hash_to_id: | |
| ic_parents.append(hash_to_id[parent_hash]) | |
| else: | |
| ic_parents.append("") | |
| ic_values.append(max(0.1, d['fitness'])) | |
| short_id = aid[-6:] if len(aid) > 6 else aid | |
| ic_labels.append(f"{short_id} G{d['generation']}") | |
| icicle_fig = go.Figure(go.Icicle( | |
| ids=ic_ids, | |
| labels=ic_labels, | |
| parents=ic_parents, | |
| values=ic_values, | |
| branchvalues="remainder", | |
| marker=dict( | |
| colors=[d['generation'] for d in lineage_data.values()], | |
| colorscale='Plasma', | |
| line=dict(width=1, color='#222') | |
| ), | |
| hovertemplate="<b>%{label}</b><br>Fit: %{value:.2f}<extra></extra>", | |
| textfont=dict(size=10, color='white'), | |
| tiling=dict(orientation='v') # Vertical icicle | |
| )) | |
| icicle_fig.update_layout( | |
| margin=dict(t=5, l=5, r=5, b=5), | |
| paper_bgcolor='#0a0a0a', | |
| height=200, | |
| font=dict(color='white'), | |
| clickmode='event+select' # Enable click events | |
| ) | |
| # Render icicle with click handling | |
| icicle_event = st.plotly_chart(icicle_fig, key="lineage_icicle", width="stretch", on_select="rerun", selection_mode="points") | |
| # Handle icicle click - extract agent ID | |
| if icicle_event and hasattr(icicle_event, 'selection') and icicle_event.selection: | |
| sel = icicle_event.selection | |
| if sel.get('points'): | |
| point = sel['points'][0] | |
| # The ID is the agent_id | |
| clicked_id = point.get('id') or point.get('label', '') | |
| if clicked_id and clicked_id in lineage_data: | |
| st.session_state.selected_agent = clicked_id | |
| st.session_state.experience_chain = st.session_state.swarm.get_experience_chain(clicked_id) | |
| st.rerun() | |
| # Summary stats | |
| if sel_agent and sel_agent in lineage_data: | |
| d = lineage_data[sel_agent] | |
| parent = d.get('parent_hash') or 'GENESIS' | |
| same_root = [a for a, dd in lineage_data.items() if dd.get('root_lineage') == d.get('root_lineage')] | |
| st.caption(f"Parent: {parent[:10]} | Family: {len(same_root)} | Pos: ({d['position'][0]:.0f},{d['position'][1]:.0f},{d['position'][2]:.0f})") | |
| else: | |
| roots = set(dd.get('root_lineage', aid) for aid, dd in lineage_data.items()) | |
| st.caption(f"{len(roots)} lineages | {len(lineage_data)} agents") | |
| st.divider() | |
| # PROVENANCE - Shows cascade chain | |
| st.markdown("### 📜 PROVENANCE") | |
| if provenance_receipt: | |
| st.code(f"Session: {provenance_receipt.get('session_id', '?')[:20]}") | |
| st.code(f"Merkle: {provenance_receipt.get('merkle_root', '?')[:20]}") | |
| # Count actual events from session | |
| event_count = len(st.session_state.events) if st.session_state.initialized else 0 | |
| st.text(f"Events logged: {event_count}") | |
| if not provenance_receipt: | |
| st.caption("Cascade bridge active") | |
| st.divider() | |
| # CASCADE LATTICE QUERY - Interactive exploration | |
| st.markdown("### 🔍 LATTICE QUERY") | |
| if CASCADE_AVAILABLE and cascade_store: | |
| # Query interface | |
| query_model = st.text_input("Model filter:", placeholder="e.g. quine_BUZZARD", key="query_model") | |
| query_limit = st.slider("Results:", 1, 20, 5, key="query_limit") | |
| if st.button("🔎 Query", key="run_query"): | |
| try: | |
| results = cascade_store.query( | |
| model_id=query_model if query_model else None, | |
| limit=query_limit | |
| ) | |
| st.session_state.query_results = results | |
| except Exception as e: | |
| st.error(f"Query failed: {e}") | |
| # Display results | |
| if hasattr(st.session_state, 'query_results') and st.session_state.query_results: | |
| for r in st.session_state.query_results: | |
| with st.container(): | |
| st.markdown(f""" | |
| <div style="background: #1a1a2e; border-left: 3px solid #0ff; padding: 8px; margin: 4px 0; border-radius: 4px;"> | |
| <code style="color: #0ff; font-size: 10px;">{r.cid[:24]}...</code><br/> | |
| <span style="color: #888; font-size: 11px;">{r.model_id[:30]}</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.caption("cascade-lattice not available") | |
| st.divider() | |
| # ARCHITECTURE | |
| st.markdown("### 🏗️ ARCHITECTURE") | |
| if arch: | |
| cols = st.columns(2) | |
| cols[0].text(f"Brain: {arch.get('brain_type', '?')}") | |
| cols[0].text(f"LoRA: r{arch.get('lora_rank', '?')} α{arch.get('lora_alpha', '?')}") | |
| cols[1].text(f"Hidden: {arch.get('hidden_size', '?')}") | |
| cols[1].text(f"Latent: {arch.get('latent_dim', '?')}") | |
| if arch.get('has_dreamer'): | |
| st.success("✓ DreamerV3") | |
| if arch.get('has_rssm'): | |
| st.success("✓ RSSM") | |
| else: | |
| st.caption("No model loaded") | |
| else: | |
| st.markdown("## Press START") | |
| st.caption("Initialize simulation first") | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # AUTO-RUN - JS-based refresh (no black flash) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Check for stop requests FIRST | |
| if st.session_state.get('stop_requested', 0) > 0: | |
| st.session_state.auto_run = False | |
| st.session_state.stop_requested = 0 | |
| # Auto-refresh using JS (smoother than st.rerun) | |
| if st.session_state.get('auto_run', False) and st.session_state.get('initialized', False): | |
| step_simulation() | |
| # JS-based refresh - much smoother, no DOM destruction | |
| st_autorefresh(interval=200, limit=None, key="auto_step") | |