diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -31,15 +31,8 @@ from world.locations import get_all_locations, get_location_by_id, update_entity 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_for_day, - render_book_of_ages, - render_current_event, - render_day_tabs, -) -from ui.entity_card import render_entity_card, render_entity_grid, render_entity_hologram -from ui.place_ribbon import render_place_ribbon +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, @@ -53,7 +46,7 @@ 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_bonds_sidebar, render_relationship_graph +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" @@ -78,108 +71,21 @@ _SOUL_LAST_REPLY: dict[str, str] = {} def opening_gate_html() -> str: - """Book cover intro, cinematic opening, and prologue overlay.""" - myth = _esc(FOUNDING_MYTH) - whisper = ( - "Thou standest at the threshold of a world that remembereth all things — " - "every soul summoned, every meeting, every hour the Realm hath lived without thee. " - "Turn the page, and let the Living Tome unfold." - ) - return f""" - + """Book cover intro and table-of-contents overlay.""" + return """
- -
-
-
-
- -
-

Book of Ages, Vol. I

- -

AETHERGARDEN

-

The Living Tome

- -
-
- -
- -
- +
+

Book of Ages, Vol. I

+ +

AETHERGARDEN

+

The Living Tome

+
- """ @@ -238,16 +125,6 @@ def _esc(text: str) -> str: ) -def summon_reveal_empty() -> str: - return """ -
-

The Oracle Awaits

-

Whisper what you would bring into the Garden on the left leaf. - Their name, face, and story shall manifest here.

-
- """ - - def summoning_html(description: str) -> str: """Immediate, evocative loading state while the Oracle generates a soul.""" desc = _esc(description.strip()) @@ -299,25 +176,44 @@ def _prewarm_oracle() -> None: def render_location_panel(location_id: int | None) -> str: if not location_id: return ( - '
' - '

Click a glowing place on the map to unveil its secrets.

' + '
' + '

Choose a glowing place on the map

' + '

Tap any marker to open that location. ' + 'On phone, you will jump straight to the Explore spread so you can enter immediately.

' '
' ) location = get_location_by_id(location_id) if not location: - return '
' + return "" - name = _esc(location["name"]) - desc = _esc(location.get("short_description", "")) - return ( - '
' - f'

' - f' ' - f'{name}' - f' — {desc}' - f'

' + entities = get_entities_by_location(location_id) + chips = "".join( + f'{e["display_name"]}' + for e in entities[:8] ) + more = f'+{len(entities) - 8} more' 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""" +
+
+
+

{location['name']}

+
+
+

{location['short_description']}

+

✦ {special}

+

+ {len(entities)} souls currently here +

+
{chips}{more}
+
+
+ """ _HIDE_CARD = gr.update(value=None, visible=False) @@ -333,8 +229,7 @@ def _summon_error(message: str): render_location_panel(None), _HIDE_CARD, _HIDE_CAPTION, - render_realm_pulse(), - render_realm_stats(), + "", ) @@ -375,8 +270,7 @@ def summon_entity(description: str, session_id: str): render_location_panel(entity["location_id"]), card_update, caption_update, - render_realm_pulse(), - render_realm_stats(), + render_diorama(entity["location_id"]), ) @@ -409,50 +303,11 @@ def filter_entities(location_filter, type_filter, status_filter, search): status=status_filter, search=search or None, ) - return render_entity_grid(entities), render_entity_hologram(entities[0] if entities else None) - - -def filter_book(day, entry_type, search): - from ui.book_display import parse_day_query - - parsed = parse_day_query(search) - if parsed is not None: - day = parsed - search = "" - try: - day_int = int(day) if day is not None else None - except (TypeError, ValueError): - day_int = None - return render_book_for_day( - day=day_int, - entry_type=entry_type, - search=search, - limit=12, - ) - - -def select_entity_by_id(entity_id: str): - if not entity_id: - return render_entity_hologram(None) - from world.entities import get_entity - - entity = get_entity(entity_id) - if not entity: - return render_entity_hologram(None) - return render_entity_hologram(entity) + return render_entity_grid(entities) -def render_realm_stats() -> str: - state = get_world_state() - day = int(state.get("current_day", 1) or 1) - count = len(get_all_entities()) - return f""" -
- World Day {day} - {count} souls awake - 8 sacred places -
- """ +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: @@ -628,48 +483,28 @@ def refresh_all_views(): _SOUL_HINT = '
Click a soul above to meet them.
' -def _explore_scene(loc_id: int | None): - if not loc_id: - return ( - render_diorama(None), - render_room(None), - gr.update(value=[]), - [], - _SOUL_HINT, - render_place_ribbon(None), - ) - items, soul_ids = soul_gallery(loc_id) - return ( - render_diorama(loc_id), - render_room(loc_id), - gr.update(value=items), - soul_ids, - _SOUL_HINT, - render_place_ribbon(loc_id), - ) - - def enter_place(evt: gr.SelectData): ids = place_ids() if evt.index is None or evt.index >= len(ids): - return (None, *_explore_scene(None)) + return None, render_diorama(None), render_room(None), gr.update(value=[]), [], _SOUL_HINT loc_id = ids[evt.index] - return (loc_id, *_explore_scene(loc_id)) + 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, *_explore_scene(None)) - return (loc_id, *_explore_scene(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): - try: - loc_id = int(location_id) if location_id else None - except (TypeError, ValueError): - loc_id = None - return _explore_scene(loc_id) + 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): @@ -690,143 +525,87 @@ def build_app() -> gr.Blocks: with gr.Blocks(**blocks_kwargs) as demo: session_id = gr.State(value=str(uuid.uuid4())) - # Pickers at root — must stay mounted (Gradio 5 skips visible=False from DOM) - with gr.Group(elem_classes=["aether-pick-bridge"]): - map_pick = gr.Textbox( - show_label=False, container=False, lines=1, max_lines=1, - elem_id="map_location_pick", elem_classes=["aether-hidden-pick"], - ) - explore_place_pick = gr.Textbox( - show_label=False, container=False, lines=1, max_lines=1, - elem_id="explore_place_pick", elem_classes=["aether-hidden-pick"], - ) - book_day_pick = gr.Textbox( - show_label=False, container=False, lines=1, max_lines=1, - elem_id="book_day_pick", elem_classes=["aether-hidden-pick"], - ) - entity_pick = gr.Textbox( - show_label=False, container=False, lines=1, max_lines=1, - elem_id="entity_pick", elem_classes=["aether-hidden-pick"], - ) - portal_pick = gr.Textbox( - show_label=False, container=False, lines=1, max_lines=1, - elem_id="portal_location_pick", elem_classes=["aether-hidden-pick"], - ) - opening_gate = gr.HTML(value=opening_gate_html(), elem_id="realm-opening-host") 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_id="tome-spreads", elem_classes=["realm-tabs"]) as tabs: with gr.Tab("The Realm", id="realm"): - with gr.Column(elem_classes=["tome-spread-shell"]): - with gr.Row(elem_classes=["tome-spread-row"]): - with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-page-centered"]): - gr.HTML( - '' - '

The Living Realm

' - '

Tap a place on the map — a vision shall rise.

' - '' - ) - world_map = gr.HTML( - value=render_world_map(), - elem_id="world-map", - ) - location_panel = gr.HTML( - value=render_location_panel(None), - elem_id="location-panel-host-wrap", - ) - gr.HTML('') - - with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-page-centered"]): - gr.HTML( - '' - '

Chronicle of the Hour

' - '

The Realm speaks — listen closely.

' - ) - realm_stats = gr.HTML(value=render_realm_stats()) - world_vignette = gr.HTML( - value=render_world_vignette(), - elem_classes=["tome-realm-vignette"], - ) - current_event = gr.HTML(value=render_current_event()) - realm_pulse = gr.HTML(value=render_realm_pulse(limit=4)) - activity_feed = gr.HTML(value=render_activity_feed(limit=4)) - gr.HTML('') - - with gr.Column(elem_classes=["tome-offpage-store"]): - realm_diorama = gr.HTML(value="") - with gr.Accordion("How this world works", open=False): - gr.HTML( - '
' - '

1 · You summon. Describe anything below and an AI gives it a name, ' - 'a look, goals, fears, and a home among the eight places.

' - '

2 · The world lives on its own. Every hour, even when no one is here, ' - 'the souls wander, meet, form bonds, and the land throws up strange events.

' - '

3 · Nothing is forgotten. Every arrival, meeting, and event is written ' - 'into the Book of Ages forever. Turn the page to Explore or open the ' - 'Book of Ages for the full chronicle.

' - '
' + 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( + '
' + '

1 · You summon. Describe anything below and an AI gives it a name, ' + 'a look, goals, fears, and a home among the eight places.

' + '

2 · The world lives on its own. Every hour, even when no one is here, ' + 'the souls wander, meet, form bonds, and the land throws up strange events.

' + '

3 · Nothing is forgotten. Every arrival, meeting, and event is written ' + 'into the Book of Ages forever. Open the Explore tab to step inside a place ' + 'and meet who lives there.

' + '
' + ) + + with gr.Row(): + with gr.Column(scale=3): + world_map = gr.HTML( + value=render_world_map(), + elem_id="world-map", ) - with gr.Accordion("⚡ Advance the World Now", open=False): - gr.HTML( - '

Manually trigger one simulation tick — the world will ' - 'generate a new event, run soul encounters, and update memories in real time.

' + location_panel = gr.HTML( + value=render_location_panel(None), ) - live_tick_btn = gr.Button("Run simulation tick →", variant="secondary", size="sm") - tick_result = gr.HTML() + 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('

Manually trigger one simulation tick — the world will generate a new event, run soul encounters, and update memories in real time.

') + live_tick_btn = gr.Button("Run simulation tick →", variant="secondary", size="sm") + tick_result = gr.HTML() location_select = gr.Dropdown( choices=[(l["name"], l["id"]) for l in get_all_locations()], label="Explore Location", - visible=False, + visible=True, ) + featured_chars = gr.HTML(value=render_featured_characters()) + with gr.Tab("Explore the Realm", 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=[]) - with gr.Column(elem_classes=["tome-spread-shell"]): - with gr.Row(elem_classes=["tome-spread-row"]): - with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-explore-index"]): - place_ribbon = gr.HTML( - value=render_place_ribbon(_default_loc_id), - container=False, - padding=False, - ) - wander_btn = gr.Button("✦ Wander at random", variant="secondary", size="sm") - - with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-explore-stage"]): - diorama_view = gr.HTML( - value=render_diorama(_default_loc_id), - container=False, - padding=False, - elem_id="explore-diorama-host", - ) - gr.HTML( - '
' - '

WASD walk · drag look · E talk · golden wisps are live visitors

' - '
' - ) - - with gr.Column(elem_classes=["tome-offpage-store"]): - 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"], - ) + gr.HTML( + '
' + '

Walk the Eight Sacred Places

' + '

Pick a place below — then WASD to walk, ' + 'drag to look, E to talk 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.

' + '
' + ) + 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=[], @@ -840,193 +619,100 @@ def build_app() -> gr.Blocks: soul_card = gr.HTML() with gr.Tab("Book of Ages", id="book"): - _book_state = get_world_state() - _init_book_day = int(_book_state.get("current_day", 1) or 1) - book_day = gr.State(value=_init_book_day) - - with gr.Column(elem_classes=["tome-spread-shell"]): - with gr.Row(elem_classes=["tome-spread-row"]): - with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-book-index"]): - day_tabs = gr.HTML(value=render_day_tabs(_init_book_day)) - _day_choices = list(range(max(1, _init_book_day - 14), _init_book_day + 1)) - book_day_select = gr.Dropdown( - choices=[(f"Day {d}", d) for d in reversed(_day_choices)], - value=_init_book_day, - label="Jump to day", - ) - book_filter = gr.Dropdown( - choices=[ - ("All", "all"), - ("Arrivals", "arrival"), - ("Interactions", "interaction"), - ("Events", "world_event"), - ("Milestones", "milestone"), - ("Dreams", "dream_fragment"), - ], - value="all", - label="Filter", - ) - book_search = gr.Textbox( - placeholder='Search name… or type "Day 3"', - label="Search", - ) - with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]): - book_display = gr.HTML( - value=render_book_for_day(_init_book_day, limit=12) - ) + 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 and Alliances", id="bonds"): - with gr.Column(elem_classes=["tome-spread-shell"]): - with gr.Row(elem_classes=["tome-spread-row"]): - with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page"]): - bonds_type_filter = gr.Dropdown( - choices=[ - ("All bonds", "all"), - ("Allied", "allied"), - ("Friends", "friends"), - ("Rivals", "rivals"), - ("Enemies", "enemies"), - ("Mentor", "mentor"), - ("Owes debt", "owes_debt"), - ("Curious", "curious"), - ], - value="all", - label="Show connections", - ) - bonds_sidebar = gr.HTML( - value=render_bonds_sidebar(), - container=False, - padding=False, - ) - with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]): - rel_graph = gr.HTML(value=render_relationship_graph()) + gr.HTML( + '
' + '

The Web of Bonds

' + '

Every alliance, rivalry, and debt forged by the simulation — ' + 'not scripted, only remembered.

' + ) + rel_graph = gr.HTML(value=render_relationship_graph()) with gr.Tab("Souls of the Garden", id="entities"): - with gr.Column(elem_classes=["tome-spread-shell"]): - with gr.Row(elem_classes=["tome-spread-row"]): - with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-souls-index"]): - gr.HTML('

Souls of the Garden

') - with gr.Row(elem_classes=["souls-filter-row"]): - loc_filter = gr.Dropdown( - choices=["all"] + [l["name"] for l in get_all_locations()], - value="all", - label="Place", - scale=1, - ) - type_filter = gr.Dropdown( - choices=["all", "character", "creature", "object", "place"], - value="all", - label="Kind", - scale=1, - ) - with gr.Row(elem_classes=["souls-filter-row"]): - status_filter = gr.Dropdown( - choices=["all", "active", "dormant", "legendary"], - value="all", - label="Status", - scale=1, - ) - with gr.Row(elem_classes=["souls-search-row"]): - entity_search = gr.Textbox( - placeholder="Search souls by name…", - label="Find", - scale=1, - ) - entity_grid = gr.HTML( - value=render_entity_grid(get_all_entities()), - container=False, - padding=False, - ) - with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-souls-stage"]): - _first_ent = get_all_entities(limit=1) - entity_detail = gr.HTML( - value=render_entity_hologram(_first_ent[0] if _first_ent else None), - container=False, - padding=False, - ) - - with gr.Column(elem_classes=["tome-offpage-store"]): - featured_chars = gr.HTML(value=render_featured_characters()) + 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", + ) with gr.Tab("Summon a New Soul", id="summon"): - with gr.Column(elem_classes=["tome-spread-shell"]): - with gr.Row(elem_classes=["tome-spread-row"]): - with gr.Column( - scale=1, - elem_classes=[ - "tome-page-left", - "tome-printed-page", - "tome-page-centered", - "tome-summon-index", - ], - ): - gr.HTML( - '' - '

Summon Into the Realm

' - '

Describe what you bring on this leaf — the Oracle ' - 'forges their identity on the right.

' - ) - summon_input = gr.Textbox( - placeholder="A clockmaker who weaves time from fallen leaves…", - label=None, - show_label=False, - lines=5, - elem_classes=["realm-summon-input", "summon-prompt-input"], - ) - gr.HTML( - '

Then seal your words below

' - ) - summon_btn = gr.Button( - "Seal & Summon ✦", - variant="primary", - elem_classes=["seal-summon-btn"], - elem_id="summon-seal-btn", - ) - gr.HTML('') - - with gr.Column( - scale=1, - elem_classes=[ - "tome-page-right", - "tome-printed-page", - "tome-page-centered", - "tome-summon-stage", - ], - ): - gr.HTML( - '' - '

Soul Unveiled

' - '

The identity the Oracle forges from your words.

' - ) - entity_result = gr.HTML( - value=summon_reveal_empty(), - elem_classes=["entity-result-slot"], - ) - soul_card_img = gr.Image( - label="Your Soul Card", - visible=False, - show_label=True, - interactive=False, - type="filepath", - elem_classes=["soul-card-image"], - height=280, - ) - share_caption_box = gr.Textbox( - label="Share caption", - visible=False, - lines=2, - interactive=True, - elem_classes=["share-caption-box"], - ) - gr.HTML('') + gr.HTML('

Summon Something Into the Realm

') + with gr.Row(): + summon_input = gr.Textbox( + placeholder="Describe what you are bringing into the world...", + label=None, + show_label=False, + lines=5, + scale=4, + ) + summon_btn = gr.Button("Seal & Summon", variant="primary", scale=1, elem_classes=["seal-summon-btn"]) + + gr.HTML( + '

Hint: "A wizard" is forgotten. ' + '"A wizard who collects the sound of doors closing" is remembered.

' + ) + 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, + ) _summon_outputs = [ world_map, header, activity_feed, entity_result, location_panel, - soul_card_img, share_caption_box, realm_pulse, realm_stats, - ] - - _explore_outputs = [ - diorama_view, room_view, souls_gallery, soul_ids_state, soul_card, place_ribbon, + soul_card_img, share_caption_box, realm_diorama, ] summon_btn.click( @@ -1048,7 +734,8 @@ def build_app() -> gr.Blocks: () => { setTimeout(() => { const card = document.querySelector('.soul-card-image'); - const target = card || document.querySelector('.tome-summon-stage .entity-card'); + const diorama = document.querySelector('.diorama-wrap'); + const target = card || diorama || document.querySelector('.entity-card'); if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 350); } @@ -1074,56 +761,55 @@ def build_app() -> gr.Blocks: () => { setTimeout(() => { const card = document.querySelector('.soul-card-image'); - const target = card || document.querySelector('.tome-summon-stage .entity-card'); + 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_map_pick(pick): - try: - loc_id = int(pick) if pick else None - except (TypeError, ValueError): - loc_id = None - return render_world_map(loc_id), render_location_panel(loc_id) + 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_map_pick, + fn=_realm_location_change, inputs=[location_select], - outputs=[world_map, location_panel], + 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( - _realm_map_pick, + _map_open_diorama, inputs=[map_pick], - outputs=[world_map, location_panel], - ) - - explore_place_pick.change( - open_diorama_from_map, - inputs=[explore_place_pick], - outputs=_explore_outputs, + 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=[explore_place_pick], + inputs=[map_pick], outputs=[explore_loc], - ).then( - None, None, None, - js=""" - () => { - setTimeout(() => { - const frame = document.querySelector('.tome-explore-stage .diorama-frame'); - if (frame) frame.focus(); - }, 200); - } - """, ) portal_pick.change( open_diorama_from_map, inputs=[portal_pick], - outputs=_explore_outputs, + 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], @@ -1133,12 +819,12 @@ def build_app() -> gr.Blocks: places_gallery.select( enter_place, inputs=None, - outputs=[explore_loc, *_explore_outputs], + 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, *_explore_outputs], + 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]) @@ -1149,7 +835,7 @@ def build_app() -> gr.Blocks: ).then( fn=refresh_all_views, outputs=[world_map, header, current_event, activity_feed, world_vignette, featured_chars, realm_pulse], - ).then(fn=render_realm_stats, outputs=[realm_stats]) + ) # ── Soul chat API ── chat_soul_id_comp = gr.Textbox(visible=False, elem_id="chat_soul_id_input") @@ -1201,89 +887,34 @@ def build_app() -> gr.Blocks: outputs=[presence_bar], ) - def _book_day_change(day_pick, day_state, entry_type, search): - try: - day = int(day_pick) if day_pick else int(day_state or 1) - except (TypeError, ValueError): - day = int(day_state or 1) - return day, render_day_tabs(day), filter_book(day, entry_type, search) - - def _book_day_select_change(day_val, entry_type, search): - try: - day = int(day_val) - except (TypeError, ValueError): - state = get_world_state() - day = int(state.get("current_day", 1) or 1) - return day, render_day_tabs(day), filter_book(day, entry_type, search) - - book_day_pick.change( - _book_day_change, - [book_day_pick, book_day, book_filter, book_search], - [book_day, day_tabs, book_display], - ).then( - lambda d: d, - inputs=[book_day], - outputs=[book_day_select], - ) - book_day_select.change( - _book_day_select_change, - [book_day_select, book_filter, book_search], - [book_day, day_tabs, book_display], - ) - book_filter.change(filter_book, [book_day, book_filter, book_search], [book_display]) - book_search.change(filter_book, [book_day, book_filter, book_search], [book_display]) - - bonds_type_filter.change( - render_bonds_sidebar, - [bonds_type_filter], - [bonds_sidebar], - ) + 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_detail], + [entity_grid], ) - entity_pick.change(select_entity_by_id, [entity_pick], [entity_detail]) + 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]).then( - fn=render_bonds_sidebar, outputs=[bonds_sidebar] - ).then(fn=render_realm_stats, outputs=[realm_stats]) + ).then(fn=render_relationship_graph, outputs=[rel_graph]) MAP_CLICK_JS = """ () => { - if (window._aetherInitBound) { - const dedupe = () => { - const hoisted = document.body.querySelector('#realm-opening.realm-opening-hoisted') - || document.body.querySelector('#realm-opening'); - if (!hoisted) return; - document.querySelectorAll('#realm-opening-host #realm-opening').forEach((el) => { - if (el !== hoisted) el.remove(); - }); - const host = document.getElementById('realm-opening-host'); - if (host && !host.querySelector('#realm-opening')) { - host.style.setProperty('display', 'none', 'important'); - } - }; - dedupe(); - return; - } - window._aetherInitBound = true; - const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const sections = [ { key: 'realm', label: 'The Realm', kind: 'tab', panel: 0 }, { key: 'explore', label: 'Explore the Realm', kind: 'tab', panel: 1 }, { key: 'book', label: 'Book of Ages', kind: 'tab', panel: 2 }, - { key: 'bonds', label: 'Bonds and Alliances', kind: 'tab', panel: 3 }, { key: 'entities', label: 'Souls of the Garden', kind: 'tab', panel: 4 }, + { key: 'bonds', label: 'Bonds and Alliances', kind: 'tab', panel: 3 }, { key: 'summon', label: 'Summon a New Soul', kind: 'tab', panel: 5 } ]; let currentSpread = 0; @@ -1305,13 +936,11 @@ def build_app() -> gr.Blocks: panels.forEach((panel, i) => { const active = i === panelIndex; panel.style.setProperty('display', active ? 'flex' : 'none', 'important'); - panel.style.setProperty('flex-direction', 'column', 'important'); panel.style.setProperty('height', active ? '565px' : '0', 'important'); panel.style.setProperty('max-height', active ? '565px' : '0', 'important'); - panel.style.setProperty('overflow', 'hidden', 'important'); + panel.style.setProperty('overflow', active ? 'auto' : 'hidden', 'important'); panel.setAttribute('aria-hidden', active ? 'false' : 'true'); }); - wirePageExpand(); const pad = document.getElementById('aether-walk-pad'); if (pad && panelIndex !== 1) { pad.classList.remove('visible', 'explore-only'); @@ -1319,126 +948,37 @@ def build_app() -> gr.Blocks: }; const ensureAmbientMelody = () => { - if (window._aetherAmbientStarted) { - window._aetherAmbientCtx?.resume?.(); - return; - } + if (window._aetherAmbientStarted) return; try { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (!AudioCtx) return; const ctx = new AudioCtx(); - window._aetherAmbientCtx = ctx; - const master = ctx.createGain(); - master.gain.value = 0.0001; - - const warmth = ctx.createBiquadFilter(); - warmth.type = 'lowpass'; - warmth.frequency.value = prefersReduced ? 1600 : 2400; - warmth.Q.value = 0.5; - - const delay = ctx.createDelay(2.8); - delay.delayTime.value = 0.48; - const delayFb = ctx.createGain(); - delayFb.gain.value = 0.26; - const delayMix = ctx.createGain(); - delayMix.gain.value = 0.32; - - warmth.connect(delay); - delay.connect(delayFb); - delayFb.connect(delay); - delay.connect(delayMix); - delayMix.connect(master); - warmth.connect(master); + master.gain.value = 0.022; master.connect(ctx.destination); - const now = ctx.currentTime; - master.gain.linearRampToValueAtTime(prefersReduced ? 0.009 : 0.013, now + 5); - - const padNotes = [220, 261.63, 293.66, 329.63, 392, 440]; - padNotes.forEach((freq, i) => { + const notes = [220, 261.63, 329.63]; + notes.forEach((freq, i) => { const osc = ctx.createOscillator(); - const osc2 = ctx.createOscillator(); - const g = ctx.createGain(); - const g2 = ctx.createGain(); - osc.type = 'sine'; - osc2.type = 'triangle'; + const gain = ctx.createGain(); + osc.type = i === 0 ? 'sine' : 'triangle'; osc.frequency.value = freq; - osc2.frequency.value = freq * 1.0015; - const base = 0.0007 / (i * 0.55 + 1); - g.gain.value = base; - g2.gain.value = base * 0.45; - osc.connect(g); - osc2.connect(g2); - g.connect(warmth); - g2.connect(warmth); + gain.gain.value = 0.0001; + osc.connect(gain); + gain.connect(master); osc.start(); - osc2.start(); - - const breathe = () => { - const t = ctx.currentTime; - g.gain.cancelScheduledValues(t); - g.gain.setValueAtTime(g.gain.value, t); - g.gain.linearRampToValueAtTime(base * 2.1, t + 4 + i * 0.4); - g.gain.linearRampToValueAtTime(base * 0.55, t + 10 + i * 0.35); + + const pulse = () => { + const now = ctx.currentTime; + gain.gain.cancelScheduledValues(now); + gain.gain.setValueAtTime(Math.max(gain.gain.value, 0.0001), now); + gain.gain.exponentialRampToValueAtTime(0.014 / (i + 1), now + 2.2 + i * 0.35); + gain.gain.exponentialRampToValueAtTime(0.00012, now + 7.5 + i * 0.4); }; - breathe(); - setInterval(breathe, 9000 + i * 800); + pulse(); + setInterval(pulse, 7000 + i * 400); }); - - const melodyNotes = [ - 392, 440, 493.88, 523.25, 587.33, 659.25, - 587.33, 523.25, 440, 392, 329.63, 293.66, 329.63, 392 - ]; - let noteIdx = 0; - const playBell = () => { - if (!window._aetherAmbientStarted) return; - const freq = melodyNotes[noteIdx % melodyNotes.length]; - noteIdx += 1; - const t = ctx.currentTime; - const osc = ctx.createOscillator(); - const env = ctx.createGain(); - osc.type = 'sine'; - osc.frequency.value = freq; - env.gain.setValueAtTime(0.0001, t); - env.gain.exponentialRampToValueAtTime(0.0065, t + 0.12); - env.gain.exponentialRampToValueAtTime(0.0001, t + 3.2); - osc.connect(env); - env.connect(warmth); - osc.start(t); - osc.stop(t + 3.4); - }; - - const scheduleMelody = () => { - if (!window._aetherAmbientStarted) return; - playBell(); - setTimeout(scheduleMelody, 2600 + Math.random() * 2400); - }; - setTimeout(scheduleMelody, 1800); - - const shimmer = () => { - if (!window._aetherAmbientStarted) return; - const t = ctx.currentTime; - const osc = ctx.createOscillator(); - const env = ctx.createGain(); - osc.type = 'triangle'; - osc.frequency.value = 880 + Math.random() * 264; - env.gain.setValueAtTime(0.0001, t); - env.gain.exponentialRampToValueAtTime(0.0028, t + 0.6); - env.gain.exponentialRampToValueAtTime(0.0001, t + 5); - osc.connect(env); - env.connect(warmth); - osc.start(t); - osc.stop(t + 5.2); - setTimeout(shimmer, 14000 + Math.random() * 10000); - }; - setTimeout(shimmer, 6000); - window._aetherAmbientStarted = true; - - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') ctx.resume(); - }, { passive: true }); } catch (_) {} }; @@ -1477,7 +1017,7 @@ def build_app() -> gr.Blocks: const page = document.getElementById('tome-page-indicator'); if (!page) return; const idx = Math.max(1, currentSpread + 1); - page.textContent = `Page ${toRoman(idx)}`; + page.textContent = `Page ${toRoman(idx + 4)}`; }; const closeToc = () => { @@ -1487,48 +1027,7 @@ def build_app() -> gr.Blocks: if (floating) floating.classList.remove('open'); }; - const closeBook = () => { - const gate = document.getElementById('realm-opening'); - const book3d = document.getElementById('realm-book-3d'); - if (!gate) return; - setAppInteractive(false); - gate.classList.remove('is-closed', 'is-opened', 'is-prologue', 'is-unfolded', 'is-unfolding'); - gate.classList.add('is-closing', 'is-reclosed'); - const openBook = document.getElementById('realm-book-open'); - const prologue = document.getElementById('realm-prologue'); - if (openBook) openBook.setAttribute('aria-hidden', 'true'); - if (prologue) { - prologue.classList.remove('visible'); - prologue.setAttribute('aria-hidden', 'true'); - } - if (book3d) { - book3d.classList.remove('is-opening'); - setTimeout(() => book3d.classList.add('show-back'), prefersReduced ? 0 : 80); - } - setTimeout(() => { - gate.classList.remove('is-closing'); - gate.classList.add('is-back-visible'); - }, prefersReduced ? 60 : 1150); - applyTurnFx('prev'); - }; - - const reopenBook = () => { - const gate = document.getElementById('realm-opening'); - const book3d = document.getElementById('realm-book-3d'); - if (!gate) return; - if (book3d) book3d.classList.remove('show-back', 'is-opening'); - gate.classList.remove('is-reclosed', 'is-back-visible', 'is-closing'); - gate.classList.add('is-opened', 'is-closed'); - setAppInteractive(true); - playOpeningChime(); - ensureAmbientMelody(); - }; - const goToSpread = (idx, direction = 'next') => { - if (idx >= sections.length && direction === 'next') { - closeBook(); - return; - } const clamped = Math.max(0, Math.min(sections.length - 1, idx)); const spread = sections[clamped]; if (!spread) return; @@ -1560,13 +1059,13 @@ def build_app() -> gr.Blocks: -
Page I
+
Page V
- ${sections.map((s, i) => ``).join('')} + ${sections.map((s, i) => ``).join('')}
`; document.body.appendChild(nav); @@ -1583,13 +1082,7 @@ def build_app() -> gr.Blocks: } if (next && !next.dataset.bound) { next.dataset.bound = '1'; - next.addEventListener('click', () => { - if (currentSpread >= sections.length - 1) { - closeBook(); - return; - } - goToSpread(currentSpread + 1, 'next'); - }); + next.addEventListener('click', () => goToSpread(currentSpread + 1, 'next')); } if (tocOpen && !tocOpen.dataset.bound) { tocOpen.dataset.bound = '1'; @@ -1620,10 +1113,7 @@ def build_app() -> gr.Blocks: window.addEventListener('keydown', (e) => { const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; - if (e.key === 'ArrowRight') { - if (currentSpread >= sections.length - 1) closeBook(); - else goToSpread(currentSpread + 1, 'next'); - } + if (e.key === 'ArrowRight') goToSpread(currentSpread + 1, 'next'); if (e.key === 'ArrowLeft') goToSpread(currentSpread - 1, 'prev'); if (e.key === 'Escape') closeToc(); }, { passive: true }); @@ -1633,465 +1123,67 @@ def build_app() -> gr.Blocks: setActivePanel(sections[currentSpread].panel); }; - const wirePageExpand = () => { - document.querySelectorAll('.tome-expand-btn, #tome-expand-backdrop').forEach((el) => { - el.remove(); - }); - }; - - const setAppInteractive = (on) => { - const app = document.querySelector('.gradio-container'); - if (app) app.style.pointerEvents = on ? '' : 'none'; - }; - const wireOpening = () => { const gate = document.getElementById('realm-opening'); if (!gate || gate.dataset.bound) return; gate.dataset.bound = '1'; - if (gate.classList.contains('is-open') && !gate.classList.contains('is-opened')) { - setAppInteractive(false); - } - const book3d = document.getElementById('realm-book-3d'); - const closedBook = document.getElementById('realm-book-closed'); - const openBook = document.getElementById('realm-book-open'); - const prologue = document.getElementById('realm-prologue'); - let prologueReady = false; - let prologueTypingGen = 0; - - const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - - const typeText = (el, text, speed = 24, gen = 0) => new Promise((resolve) => { - if (!el || !text) { resolve(); return; } - let i = 0; - el.textContent = ''; - el.classList.add('is-typing'); - const tick = () => { - if (gen !== prologueTypingGen) { - el.classList.remove('is-typing'); - resolve(); - return; - } - if (i < text.length) { - el.textContent += text[i++]; - setTimeout(tick, speed + Math.random() * 18); - } else { - el.classList.remove('is-typing'); - resolve(); - } - }; - tick(); - }); - - const setPrologueReady = () => { - prologueReady = true; - const btn = document.getElementById('realm-prologue-enter'); - const skipBtn = document.getElementById('realm-prologue-skip'); - const cursor = document.getElementById('realm-prologue-cursor'); - if (cursor) cursor.classList.remove('active'); - if (skipBtn) skipBtn.classList.add('is-hidden'); - if (btn) { - btn.disabled = false; - btn.classList.add('ready'); - } - }; - - const skipPrologue = () => { - if (prologueReady) return; - prologueTypingGen += 1; - const kicker = document.getElementById('realm-prologue-kicker'); - const myth = document.getElementById('realm-prologue-myth'); - const whisper = document.getElementById('realm-prologue-whisper'); - if (kicker) { - kicker.textContent = kicker.dataset.text || ''; - kicker.classList.remove('is-typing'); - } - if (myth) { - myth.textContent = myth.dataset.text || ''; - myth.classList.remove('is-typing'); - } - if (whisper) { - whisper.textContent = whisper.dataset.text || ''; - whisper.classList.remove('is-typing'); - } - setPrologueReady(); - }; - - const runPrologueTyping = async () => { - const kicker = document.getElementById('realm-prologue-kicker'); - const myth = document.getElementById('realm-prologue-myth'); - const whisper = document.getElementById('realm-prologue-whisper'); - const skipBtn = document.getElementById('realm-prologue-skip'); - const cursor = document.getElementById('realm-prologue-cursor'); - const gen = ++prologueTypingGen; - if (skipBtn) skipBtn.classList.remove('is-hidden'); - if (cursor) cursor.classList.add('active'); - - if (prefersReduced) { - skipPrologue(); - return; - } - - await typeText(kicker, kicker?.dataset.text || '', 34, gen); - if (gen !== prologueTypingGen) return; - await sleep(380); - await typeText(myth, myth?.dataset.text || '', 20, gen); - if (gen !== prologueTypingGen) return; - await sleep(420); - await typeText(whisper, whisper?.dataset.text || '', 22, gen); - if (gen !== prologueTypingGen) return; - setPrologueReady(); - }; - - const hideOpeningShell = () => { - const host = document.getElementById('realm-opening-host'); - if (host) { - host.style.setProperty('display', 'none', 'important'); - host.style.setProperty('visibility', 'hidden', 'important'); - host.style.setProperty('pointer-events', 'none', 'important'); - } - const hoisted = document.getElementById('realm-opening'); - if (hoisted) { - hoisted.classList.add('is-closed'); - hoisted.style.setProperty('pointer-events', 'none', 'important'); - } - document.body.classList.add('aether-realm-entered'); - }; - - const revealRealm = () => { - setAppInteractive(true); - setActivePanel(sections[currentSpread].panel); - updatePageBadge(); - const nav = document.getElementById('tome-nav'); - if (nav) nav.style.removeProperty('display'); - }; - - const finishOpening = () => { - if (!prologueReady) return; - playOpeningChime(); - ensureAmbientMelody(); - gate.classList.remove('is-reclosed'); - gate.classList.add('is-opened'); - hideOpeningShell(); - revealRealm(); - closeToc(); - setTimeout(() => { - gate.classList.add('is-closed'); - hideOpeningShell(); - revealRealm(); - }, 700); - }; - - const beginUnfold = () => { - playOpeningChime(); - ensureAmbientMelody(); - setAppInteractive(false); - gate.classList.add('is-unfolding'); - if (book3d) book3d.classList.add('is-opening'); - if (openBook) openBook.setAttribute('aria-hidden', 'false'); - setTimeout(() => { - gate.classList.add('is-unfolded', 'is-prologue'); - if (prologue) { - prologue.setAttribute('aria-hidden', 'false'); - prologue.classList.add('visible'); - } - runPrologueTyping(); - }, prefersReduced ? 120 : 1200); - }; - - gate.addEventListener('click', (e) => { - const target = e.target; - if (!(target instanceof Element)) return; - if (gate.classList.contains('is-reclosed') && - (target.id === 'realm-opening-enter' || target.id === 'realm-reopen-enter')) { - e.preventDefault(); - e.stopPropagation(); - reopenBook(); - return; - } - if (target.id === 'realm-opening-enter') { - e.preventDefault(); - e.stopPropagation(); - beginUnfold(); - } - if (target.id === 'realm-prologue-skip') { - e.preventDefault(); - e.stopPropagation(); - skipPrologue(); - return; - } - if (target.id === 'realm-prologue-enter' && prologueReady) { - e.preventDefault(); - e.stopPropagation(); - finishOpening(); - } - }); - - window.addEventListener('keydown', (e) => { - if (!prologueReady || !gate.classList.contains('is-prologue')) return; - const tag = document.activeElement?.tagName; - if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - finishOpening(); - } - }, { passive: false }); - }; - - const dedupeOpening = () => { - const hoisted = document.body.querySelector('#realm-opening.realm-opening-hoisted') - || document.body.querySelector('#realm-opening'); - if (hoisted) { - document.querySelectorAll('#realm-opening').forEach((el) => { - if (el !== hoisted) el.remove(); + const btn = document.getElementById('realm-opening-enter'); + if (btn) { + btn.addEventListener('click', () => { + playOpeningChime(); + ensureAmbientMelody(); + gate.classList.add('is-opened'); + closeToc(); + setTimeout(() => { + gate.classList.add('is-closed'); + }, 450); }); } - const host = document.getElementById('realm-opening-host'); - if (host) { - host.querySelectorAll('#realm-opening').forEach((el) => el.remove()); - if (!host.querySelector('#realm-opening')) { - host.style.setProperty('display', 'none', 'important'); - host.style.setProperty('visibility', 'hidden', 'important'); - host.style.setProperty('pointer-events', 'none', 'important'); - } - } - }; - - const hoistOpening = () => { - if (window._aetherOpeningHoisted) { - dedupeOpening(); - return; - } - const gate = document.getElementById('realm-opening'); - const critical = document.getElementById('realm-opening-critical'); - const host = document.getElementById('realm-opening-host'); - if (gate && gate.parentElement !== document.body) { - window._aetherOpeningHoisted = true; - gate.classList.add('realm-opening-hoisted'); - document.body.appendChild(gate); - if (critical && critical.parentElement !== document.head) { - document.head.appendChild(critical); - } - } else if (gate && gate.parentElement === document.body) { - window._aetherOpeningHoisted = true; - gate.classList.add('realm-opening-hoisted'); - } - dedupeOpening(); - if (host && !host.querySelector('#realm-opening')) { - host.style.setProperty('display', 'none', 'important'); - host.style.setProperty('visibility', 'hidden', 'important'); - host.style.setProperty('pointer-events', 'none', 'important'); - } }; - hoistOpening(); wireOpening(); wireTomeNav(); - wirePageExpand(); - - const gateOnLoad = document.getElementById('realm-opening'); - if (gateOnLoad && gateOnLoad.classList.contains('is-open') && !gateOnLoad.classList.contains('is-opened')) { - setAppInteractive(false); - } - - const triggerGradioInput = (elemId, value) => { - const clean = String(elemId || '').replace(/^#/, ''); - let root = document.getElementById(clean) - || document.querySelector('#' + clean) - || document.querySelector('[id="' + clean + '"]'); - if (!root) { - const cfg = window.gradio_config?.components || []; - const comp = cfg.find((c) => c?.props?.elem_id === clean); - if (comp) root = document.getElementById('component-' + comp.id); - } - if (!root) { - root = document.querySelector('.aether-hidden-pick#' + clean) - || document.querySelector('.aether-hidden-pick[id="' + clean + '"]'); - } - if (!root) return false; - const input = (root.tagName === 'TEXTAREA' || root.tagName === 'INPUT') ? root - : (root.querySelector('textarea:not([disabled])') - || root.querySelector('input[type="text"]') - || root.querySelector('input:not([type="hidden"])')); - if (!input) return false; - const proto = input.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; - const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set; - const next = String(value); - if (setter) setter.call(input, next); - else input.value = next; - input.dispatchEvent(new Event('input', { bubbles: true, composed: true })); - input.dispatchEvent(new Event('change', { bubbles: true, composed: true })); - return true; - }; - - const ensureLocationModal = () => { - let modal = document.getElementById('aether-location-modal'); - if (modal) return modal; - modal = document.createElement('div'); - modal.id = 'aether-location-modal'; - modal.className = 'location-modal-overlay'; - modal.innerHTML = ` -
-
-
-
- -
-
-

-

-

-

-
-
-
`; - document.body.appendChild(modal); - const closeBtn = modal.querySelector('.location-modal-close'); - if (closeBtn) { - closeBtn.addEventListener('click', () => modal.classList.remove('is-open')); - closeBtn.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - modal.classList.remove('is-open'); - } - }); - } - modal.addEventListener('click', (e) => { - if (e.target === modal) modal.classList.remove('is-open'); - }); - return modal; - }; - - const openLocationFromNode = (node) => { - if (!node) return; - const id = node.getAttribute('data-location-id'); - if (!id) return; - const modal = ensureLocationModal(); - const name = node.getAttribute('data-name') || 'Unknown place'; - const desc = node.getAttribute('data-desc') || ''; - const special = node.getAttribute('data-special') || ''; - const souls = node.getAttribute('data-souls') || '0'; - const img = node.getAttribute('data-img') || ''; - const nameEl = modal.querySelector('.location-panel-name'); - const descEl = modal.querySelector('.location-panel-desc'); - const specialEl = modal.querySelector('.location-panel-special'); - const soulsEl = modal.querySelector('.location-soul-count'); - const imgEl = modal.querySelector('.location-panel-image'); - const imgWrap = modal.querySelector('.location-panel-image-wrap'); - if (nameEl) nameEl.textContent = name; - if (descEl) descEl.textContent = desc; - if (specialEl) { - specialEl.textContent = special ? ('✦ ' + special) : ''; - specialEl.style.display = special ? '' : 'none'; - } - if (soulsEl) soulsEl.textContent = souls + ' souls currently here'; - if (imgEl) { - imgEl.src = img || ''; - imgEl.alt = name; - } - if (imgWrap) imgWrap.style.display = img ? '' : 'none'; - modal.classList.add('is-open'); - document.querySelectorAll('.location-node').forEach((n) => { - n.classList.toggle('location-selected', n.getAttribute('data-location-id') === id); - }); - triggerGradioInput('map_location_pick', id); - }; const bind = () => { - document.querySelectorAll('#world-map .location-node, [id="world-map"] .location-node').forEach((node) => { + document.querySelectorAll('#world-map [data-location-id]').forEach((node) => { + if (node.dataset.mapBound) return; + node.dataset.mapBound = '1'; node.style.cursor = 'pointer'; - }); - }; - - if (!window._aetherMapDelegationBound) { - window._aetherMapDelegationBound = true; - const mapPlaceTarget = (target) => { - if (!(target instanceof Element)) return null; - const hit = target.closest('.location-hit, .location-circle, .location-label, .location-count'); - if (!hit) return null; - const node = hit.closest('.location-node[data-location-id]'); - if (!node) return null; - const inMap = node.closest('#world-map') || node.closest('[id="world-map"]'); - return inMap ? node : null; - }; - document.addEventListener('click', (e) => { - const node = mapPlaceTarget(e.target); - if (!node) return; - e.preventDefault(); - e.stopPropagation(); - openLocationFromNode(node); - }); - } - const fireHiddenInput = (id, value) => { - const elemId = (id || '').replace(/^#/, ''); - triggerGradioInput(elemId, value); - }; - - const wireClickable = (el, handler) => { - if (!el || el.dataset.boundClick) return; - el.dataset.boundClick = '1'; - el.addEventListener('click', handler); - el.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handler(e); - } - }); - }; - - const closeLocationModal = () => { - document.querySelectorAll('.location-modal-overlay').forEach((m) => m.classList.remove('is-open')); - }; - window._aetherCloseLocationModal = closeLocationModal; - - const bindSpreadClicks = () => { - document.querySelectorAll('.place-ribbon-item').forEach((btn) => { - wireClickable(btn, () => { - const id = btn.getAttribute('data-location-id'); - if (!id) return; - document.querySelectorAll('.place-ribbon-item').forEach((el) => { - el.classList.toggle('is-selected', el.getAttribute('data-location-id') === id); - }); - fireHiddenInput('explore_place_pick', id); - }); - }); - document.querySelectorAll('.day-tab').forEach((btn) => { - wireClickable(btn, () => { - const day = btn.getAttribute('data-day'); - if (day) fireHiddenInput('#book_day_pick', day); - }); - }); - document.querySelectorAll('.entity-grid-card').forEach((card) => { - const id = card.getAttribute('data-entity-id'); - if (!id) return; - card.style.cursor = 'pointer'; - if (card.dataset.entityBound === id) return; - card.dataset.entityBound = id; - wireClickable(card, () => { - document.querySelectorAll('.entity-grid-card').forEach((el) => { - el.classList.toggle('is-selected', el.getAttribute('data-entity-id') === id); - }); - fireHiddenInput('entity_pick', id); - }); - }); - document.querySelectorAll('.location-modal-close').forEach((btn) => { - wireClickable(btn, closeLocationModal); - }); - document.querySelectorAll('.location-modal-overlay').forEach((overlay) => { - if (overlay.dataset.overlayBound) return; - overlay.dataset.overlayBound = '1'; - overlay.addEventListener('click', (e) => { - if (e.target === overlay) closeLocationModal(); + const setHiddenValue = (rootId, value) => { + const root = document.querySelector(`#${rootId}`); + const input = root && (root.querySelector('textarea') || root.querySelector('input')); + if (!input || !value) return; + input.value = value; + input.dispatchEvent(new Event('input', { bubbles: true })); + }; + node.addEventListener('click', () => { + const id = node.getAttribute('data-location-id'); + if (!id) return; + + setHiddenValue('map_location_pick', id); + setHiddenValue('portal_location_pick', id); + + const isPhoneLayout = window.matchMedia('(max-width: 920px)').matches; + if (isPhoneLayout) { + goToSpread(1, 'next'); + setTimeout(() => { + const target = document.querySelector('iframe.diorama-frame') + || document.querySelector('.room-view') + || document.querySelector('.diorama-empty'); + if (target) { + target.scrollIntoView({ + behavior: prefersReduced ? 'auto' : 'smooth', + block: 'start' + }); + } + }, 360); + } }); }); }; - bind(); - bindSpreadClicks(); - new MutationObserver(() => { - bind(); - bindSpreadClicks(); - }).observe(document.body, { childList: true, subtree: true }); + new MutationObserver(bind).observe(document.body, { childList: true, subtree: true }); window._aetherHoverDiorama = false; window._aetherDioramaActive = false; @@ -2155,37 +1247,29 @@ def build_app() -> gr.Blocks: }; const ensureGlobalWalkPad = () => { - let el = document.getElementById('aether-walk-pad'); - if (!el) { - el = document.createElement('div'); - el.id = 'aether-walk-pad'; - el.setAttribute('aria-label', 'Walk'); - el.innerHTML = ` -
- - - - - - -
`; - } - const stage = getVisibleDioramaFrame()?.closest('.diorama-stage'); - if (stage && el.parentElement !== stage) { - stage.appendChild(el); - } + if (document.getElementById('aether-walk-pad')) return; + const el = document.createElement('div'); + el.id = 'aether-walk-pad'; + el.setAttribute('aria-label', 'Walk'); + el.innerHTML = ` +
+ + + + + + +
`; + document.body.appendChild(el); }; const updateWalkPadVisibility = () => { - ensureGlobalWalkPad(); const pad = document.getElementById('aether-walk-pad'); if (!pad) return; const frame = getVisibleDioramaFrame(); - const stage = frame?.closest('.diorama-stage'); - if (stage && pad.parentElement !== stage) stage.appendChild(pad); - const show = !!frame && !!stage && !pad.classList.contains('controls-hidden') && window._aetherCurrentSpread === 'explore'; + const show = !!frame && !pad.classList.contains('controls-hidden') && window._aetherCurrentSpread === 'explore'; pad.classList.toggle('visible', show); - pad.classList.toggle('explore-only', show); + pad.classList.toggle('explore-only', show); }; const wireWalkPad = () => { @@ -2261,7 +1345,6 @@ def build_app() -> gr.Blocks: wireDioramaFrames(); wireWalkPad(); updateWalkPadVisibility(); - wirePageExpand(); }).observe(document.body, { childList: true, subtree: true }); setInterval(updateWalkPadVisibility, 800);