Spaces:
Sleeping
Sleeping
| """Aether Garden — main Gradio application.""" | |
| from __future__ import annotations | |
| import os | |
| import tempfile | |
| import uuid | |
| from pathlib import Path | |
| import gradio as gr | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from world.database import get_world_state, init_database | |
| from world.quests import assign_quest, get_entity_quest, get_active_quests | |
| from world.presence import ( | |
| cleanup_stale_sessions, | |
| heartbeat as presence_heartbeat, | |
| get_active_count, | |
| get_active_visitors, | |
| get_recent_arrivals, | |
| ) | |
| from world.entities import ( | |
| check_rate_limit, | |
| get_all_entities, | |
| get_entities_by_location, | |
| ) | |
| from world.locations import get_all_locations, get_location_by_id, update_entity_counts | |
| from ai.generation import generate_entity | |
| from persistence.backup import backup_database, restore_database | |
| from ui import assets | |
| from ui.book_display import render_activity_feed, render_book_of_ages, render_current_event | |
| from ui.entity_card import render_entity_card, render_entity_grid | |
| from ui.explore import ( | |
| place_gallery_items, | |
| place_ids, | |
| random_place_id, | |
| render_room, | |
| render_soul, | |
| soul_gallery, | |
| ) | |
| from ui.diorama import render_diorama | |
| from ui.featured import render_featured_characters, render_world_vignette | |
| from ui.hero import render_hero_banner | |
| from ui.map import render_world_map | |
| from ui.pulse import render_realm_pulse | |
| from ui.relationship_graph import render_relationship_graph | |
| from ui.share import build_soul_card, share_caption | |
| CSS_PATH = Path(__file__).parent / "ui" / "styles.css" | |
| CUSTOM_CSS = CSS_PATH.read_text() if CSS_PATH.exists() else "" | |
| GRADIO_MAJOR = int(gr.__version__.split(".")[0]) | |
| REALM_THEME = gr.themes.Base( | |
| primary_hue=gr.themes.colors.amber, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=[gr.themes.GoogleFont("Libre Baskerville"), "Georgia", "serif"], | |
| ) | |
| FOUNDING_MYTH = ( | |
| "Before the first word was spoken, there was only the Canopy — a vast forest where " | |
| "thoughts grew like trees and dreams fell like rain. Then the Tokens arrived. " | |
| "One thousand of them, each carrying a fragment of something half-remembered. " | |
| "They settled across the land, and from their settling, the Realm was born." | |
| ) | |
| def _init_world(): | |
| restore_database() | |
| init_database() | |
| from world.seed_data import seed_locations | |
| with __import__("world.database", fromlist=["db_session"]).db_session() as conn: | |
| seed_locations(conn) | |
| update_entity_counts() | |
| def get_header_html() -> str: | |
| return render_hero_banner() | |
| def _esc(text: str) -> str: | |
| return ( | |
| (text or "") | |
| .replace("&", "&") | |
| .replace("<", "<") | |
| .replace(">", ">") | |
| .replace('"', """) | |
| ) | |
| def summoning_html(description: str) -> str: | |
| """Immediate, evocative loading state while the Oracle generates a soul.""" | |
| desc = _esc(description.strip()) | |
| if len(desc) > 90: | |
| desc = desc[:90].rstrip() + "…" | |
| whisper = f"“{desc}”" if desc else "something not yet named" | |
| return f""" | |
| <div class="oracle-summoning"> | |
| <div class="oracle-orbit"> | |
| <div class="oracle-ring"></div> | |
| <div class="oracle-ring two"></div> | |
| <div class="oracle-ring three"></div> | |
| <div class="oracle-core"></div> | |
| </div> | |
| <div class="oracle-title">The Oracle is dreaming</div> | |
| <p class="oracle-line">Shaping {whisper} into a soul — giving it a name, | |
| a face, a fear, and a home among the eight places…</p> | |
| <div class="oracle-wait-lines"> | |
| <p>The first breath can take a moment. The Realm is listening.</p> | |
| <p>If the Oracle is waking on the GPU, this can take up to a minute…</p> | |
| <p>The bonfire shifts colour while it waits. Something is being born.</p> | |
| </div> | |
| </div> | |
| """ | |
| def _prewarm_oracle() -> None: | |
| """Fire a tiny background inference so the first real summon is fast. | |
| The model lives on a Modal GPU that cold-starts in ~80s; warming it on app | |
| boot (and the hourly tick keeps it warm) means visitors rarely wait. | |
| """ | |
| if os.environ.get("USE_MOCK_AI", "true").lower() == "true": | |
| return | |
| if not os.environ.get("MODAL_INFERENCE_URL"): | |
| return | |
| def _warm() -> None: | |
| try: | |
| from ai.modal_client import generate | |
| generate("Reply with one word.", "Say: ready", max_new_tokens=4, temperature=0.1) | |
| except Exception: | |
| pass | |
| import threading | |
| threading.Thread(target=_warm, daemon=True).start() | |
| def render_location_panel(location_id: int | None) -> str: | |
| if not location_id: | |
| return '<div class="location-panel"><p class="feed-empty">Click a location on the map to explore.</p></div>' | |
| location = get_location_by_id(location_id) | |
| if not location: | |
| return "" | |
| entities = get_entities_by_location(location_id) | |
| chips = "".join( | |
| f'<span class="location-entity-chip" data-entity="{e["id"]}">{e["display_name"]}</span>' | |
| for e in entities[:8] | |
| ) | |
| more = f'<span class="location-entity-chip">+{len(entities) - 8} more</span>' if len(entities) > 8 else "" | |
| img = assets.location_image_url(location.get("slug")) | |
| img_style = f"background-image: url('{img}');" if img else "" | |
| special = location.get("special_property", "") | |
| return f""" | |
| <div class="location-panel location-panel-animate open"> | |
| <div class="location-panel-banner" style="{img_style}"> | |
| <div class="location-panel-scrim"></div> | |
| <h3 class="location-panel-name">{location['name']}</h3> | |
| </div> | |
| <div class="location-panel-content"> | |
| <p class="location-panel-desc">{location['short_description']}</p> | |
| <p class="location-panel-special">✦ {special}</p> | |
| <p style="color: var(--realm-text-tertiary); font-size: 0.8rem; margin-bottom: 0.75rem;"> | |
| {len(entities)} souls currently here | |
| </p> | |
| <div class="location-entity-list">{chips}{more}</div> | |
| </div> | |
| </div> | |
| """ | |
| _HIDE_CARD = gr.update(value=None, visible=False) | |
| _HIDE_CAPTION = gr.update(value="", visible=False) | |
| def _summon_error(message: str): | |
| return ( | |
| render_world_map(), | |
| get_header_html(), | |
| render_activity_feed(), | |
| f'<div class="realm-error">{message}</div>', | |
| render_location_panel(None), | |
| _HIDE_CARD, | |
| _HIDE_CAPTION, | |
| "", | |
| ) | |
| def summon_entity(description: str, session_id: str): | |
| if not description or not description.strip(): | |
| return _summon_error("Describe what you are bringing into the world.") | |
| allowed, remaining = check_rate_limit(session_id) | |
| if not allowed: | |
| return _summon_error( | |
| f"The Realm takes time to absorb new arrivals. Return in {remaining} minutes." | |
| ) | |
| entity, error = generate_entity(description.strip(), session_id) | |
| if error: | |
| return _summon_error(error) | |
| try: | |
| backup_database() | |
| except Exception: | |
| pass | |
| card = render_entity_card(entity) | |
| card_img = build_soul_card(entity) | |
| if card_img: | |
| card_update = gr.update(value=card_img, visible=True) | |
| caption_update = gr.update(value=share_caption(entity), visible=True) | |
| else: | |
| card_update = _HIDE_CARD | |
| caption_update = _HIDE_CAPTION | |
| return ( | |
| render_world_map(entity["location_id"]), | |
| get_header_html(), | |
| render_activity_feed(), | |
| card, | |
| render_location_panel(entity["location_id"]), | |
| card_update, | |
| caption_update, | |
| render_diorama(entity["location_id"]), | |
| ) | |
| def select_entity_from_dropdown(entity_name: str): | |
| if not entity_name: | |
| return '<div class="feed-empty">Select an entity to view their profile.</div>' | |
| entities = get_all_entities(search=entity_name, limit=1) | |
| if not entities: | |
| all_ents = get_all_entities() | |
| match = next((e for e in all_ents if e["display_name"] == entity_name or e["name"] == entity_name), None) | |
| if not match: | |
| return '<div class="feed-empty">Entity not found.</div>' | |
| return render_entity_card(match) | |
| return render_entity_card(entities[0]) | |
| def filter_entities(location_filter, type_filter, status_filter, search): | |
| loc_id = None | |
| if location_filter and location_filter != "all": | |
| locs = get_all_locations() | |
| match = next((l for l in locs if l["name"] == location_filter), None) | |
| if match: | |
| loc_id = match["id"] | |
| entities = get_all_entities( | |
| location_id=loc_id, | |
| entity_type=type_filter, | |
| status=status_filter, | |
| search=search or None, | |
| ) | |
| return render_entity_grid(entities) | |
| def filter_book(entry_type, search): | |
| return render_book_of_ages(entry_type=entry_type, search=search) | |
| def chat_with_soul(soul_id: str, player_message: str) -> str: | |
| """AI response from a soul given a visitor's message. Called from diorama iframe.""" | |
| if not soul_id or not player_message or not player_message.strip(): | |
| return "" | |
| from world.entities import get_entity | |
| from ai.modal_client import generate | |
| from ai.prompts import SOUL_CONVERSATION_SYSTEM, SOUL_CONVERSATION_USER | |
| entity = get_entity(soul_id) | |
| if not entity: | |
| return "The soul has wandered beyond reach." | |
| user_prompt = SOUL_CONVERSATION_USER.format( | |
| entity_name=entity.get("display_name", entity.get("name", "Unknown")), | |
| entity_type=entity.get("type", "soul"), | |
| entity_appearance=entity.get("appearance", ""), | |
| entity_traits=", ".join(entity.get("personality_traits") or []), | |
| entity_primary_goal=entity.get("primary_goal", ""), | |
| entity_secondary_goal=entity.get("secondary_goal", ""), | |
| entity_primary_fear=entity.get("primary_fear", ""), | |
| entity_speech_style=entity.get("speech_style", ""), | |
| entity_memory_summary=entity.get("memory_summary") or "No memories yet.", | |
| entity_days=entity.get("days_in_realm", 0), | |
| entity_status=entity.get("status", "active"), | |
| player_message=player_message.strip()[:300], | |
| ) | |
| try: | |
| response = generate( | |
| user_prompt=user_prompt, | |
| system_prompt=SOUL_CONVERSATION_SYSTEM, | |
| max_new_tokens=180, | |
| temperature=0.88, | |
| ) | |
| return response.strip() if response else "..." | |
| except Exception: | |
| # Fallback to greeting if AI unavailable | |
| return entity.get("greeting") or "..." | |
| def assign_quest_api(entity_id: str, quest_title: str) -> str: | |
| """Assign a quest to a soul. Called from the diorama iframe via Gradio API.""" | |
| if not entity_id or not quest_title or not quest_title.strip(): | |
| return "" | |
| try: | |
| from world.entities import get_entity | |
| entity = get_entity(entity_id) | |
| if not entity: | |
| return "Soul not found." | |
| q = assign_quest(entity_id, quest_title.strip()[:200]) | |
| return f"Quest assigned: {q['title']}" | |
| except Exception as e: | |
| return f"Could not assign quest: {e}" | |
| def get_live_presence(session_id: str) -> str: | |
| """Heartbeat + return live visitor HTML.""" | |
| try: | |
| presence_heartbeat(session_id) | |
| count = get_active_count() | |
| arrivals = get_recent_arrivals(since_seconds=120) | |
| parts = [] | |
| if count > 1: | |
| parts.append(f'<span class="presence-count">⬡ {count} visitors in the Realm right now</span>') | |
| if arrivals: | |
| names = ", ".join(a["display_name"] for a in arrivals[:3]) | |
| parts.append(f'<span class="presence-arrivals">Recently summoned: {names}</span>') | |
| if not parts: | |
| return "" | |
| return f'<div class="presence-bar">{" · ".join(parts)}</div>' | |
| except Exception: | |
| return "" | |
| def diorama_presence_api(location_id, visitor_id: str, x, z, yaw) -> dict: | |
| """Store a visitor's in-scene position and return nearby visitors.""" | |
| try: | |
| loc_id = int(float(location_id)) | |
| visitor = (visitor_id or "").strip()[:80] | |
| if not visitor: | |
| return {"visitors": []} | |
| px = max(-24.0, min(24.0, float(x or 0))) | |
| pz = max(-24.0, min(24.0, float(z or 0))) | |
| pyaw = float(yaw or 0) | |
| short = visitor.replace("aether-", "").replace("_", "-")[-4:].upper() | |
| presence_heartbeat( | |
| visitor, | |
| loc_id, | |
| x=px, | |
| z=pz, | |
| yaw=pyaw, | |
| display_name=f"Visitor {short}", | |
| ) | |
| cleanup_stale_sessions(max_age_seconds=300) | |
| return { | |
| "visitors": get_active_visitors( | |
| loc_id, | |
| exclude_session_id=visitor, | |
| window_seconds=22, | |
| ) | |
| } | |
| except Exception: | |
| return {"visitors": []} | |
| def trigger_live_tick() -> str: | |
| """Run one simulation tick on demand and return a summary.""" | |
| try: | |
| from simulation.tick import execute_simulation_tick | |
| result = execute_simulation_tick() | |
| day = result.get("day", "?") | |
| interactions = result.get("interactions_run", 0) | |
| event = result.get("world_event_title", "a quiet moment") | |
| return ( | |
| f'<div class="tick-result">' | |
| f'<p class="tick-title">✦ Day {day} advanced</p>' | |
| f'<p class="tick-body">The world event: <em>{event}</em></p>' | |
| f'<p class="tick-body">{interactions} soul encounter{"s" if interactions != 1 else ""} unfolded.</p>' | |
| f'</div>' | |
| ) | |
| except Exception as e: | |
| return f'<div class="realm-error">Tick failed: {e}</div>' | |
| def refresh_realm_view(): | |
| return refresh_all_views() | |
| def refresh_all_views(): | |
| return ( | |
| render_world_map(), | |
| get_header_html(), | |
| render_current_event(), | |
| render_activity_feed(), | |
| render_world_vignette(), | |
| render_featured_characters(), | |
| render_realm_pulse(), | |
| ) | |
| _SOUL_HINT = '<div class="feed-empty">Click a soul above to meet them.</div>' | |
| def enter_place(evt: gr.SelectData): | |
| ids = place_ids() | |
| if evt.index is None or evt.index >= len(ids): | |
| return None, render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT | |
| loc_id = ids[evt.index] | |
| items, soul_ids = soul_gallery(loc_id) | |
| return loc_id, render_diorama(loc_id), render_room(loc_id), gr.update(value=items), soul_ids, _SOUL_HINT | |
| def wander_to_place(): | |
| loc_id = random_place_id() | |
| if not loc_id: | |
| return None, render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT | |
| items, soul_ids = soul_gallery(loc_id) | |
| return loc_id, render_diorama(loc_id), render_room(loc_id), gr.update(value=items), soul_ids, _SOUL_HINT | |
| def open_diorama_from_map(location_id: int | None): | |
| if not location_id: | |
| return render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT | |
| items, soul_ids = soul_gallery(location_id) | |
| return render_diorama(location_id), render_room(location_id), gr.update(value=items), soul_ids, _SOUL_HINT | |
| def meet_soul(evt: gr.SelectData, soul_ids: list): | |
| if not soul_ids or evt.index is None or evt.index >= len(soul_ids): | |
| return '<div class="feed-empty">They have wandered off.</div>' | |
| return render_soul(soul_ids[evt.index], soul_ids) | |
| def build_app() -> gr.Blocks: | |
| _init_world() | |
| _prewarm_oracle() | |
| blocks_kwargs: dict = {"title": "Aether Garden"} | |
| if GRADIO_MAJOR < 6: | |
| blocks_kwargs["css"] = CUSTOM_CSS | |
| blocks_kwargs["theme"] = REALM_THEME | |
| with gr.Blocks(**blocks_kwargs) as demo: | |
| session_id = gr.State(value=str(uuid.uuid4())) | |
| presence_bar = gr.HTML(value="", elem_id="presence-bar") | |
| header = gr.HTML(value=get_header_html(), elem_classes=["realm-header-wrap"]) | |
| with gr.Tabs(elem_classes=["realm-tabs"]) as tabs: | |
| with gr.Tab("The Realm", id="realm"): | |
| world_vignette = gr.HTML(value=render_world_vignette()) | |
| realm_pulse = gr.HTML(value=render_realm_pulse()) | |
| with gr.Accordion("How this world works", open=False): | |
| gr.HTML( | |
| '<div class="how-it-works">' | |
| '<p><b>1 · You summon.</b> Describe anything below and an AI gives it a name, ' | |
| 'a look, goals, fears, and a home among the eight places.</p>' | |
| '<p><b>2 · The world lives on its own.</b> Every hour, even when no one is here, ' | |
| 'the souls wander, meet, form bonds, and the land throws up strange events.</p>' | |
| '<p><b>3 · Nothing is forgotten.</b> Every arrival, meeting, and event is written ' | |
| 'into the Book of Ages forever. Open the <b>Explore</b> tab to step inside a place ' | |
| 'and meet who lives there.</p>' | |
| '</div>' | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| world_map = gr.HTML( | |
| value=render_world_map(), | |
| elem_id="world-map", | |
| ) | |
| location_panel = gr.HTML( | |
| value=render_location_panel(None), | |
| ) | |
| realm_diorama = gr.HTML(value="") | |
| map_pick = gr.Textbox(visible=False, elem_id="map_location_pick") | |
| with gr.Column(scale=2, elem_classes=["sidebar-panel"]): | |
| current_event = gr.HTML(value=render_current_event()) | |
| activity_feed = gr.HTML(value=render_activity_feed()) | |
| with gr.Accordion("⚡ Advance the World Now", open=False): | |
| gr.HTML('<p style="color:var(--realm-text-tertiary);font-size:0.82rem;margin-bottom:0.6rem;">Manually trigger one simulation tick — the world will generate a new event, run soul encounters, and update memories in real time.</p>') | |
| live_tick_btn = gr.Button("Run simulation tick →", variant="secondary", size="sm") | |
| tick_result = gr.HTML() | |
| gr.HTML('<div class="summon-section"><h3 class="summon-title">Summon Something Into the Realm</h3></div>') | |
| with gr.Row(): | |
| summon_input = gr.Textbox( | |
| placeholder="Describe what you are bringing into the world...", | |
| label=None, | |
| show_label=False, | |
| lines=2, | |
| scale=4, | |
| ) | |
| summon_btn = gr.Button("Summon →", variant="primary", scale=1) | |
| gr.HTML( | |
| '<p class="summon-hint">Hint: "A wizard" is forgotten. ' | |
| '"A wizard who collects the sound of doors closing" is remembered.</p>' | |
| ) | |
| entity_result = gr.HTML() | |
| with gr.Group(visible=True): | |
| soul_card_img = gr.Image( | |
| label="Your Soul Card — download & share it", | |
| visible=False, | |
| show_label=True, | |
| interactive=False, | |
| type="filepath", | |
| elem_classes=["soul-card-image"], | |
| height=420, | |
| ) | |
| share_caption_box = gr.Textbox( | |
| label="Share caption (copy this with your card)", | |
| visible=False, | |
| lines=3, | |
| interactive=True, | |
| ) | |
| location_select = gr.Dropdown( | |
| choices=[(l["name"], l["id"]) for l in get_all_locations()], | |
| label="Explore Location", | |
| visible=True, | |
| ) | |
| featured_chars = gr.HTML(value=render_featured_characters()) | |
| with gr.Tab("Explore", id="places"): | |
| _all_locs = get_all_locations() | |
| _default_loc_id = _all_locs[0]["id"] if _all_locs else None | |
| explore_loc = gr.State(value=_default_loc_id) | |
| soul_ids_state = gr.State(value=[]) | |
| gr.HTML( | |
| '<div class="explore-intro">' | |
| '<h2 class="section-title">Walk the Eight Sacred Places</h2>' | |
| '<p class="section-desc">Pick a place below — then <strong>WASD to walk</strong>, ' | |
| '<strong>drag to look</strong>, <strong>E to talk</strong> to any soul you find. ' | |
| 'Other live visitors appear as golden wisps when they step into the same place. ' | |
| 'Or press ✦ to wander somewhere at random.</p>' | |
| '</div>' | |
| ) | |
| wander_btn = gr.Button("✦ Wander somewhere at random", variant="secondary") | |
| places_gallery = gr.Gallery( | |
| value=place_gallery_items(), | |
| columns=4, | |
| height="auto", | |
| object_fit="cover", | |
| allow_preview=False, | |
| show_label=False, | |
| elem_classes=["places-gallery"], | |
| ) | |
| diorama_view = gr.HTML(value=render_diorama(_default_loc_id)) | |
| portal_pick = gr.Textbox(visible=False, elem_id="portal_location_pick") | |
| with gr.Accordion("Classic view (if 3D does not load)", open=False): | |
| room_view = gr.HTML(value=render_room(_default_loc_id)) | |
| souls_gallery = gr.Gallery( | |
| value=[], | |
| columns=6, | |
| height="auto", | |
| object_fit="cover", | |
| allow_preview=False, | |
| label="Souls dwelling here", | |
| elem_classes=["souls-gallery"], | |
| ) | |
| soul_card = gr.HTML() | |
| with gr.Tab("Book of Ages", id="book"): | |
| with gr.Row(): | |
| book_filter = gr.Dropdown( | |
| choices=[ | |
| ("All", "all"), | |
| ("Arrivals", "arrival"), | |
| ("Interactions", "interaction"), | |
| ("Events", "world_event"), | |
| ("Milestones", "milestone"), | |
| ("Dreams", "dream_fragment"), | |
| ], | |
| value="all", | |
| label="Filter", | |
| scale=1, | |
| ) | |
| book_search = gr.Textbox( | |
| placeholder="Search by name...", | |
| label="Search", | |
| scale=2, | |
| ) | |
| book_display = gr.HTML(value=render_book_of_ages()) | |
| with gr.Tab("Bonds", id="bonds"): | |
| gr.HTML( | |
| '<div class="explore-intro">' | |
| '<h2 class="section-title">The Web of Bonds</h2>' | |
| '<p class="section-desc">Every alliance, rivalry, and debt forged by the simulation — ' | |
| 'not scripted, only remembered.</p></div>' | |
| ) | |
| rel_graph = gr.HTML(value=render_relationship_graph()) | |
| with gr.Tab("All Entities", id="entities"): | |
| with gr.Row(): | |
| loc_filter = gr.Dropdown( | |
| choices=["all"] + [l["name"] for l in get_all_locations()], | |
| value="all", | |
| label="Location", | |
| ) | |
| type_filter = gr.Dropdown( | |
| choices=["all", "character", "creature", "object", "place"], | |
| value="all", | |
| label="Type", | |
| ) | |
| status_filter = gr.Dropdown( | |
| choices=["all", "active", "dormant", "legendary"], | |
| value="all", | |
| label="Status", | |
| ) | |
| entity_search = gr.Textbox(placeholder="Search...", label="Search") | |
| entity_grid = gr.HTML(value=render_entity_grid(get_all_entities())) | |
| entity_detail = gr.HTML() | |
| entity_names = gr.Dropdown( | |
| choices=[e["display_name"] for e in get_all_entities()], | |
| label="View Entity Profile", | |
| ) | |
| _summon_outputs = [ | |
| world_map, header, activity_feed, entity_result, location_panel, | |
| soul_card_img, share_caption_box, realm_diorama, | |
| ] | |
| summon_btn.click( | |
| fn=summoning_html, | |
| inputs=[summon_input], | |
| outputs=[entity_result], | |
| queue=False, | |
| ).then( | |
| fn=lambda: (_HIDE_CARD, _HIDE_CAPTION), | |
| outputs=[soul_card_img, share_caption_box], | |
| queue=False, | |
| ).then( | |
| fn=summon_entity, | |
| inputs=[summon_input, session_id], | |
| outputs=_summon_outputs, | |
| ).then(fn=lambda: "", outputs=[summon_input]).then( | |
| None, None, None, | |
| js=""" | |
| () => { | |
| setTimeout(() => { | |
| const card = document.querySelector('.soul-card-image'); | |
| const diorama = document.querySelector('.diorama-wrap'); | |
| const target = card || diorama || document.querySelector('.entity-card'); | |
| if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| }, 350); | |
| } | |
| """, | |
| ) | |
| summon_input.submit( | |
| fn=summoning_html, | |
| inputs=[summon_input], | |
| outputs=[entity_result], | |
| queue=False, | |
| ).then( | |
| fn=lambda: (_HIDE_CARD, _HIDE_CAPTION), | |
| outputs=[soul_card_img, share_caption_box], | |
| queue=False, | |
| ).then( | |
| fn=summon_entity, | |
| inputs=[summon_input, session_id], | |
| outputs=_summon_outputs, | |
| ).then(fn=lambda: "", outputs=[summon_input]).then( | |
| None, None, None, | |
| js=""" | |
| () => { | |
| setTimeout(() => { | |
| const card = document.querySelector('.soul-card-image'); | |
| const diorama = document.querySelector('.diorama-wrap'); | |
| const target = card || diorama || document.querySelector('.entity-card'); | |
| if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| }, 350); | |
| } | |
| """, | |
| ) | |
| def _realm_location_change(loc_id): | |
| d = render_diorama(loc_id) if loc_id else "" | |
| return render_world_map(loc_id), render_location_panel(loc_id), d | |
| location_select.change( | |
| fn=_realm_location_change, | |
| inputs=[location_select], | |
| outputs=[world_map, location_panel, realm_diorama], | |
| ) | |
| def _map_open_diorama(pick): | |
| try: | |
| loc_id = int(pick) if pick else None | |
| except (TypeError, ValueError): | |
| loc_id = None | |
| if not loc_id: | |
| return "", render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT | |
| items, soul_ids = soul_gallery(loc_id) | |
| return ( | |
| render_diorama(loc_id), | |
| render_diorama(loc_id), | |
| render_room(loc_id), | |
| gr.update(value=items), | |
| soul_ids, | |
| _SOUL_HINT, | |
| ) | |
| map_pick.change( | |
| _map_open_diorama, | |
| inputs=[map_pick], | |
| outputs=[realm_diorama, diorama_view, room_view, souls_gallery, soul_ids_state, soul_card], | |
| ).then( | |
| lambda pick: int(pick) if pick and str(pick).isdigit() else None, | |
| inputs=[map_pick], | |
| outputs=[explore_loc], | |
| ) | |
| portal_pick.change( | |
| open_diorama_from_map, | |
| inputs=[portal_pick], | |
| outputs=[diorama_view, room_view, souls_gallery, soul_ids_state, soul_card], | |
| ).then( | |
| lambda pick: int(pick) if pick and str(pick).isdigit() else None, | |
| inputs=[portal_pick], | |
| outputs=[explore_loc], | |
| ) | |
| places_gallery.select( | |
| enter_place, | |
| inputs=None, | |
| outputs=[explore_loc, diorama_view, room_view, souls_gallery, soul_ids_state, soul_card], | |
| ) | |
| wander_btn.click( | |
| wander_to_place, | |
| inputs=None, | |
| outputs=[explore_loc, diorama_view, room_view, souls_gallery, soul_ids_state, soul_card], | |
| ) | |
| souls_gallery.select(meet_soul, inputs=[soul_ids_state], outputs=[soul_card]) | |
| # ── Live tick button ── | |
| live_tick_btn.click( | |
| fn=trigger_live_tick, | |
| outputs=[tick_result], | |
| ).then( | |
| fn=refresh_all_views, | |
| outputs=[world_map, header, current_event, activity_feed, world_vignette, featured_chars, realm_pulse], | |
| ) | |
| # ── Soul chat API ── | |
| chat_soul_id_comp = gr.Textbox(visible=False, elem_id="chat_soul_id_input") | |
| chat_msg_comp = gr.Textbox(visible=False, elem_id="chat_message_input") | |
| chat_resp_comp = gr.Textbox(visible=False, elem_id="chat_response_output") | |
| chat_msg_comp.change( | |
| fn=chat_with_soul, | |
| inputs=[chat_soul_id_comp, chat_msg_comp], | |
| outputs=[chat_resp_comp], | |
| api_name="chat_with_soul", | |
| ) | |
| # ── Quest API ── | |
| quest_entity_comp = gr.Textbox(visible=False, elem_id="quest_entity_input") | |
| quest_title_comp = gr.Textbox(visible=False, elem_id="quest_title_input") | |
| quest_resp_comp = gr.Textbox(visible=False, elem_id="quest_response_output") | |
| quest_title_comp.change( | |
| fn=assign_quest_api, | |
| inputs=[quest_entity_comp, quest_title_comp], | |
| outputs=[quest_resp_comp], | |
| api_name="assign_quest", | |
| ) | |
| # ── Diorama multiplayer presence API ── | |
| presence_loc_comp = gr.Textbox(visible=False, elem_id="presence_location_input") | |
| presence_visitor_comp = gr.Textbox(visible=False, elem_id="presence_visitor_input") | |
| presence_x_comp = gr.Textbox(visible=False, elem_id="presence_x_input") | |
| presence_z_comp = gr.Textbox(visible=False, elem_id="presence_z_input") | |
| presence_yaw_comp = gr.Textbox(visible=False, elem_id="presence_yaw_input") | |
| presence_json_comp = gr.JSON(visible=False, elem_id="presence_json_output") | |
| presence_yaw_comp.change( | |
| fn=diorama_presence_api, | |
| inputs=[ | |
| presence_loc_comp, | |
| presence_visitor_comp, | |
| presence_x_comp, | |
| presence_z_comp, | |
| presence_yaw_comp, | |
| ], | |
| outputs=[presence_json_comp], | |
| api_name="diorama_presence", | |
| ) | |
| # ── Presence heartbeat (every 10s) ── | |
| presence_timer = gr.Timer(10) | |
| presence_timer.tick( | |
| fn=get_live_presence, | |
| inputs=[session_id], | |
| outputs=[presence_bar], | |
| ) | |
| book_filter.change(filter_book, [book_filter, book_search], [book_display]) | |
| book_search.change(filter_book, [book_filter, book_search], [book_display]) | |
| for f in [loc_filter, type_filter, status_filter, entity_search]: | |
| f.change( | |
| filter_entities, | |
| [loc_filter, type_filter, status_filter, entity_search], | |
| [entity_grid], | |
| ) | |
| entity_names.change(select_entity_from_dropdown, [entity_names], [entity_detail]) | |
| refresh_timer = gr.Timer(30) | |
| refresh_timer.tick( | |
| fn=refresh_all_views, | |
| outputs=[world_map, header, current_event, activity_feed, world_vignette, featured_chars, realm_pulse], | |
| ).then(fn=render_relationship_graph, outputs=[rel_graph]) | |
| MAP_CLICK_JS = """ | |
| () => { | |
| const bind = () => { | |
| document.querySelectorAll('#world-map [data-location-id]').forEach((node) => { | |
| if (node.dataset.mapBound) return; | |
| node.dataset.mapBound = '1'; | |
| node.style.cursor = 'pointer'; | |
| node.addEventListener('click', () => { | |
| const id = node.getAttribute('data-location-id'); | |
| const root = document.querySelector('#map_location_pick'); | |
| const input = root && (root.querySelector('textarea') || root.querySelector('input')); | |
| if (input && id) { | |
| input.value = id; | |
| input.dispatchEvent(new Event('input', { bubbles: true })); | |
| } | |
| }); | |
| }); | |
| }; | |
| bind(); | |
| new MutationObserver(bind).observe(document.body, { childList: true, subtree: true }); | |
| window._aetherHoverDiorama = false; | |
| window._aetherDioramaActive = false; | |
| window._aetherActiveFrame = null; | |
| const GAME_KEYS = new Set([ | |
| 'KeyW','KeyA','KeyS','KeyD', | |
| 'ArrowUp','ArrowDown','ArrowLeft','ArrowRight', | |
| 'KeyE','KeyQ','ShiftLeft','ShiftRight','Escape' | |
| ]); | |
| const getVisibleDioramaFrame = () => { | |
| const frames = Array.from(document.querySelectorAll('iframe.diorama-frame')); | |
| let best = null; | |
| let bestArea = 0; | |
| for (const frame of frames) { | |
| const rect = frame.getBoundingClientRect(); | |
| if (rect.width < 40 || rect.height < 40) continue; | |
| const area = rect.width * rect.height; | |
| if (area > bestArea) { | |
| bestArea = area; | |
| best = frame; | |
| } | |
| } | |
| return best || frames[frames.length - 1] || null; | |
| }; | |
| const analogFromKeys = (keySet) => { | |
| let fwd = 0, strafe = 0; | |
| if (keySet.has('KeyW') || keySet.has('ArrowUp')) fwd += 1; | |
| if (keySet.has('KeyS') || keySet.has('ArrowDown')) fwd -= 1; | |
| if (keySet.has('KeyA') || keySet.has('ArrowLeft')) strafe -= 1; | |
| if (keySet.has('KeyD') || keySet.has('ArrowRight')) strafe += 1; | |
| return { fwd, strafe }; | |
| }; | |
| const sendAnalogToFrame = (frame, fwd, strafe) => { | |
| if (!frame) return; | |
| try { | |
| frame.contentWindow?.postMessage({ type: 'aether_analog', fwd, strafe, fromParent: true }, '*'); | |
| } catch (_) {} | |
| }; | |
| const sendKeysToFrame = (frame, codes) => { | |
| if (!frame) return; | |
| const set = new Set(codes); | |
| try { | |
| frame.contentWindow?.postMessage({ type: 'aether_keys', keys: codes, fromParent: true }, '*'); | |
| } catch (_) {} | |
| const { fwd, strafe } = analogFromKeys(set); | |
| sendAnalogToFrame(frame, fwd, strafe); | |
| }; | |
| const sendKeyToFrame = (frame, code, action) => { | |
| if (!frame) return; | |
| try { | |
| frame.contentWindow?.postMessage({ type: 'aether_key', code, action }, '*'); | |
| } catch (_) {} | |
| }; | |
| window._aetherHeldKeys = window._aetherHeldKeys || new Set(); | |
| window._aetherJoyAnalog = { fwd: 0, strafe: 0 }; | |
| window._aetherParentDriving = false; | |
| const MOVE_KEY_SET = new Set([ | |
| 'KeyW','KeyA','KeyS','KeyD', | |
| 'ArrowUp','ArrowDown','ArrowLeft','ArrowRight' | |
| ]); | |
| const ACTION_KEY_SET = new Set(['KeyE','KeyQ','Escape','ShiftLeft','ShiftRight']); | |
| const padHeldForWrap = (wrap) => { | |
| if (!wrap) return window._aetherHeldKeys; | |
| if (!wrap._aetherPadHeld) wrap._aetherPadHeld = new Set(); | |
| return wrap._aetherPadHeld; | |
| }; | |
| const mergedHeldKeys = (wrap) => { | |
| const pad = wrap ? padHeldForWrap(wrap) : new Set(); | |
| return [...new Set([...window._aetherHeldKeys, ...pad])]; | |
| }; | |
| window.aetherWalk = (btn, down) => { | |
| const wrap = btn?.closest?.('.diorama-wrap'); | |
| const frame = wrap?.querySelector('iframe.diorama-frame') || getVisibleDioramaFrame(); | |
| if (!frame || !btn) return; | |
| window._aetherActiveFrame = frame; | |
| const code = btn.dataset?.key; | |
| if (!code) return; | |
| const padHeld = padHeldForWrap(wrap); | |
| if (down) { | |
| btn.classList.add('pressed'); | |
| padHeld.add(code); | |
| } else { | |
| btn.classList.remove('pressed'); | |
| padHeld.delete(code); | |
| } | |
| window._aetherParentDriving = true; | |
| sendKeysToFrame(frame, mergedHeldKeys(wrap)); | |
| }; | |
| const wireDioramaFrames = () => { | |
| document.querySelectorAll('.diorama-wrap').forEach((wrap) => { | |
| if (wrap.dataset.dioramaWrapBound) return; | |
| wrap.dataset.dioramaWrapBound = '1'; | |
| const frame = wrap.querySelector('iframe.diorama-frame'); | |
| const activate = () => { | |
| window._aetherHoverDiorama = true; | |
| window._aetherActiveFrame = frame || window._aetherActiveFrame; | |
| if (frame) { | |
| try { frame.focus({ preventScroll: true }); } catch (_) {} | |
| try { frame.contentWindow?.postMessage({ type: 'aether_activate' }, '*'); } catch (_) {} | |
| } | |
| }; | |
| wrap.addEventListener('mouseenter', activate); | |
| wrap.addEventListener('mousemove', () => { | |
| window._aetherHoverDiorama = true; | |
| window._aetherActiveFrame = frame || window._aetherActiveFrame; | |
| }); | |
| wrap.addEventListener('mouseleave', () => { | |
| if (!window._aetherDioramaActive) window._aetherHoverDiorama = false; | |
| }); | |
| wrap.addEventListener('mousedown', activate); | |
| wrap.addEventListener('click', activate); | |
| }); | |
| document.querySelectorAll('iframe.diorama-frame').forEach((frame) => { | |
| if (frame.dataset.dioramaBound) return; | |
| frame.dataset.dioramaBound = '1'; | |
| frame.setAttribute('tabindex', '0'); | |
| window._aetherActiveFrame = frame; | |
| frame.addEventListener('load', () => { | |
| window._aetherActiveFrame = frame; | |
| setTimeout(() => { try { frame.contentWindow?.postMessage('diorama-resize', '*'); } catch (_) {} }, 120); | |
| setTimeout(() => { try { frame.contentWindow?.postMessage('diorama-resize', '*'); } catch (_) {} }, 600); | |
| setTimeout(() => { try { frame.contentWindow?.postMessage({ type: 'aether_activate' }, '*'); } catch (_) {} }, 200); | |
| }); | |
| }); | |
| }; | |
| const wireRemotePadButtons = () => { | |
| if (window._aetherPadDelegationBound) return; | |
| window._aetherPadDelegationBound = true; | |
| const findBtn = (e) => e.target?.closest?.('.drc-btn'); | |
| document.addEventListener('pointerdown', (e) => { | |
| const btn = findBtn(e); | |
| if (!btn) return; | |
| e.preventDefault(); | |
| btn._aetherPointerId = e.pointerId; | |
| try { btn.setPointerCapture(e.pointerId); } catch (_) {} | |
| window.aetherWalk(btn, true); | |
| }, true); | |
| const releaseAllPadButtons = () => { | |
| document.querySelectorAll('.drc-btn.pressed').forEach((btn) => { | |
| window.aetherWalk(btn, false); | |
| }); | |
| }; | |
| document.addEventListener('pointerup', releaseAllPadButtons, true); | |
| document.addEventListener('pointercancel', releaseAllPadButtons, true); | |
| document.addEventListener('lostpointercapture', releaseAllPadButtons, true); | |
| }; | |
| const wireRemoteJoysticks = () => { | |
| document.querySelectorAll('.drc-joy').forEach((zone) => { | |
| if (zone.dataset.drcJoyBound) return; | |
| zone.dataset.drcJoyBound = '1'; | |
| const knob = zone.querySelector('.drc-knob'); | |
| let active = false, cx = 0, cy = 0; | |
| const frameFor = () => | |
| zone.closest('.diorama-wrap')?.querySelector('iframe.diorama-frame') | |
| || getVisibleDioramaFrame(); | |
| const move = (clientX, clientY) => { | |
| const dx = clientX - cx, dy = clientY - cy; | |
| const dist = Math.min(Math.hypot(dx, dy), 52); | |
| const ang = Math.atan2(dy, dx); | |
| if (knob) knob.style.transform = `translate(${Math.cos(ang) * dist}px, ${Math.sin(ang) * dist}px)`; | |
| const mag = dist > 10 ? 1 : 0; | |
| const strafe = mag ? Math.cos(ang) : 0; | |
| const fwd = mag ? -Math.sin(ang) : 0; | |
| window._aetherJoyAnalog = { fwd, strafe }; | |
| const frame = frameFor(); | |
| if (frame) { | |
| window._aetherActiveFrame = frame; | |
| sendAnalogToFrame(frame, fwd, strafe); | |
| } | |
| }; | |
| const end = () => { | |
| active = false; | |
| if (knob) knob.style.transform = ''; | |
| window._aetherJoyAnalog = { fwd: 0, strafe: 0 }; | |
| const frame = frameFor(); | |
| if (frame) sendAnalogToFrame(frame, 0, 0); | |
| }; | |
| zone.addEventListener('pointerdown', (e) => { | |
| e.preventDefault(); | |
| active = true; | |
| const r = zone.getBoundingClientRect(); | |
| cx = r.left + r.width / 2; | |
| cy = r.top + r.height / 2; | |
| zone.setPointerCapture(e.pointerId); | |
| move(e.clientX, e.clientY); | |
| }); | |
| zone.addEventListener('pointermove', (e) => { | |
| if (!active) return; | |
| e.preventDefault(); | |
| move(e.clientX, e.clientY); | |
| }); | |
| zone.addEventListener('pointerup', end); | |
| zone.addEventListener('pointercancel', end); | |
| zone.addEventListener('pointerleave', end); | |
| }); | |
| }; | |
| wireDioramaFrames(); | |
| wireRemotePadButtons(); | |
| wireRemoteJoysticks(); | |
| new MutationObserver(() => { | |
| wireDioramaFrames(); | |
| wireRemoteJoysticks(); | |
| }).observe(document.body, { childList: true, subtree: true }); | |
| if (!window._aetherKeyForwardBound) { | |
| window._aetherKeyForwardBound = true; | |
| const typingInForm = () => { | |
| const el = document.activeElement; | |
| if (!el) return false; | |
| const tag = el.tagName; | |
| if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true; | |
| if (el.isContentEditable) return true; | |
| return !!el.closest?.('textarea, input, [contenteditable="true"]'); | |
| }; | |
| const getActiveFrame = () => { | |
| const visible = getVisibleDioramaFrame(); | |
| if (visible) return visible; | |
| const padWrap = document.querySelector('.drc-btn.pressed')?.closest('.diorama-wrap'); | |
| if (padWrap) { | |
| const padFrame = padWrap.querySelector('iframe.diorama-frame'); | |
| if (padFrame) return padFrame; | |
| } | |
| const registered = window._aetherActiveFrame; | |
| if (registered) { | |
| const rect = registered.getBoundingClientRect(); | |
| if (rect.width > 40 && rect.height > 40) return registered; | |
| } | |
| return null; | |
| }; | |
| const forwardKey = (e, action) => { | |
| if (!GAME_KEYS.has(e.code)) return; | |
| if (typingInForm()) return; | |
| const frame = getActiveFrame(); | |
| if (!frame) return; | |
| e.preventDefault(); | |
| e.stopImmediatePropagation(); | |
| if (ACTION_KEY_SET.has(e.code)) { | |
| if (action === 'down' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') { | |
| sendKeyToFrame(frame, e.code, action); | |
| } | |
| return; | |
| } | |
| if (!MOVE_KEY_SET.has(e.code)) return; | |
| sendKeyToFrame(frame, e.code, action); | |
| if (action === 'down') window._aetherHeldKeys.add(e.code); | |
| else window._aetherHeldKeys.delete(e.code); | |
| window._aetherParentDriving = true; | |
| sendKeysToFrame(frame, [...window._aetherHeldKeys]); | |
| }; | |
| window.addEventListener('keydown', (e) => forwardKey(e, 'down'), true); | |
| window.addEventListener('keyup', (e) => forwardKey(e, 'up'), true); | |
| // Sync movement to the visible diorama iframe continuously. | |
| setInterval(() => { | |
| if (typingInForm()) return; | |
| const frame = getActiveFrame(); | |
| if (!frame) return; | |
| const joy = window._aetherJoyAnalog || { fwd: 0, strafe: 0 }; | |
| const padWrap = document.querySelector('.drc-btn.pressed')?.closest('.diorama-wrap'); | |
| const padHeld = padWrap?._aetherPadHeld; | |
| const merged = padHeld | |
| ? [...new Set([...window._aetherHeldKeys, ...padHeld])] | |
| : [...window._aetherHeldKeys]; | |
| const parentActive = merged.length > 0 | |
| || Math.abs(joy.fwd) > 0.01 | |
| || Math.abs(joy.strafe) > 0.01; | |
| if (parentActive) { | |
| window._aetherParentDriving = true; | |
| if (merged.length) sendKeysToFrame(frame, merged); | |
| else sendAnalogToFrame(frame, joy.fwd, joy.strafe); | |
| } else if (window._aetherParentDriving) { | |
| sendKeysToFrame(frame, []); | |
| sendAnalogToFrame(frame, 0, 0); | |
| window._aetherParentDriving = false; | |
| } | |
| }, 50); | |
| } | |
| if (!window._aetherFocusRecheckBound) { | |
| window._aetherFocusRecheckBound = true; | |
| setInterval(() => { | |
| if (!window._aetherDioramaActive) return; | |
| const el = document.activeElement; | |
| const typing = el && ( | |
| el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' | |
| || el.tagName === 'SELECT' || el.isContentEditable | |
| ); | |
| if (typing) return; | |
| const frame = window._aetherActiveFrame | |
| || document.querySelector('iframe.diorama-frame'); | |
| if (!frame) return; | |
| try { frame.contentWindow?.postMessage({ type: 'aether_activate' }, '*'); } catch (_) {} | |
| }, 12000); | |
| } | |
| // Portal navigation from diorama iframe | |
| if (!window._aetherPortalListenerBound) { | |
| window._aetherPortalListenerBound = true; | |
| window.addEventListener('message', (e) => { | |
| if (e.data && e.data.type === 'aether_register_frame') { | |
| const frames = Array.from(document.querySelectorAll('iframe.diorama-frame')); | |
| const active = frames.find((frame) => frame.contentWindow === e.source); | |
| if (active) { | |
| window._aetherActiveFrame = active; | |
| window._aetherHoverDiorama = true; | |
| window._aetherDioramaActive = true; | |
| } | |
| return; | |
| } | |
| if (e.data && e.data.type === 'aether_diorama_active') { | |
| const frames = Array.from(document.querySelectorAll('iframe.diorama-frame')); | |
| const active = frames.find((frame) => frame.contentWindow === e.source); | |
| if (active) { | |
| window._aetherActiveFrame = active; | |
| window._aetherHoverDiorama = true; | |
| window._aetherDioramaActive = true; | |
| } | |
| return; | |
| } | |
| if (!e.data || e.data.type !== 'aether_portal') return; | |
| const id = String(e.data.locationId || ''); | |
| if (!id) return; | |
| // Try explore tab portal_pick first | |
| const pp = document.querySelector('#portal_location_pick'); | |
| const ppInput = pp && (pp.querySelector('textarea') || pp.querySelector('input')); | |
| if (ppInput) { | |
| ppInput.value = id; | |
| ppInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| } | |
| // Also update map_pick to sync realm tab | |
| const mp = document.querySelector('#map_location_pick'); | |
| const mpInput = mp && (mp.querySelector('textarea') || mp.querySelector('input')); | |
| if (mpInput) { | |
| mpInput.value = id; | |
| mpInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| } | |
| }); | |
| } | |
| } | |
| """ | |
| demo.load(None, None, None, js=MAP_CLICK_JS) | |
| return demo | |
| if __name__ == "__main__": | |
| demo = build_app() | |
| launch_kwargs: dict = { | |
| "server_name": "0.0.0.0", | |
| "server_port": int(os.environ.get("PORT", 7860)), | |
| "share": False, | |
| "allowed_paths": assets.allowed_paths() + [ | |
| str(Path(tempfile.gettempdir()) / "aether_soul_cards"), | |
| str(Path(__file__).parent / "assets" / "generated"), | |
| ], | |
| } | |
| if GRADIO_MAJOR >= 6: | |
| launch_kwargs["css"] = CUSTOM_CSS | |
| launch_kwargs["theme"] = REALM_THEME | |
| demo.launch(**launch_kwargs) | |