|
13 | 13 | load_dotenv() |
14 | 14 |
|
15 | 15 | from world.database import get_world_state, init_database |
| 16 | +from world.quests import assign_quest, get_entity_quest, get_active_quests |
| 17 | +from world.presence import heartbeat as presence_heartbeat, get_active_count, get_recent_arrivals |
16 | 18 | from world.entities import ( |
17 | 19 | check_rate_limit, |
18 | 20 | get_all_entities, |
@@ -301,6 +303,40 @@ def chat_with_soul(soul_id: str, player_message: str) -> str: |
301 | 303 | return entity.get("greeting") or "..." |
302 | 304 |
|
303 | 305 |
|
| 306 | +def assign_quest_api(entity_id: str, quest_title: str) -> str: |
| 307 | + """Assign a quest to a soul. Called from the diorama iframe via Gradio API.""" |
| 308 | + if not entity_id or not quest_title or not quest_title.strip(): |
| 309 | + return "" |
| 310 | + try: |
| 311 | + from world.entities import get_entity |
| 312 | + entity = get_entity(entity_id) |
| 313 | + if not entity: |
| 314 | + return "Soul not found." |
| 315 | + q = assign_quest(entity_id, quest_title.strip()[:200]) |
| 316 | + return f"Quest assigned: {q['title']}" |
| 317 | + except Exception as e: |
| 318 | + return f"Could not assign quest: {e}" |
| 319 | + |
| 320 | + |
| 321 | +def get_live_presence(session_id: str) -> str: |
| 322 | + """Heartbeat + return live visitor HTML.""" |
| 323 | + try: |
| 324 | + presence_heartbeat(session_id) |
| 325 | + count = get_active_count() |
| 326 | + arrivals = get_recent_arrivals(since_seconds=120) |
| 327 | + parts = [] |
| 328 | + if count > 1: |
| 329 | + parts.append(f'<span class="presence-count">⬡ {count} visitors in the Realm right now</span>') |
| 330 | + if arrivals: |
| 331 | + names = ", ".join(a["display_name"] for a in arrivals[:3]) |
| 332 | + parts.append(f'<span class="presence-arrivals">Recently summoned: {names}</span>') |
| 333 | + if not parts: |
| 334 | + return "" |
| 335 | + return f'<div class="presence-bar">{" · ".join(parts)}</div>' |
| 336 | + except Exception: |
| 337 | + return "" |
| 338 | + |
| 339 | + |
304 | 340 | def trigger_live_tick() -> str: |
305 | 341 | """Run one simulation tick on demand and return a summary.""" |
306 | 342 | try: |
@@ -381,6 +417,7 @@ def build_app() -> gr.Blocks: |
381 | 417 | with gr.Blocks(**blocks_kwargs) as demo: |
382 | 418 | session_id = gr.State(value=str(uuid.uuid4())) |
383 | 419 |
|
| 420 | + presence_bar = gr.HTML(value="", elem_id="presence-bar") |
384 | 421 | header = gr.HTML(value=get_header_html(), elem_classes=["realm-header-wrap"]) |
385 | 422 |
|
386 | 423 | with gr.Tabs(elem_classes=["realm-tabs"]) as tabs: |
@@ -689,18 +726,36 @@ def _map_open_diorama(pick): |
689 | 726 | outputs=[world_map, header, current_event, activity_feed, world_vignette, featured_chars, realm_pulse], |
690 | 727 | ) |
691 | 728 |
|
692 | | - # ── Soul chat API (called from diorama iframe via fetch) ── |
| 729 | + # ── Soul chat API ── |
693 | 730 | chat_soul_id_comp = gr.Textbox(visible=False, elem_id="chat_soul_id_input") |
694 | 731 | chat_msg_comp = gr.Textbox(visible=False, elem_id="chat_message_input") |
695 | 732 | chat_resp_comp = gr.Textbox(visible=False, elem_id="chat_response_output") |
696 | | - |
697 | 733 | chat_msg_comp.change( |
698 | 734 | fn=chat_with_soul, |
699 | 735 | inputs=[chat_soul_id_comp, chat_msg_comp], |
700 | 736 | outputs=[chat_resp_comp], |
701 | 737 | api_name="chat_with_soul", |
702 | 738 | ) |
703 | 739 |
|
| 740 | + # ── Quest API ── |
| 741 | + quest_entity_comp = gr.Textbox(visible=False, elem_id="quest_entity_input") |
| 742 | + quest_title_comp = gr.Textbox(visible=False, elem_id="quest_title_input") |
| 743 | + quest_resp_comp = gr.Textbox(visible=False, elem_id="quest_response_output") |
| 744 | + quest_title_comp.change( |
| 745 | + fn=assign_quest_api, |
| 746 | + inputs=[quest_entity_comp, quest_title_comp], |
| 747 | + outputs=[quest_resp_comp], |
| 748 | + api_name="assign_quest", |
| 749 | + ) |
| 750 | + |
| 751 | + # ── Presence heartbeat (every 10s) ── |
| 752 | + presence_timer = gr.Timer(10) |
| 753 | + presence_timer.tick( |
| 754 | + fn=get_live_presence, |
| 755 | + inputs=[session_id], |
| 756 | + outputs=[presence_bar], |
| 757 | + ) |
| 758 | + |
704 | 759 | book_filter.change(filter_book, [book_filter, book_search], [book_display]) |
705 | 760 | book_search.change(filter_book, [book_filter, book_search], [book_display]) |
706 | 761 |
|
@@ -776,7 +831,10 @@ def _map_open_diorama(pick): |
776 | 831 | "server_name": "0.0.0.0", |
777 | 832 | "server_port": int(os.environ.get("PORT", 7860)), |
778 | 833 | "share": False, |
779 | | - "allowed_paths": assets.allowed_paths() + [str(Path(tempfile.gettempdir()) / "aether_soul_cards")], |
| 834 | + "allowed_paths": assets.allowed_paths() + [ |
| 835 | + str(Path(tempfile.gettempdir()) / "aether_soul_cards"), |
| 836 | + str(Path(__file__).parent / "assets" / "generated"), |
| 837 | + ], |
780 | 838 | } |
781 | 839 | if GRADIO_MAJOR >= 6: |
782 | 840 | launch_kwargs["css"] = CUSTOM_CSS |
|
0 commit comments