From 20cb279793b93e0563a7c43822219547b1c6174e Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Tue, 25 Nov 2025 20:44:24 -0600 Subject: [PATCH] fix: resolve NPC chat database persistence and modal targeting Fixed two critical bugs in NPC chat functionality: 1. Database Persistence - Metadata serialization bug - Empty dict {} was falsy, preventing JSON conversion - Changed to unconditional serialization in ChatMessageService - Messages now successfully save to chat_messages collection 2. Modal Targeting - HTMX targeting lost during polling - poll_job() wasn't preserving hx-target/hx-swap parameters - Pass targeting params through query string in polling cycle - Responses now correctly load in modal instead of main panel Files modified: - api/app/services/chat_message_service.py - public_web/templates/game/partials/job_polling.html - public_web/app/views/game_views.py --- api/app/services/chat_message_service.py | 6 +++--- public_web/app/views/game_views.py | 14 ++++++++++++-- .../templates/game/partials/job_polling.html | 6 +++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/api/app/services/chat_message_service.py b/api/app/services/chat_message_service.py index 6d54b0c..0bdcff0 100644 --- a/api/app/services/chat_message_service.py +++ b/api/app/services/chat_message_service.py @@ -108,9 +108,9 @@ class ChatMessageService: # Save to database message_data = chat_message.to_dict() - # Convert metadata dict to JSON string for storage - if message_data.get('metadata'): - message_data['metadata'] = json.dumps(message_data['metadata']) + # Convert metadata dict to JSON string for storage (Appwrite requires string type) + # Always convert, even if empty dict (empty dict {} is falsy in Python!) + message_data['metadata'] = json.dumps(message_data.get('metadata') or {}) self.db.create_row( table_id='chat_messages', diff --git a/public_web/app/views/game_views.py b/public_web/app/views/game_views.py index 8bd607a..65f7f1d 100644 --- a/public_web/app/views/game_views.py +++ b/public_web/app/views/game_views.py @@ -506,6 +506,10 @@ def poll_job(session_id: str, job_id: str): """Poll job status - returns updated partial.""" client = get_api_client() + # Get hx_target and hx_swap from query params (passed through from original request) + hx_target = request.args.get('_hx_target') + hx_swap = request.args.get('_hx_swap') + try: response = client.get(f'/api/v1/jobs/{job_id}/status') result = response.get('result', {}) @@ -540,11 +544,14 @@ def poll_job(session_id: str, job_id: str): else: # Still processing - return polling partial to continue + # Pass through hx_target and hx_swap to maintain targeting return render_template( 'game/partials/job_polling.html', session_id=session_id, job_id=job_id, - status=status + status=status, + hx_target=hx_target, + hx_swap=hx_swap ) except APIError as e: @@ -767,12 +774,15 @@ def talk_to_npc(session_id: str, npc_id: str): job_id = result.get('job_id') if job_id: # Return job polling partial for the chat area + # Use hx-target="this" and hx-swap="outerHTML" to replace loading div with response in-place return render_template( 'game/partials/job_polling.html', job_id=job_id, session_id=session_id, status='queued', - is_npc_dialogue=True + is_npc_dialogue=True, + hx_target='this', # Target the loading div itself + hx_swap='outerHTML' # Replace entire loading div with response ) # Immediate response (if AI is sync or cached) diff --git a/public_web/templates/game/partials/job_polling.html b/public_web/templates/game/partials/job_polling.html index ad9e2d4..74a870b 100644 --- a/public_web/templates/game/partials/job_polling.html +++ b/public_web/templates/game/partials/job_polling.html @@ -10,10 +10,10 @@ Shows loading state while waiting for AI response, auto-polls for completion {% endif %}
+ hx-swap="{{ hx_swap|default('innerHTML') }}" + hx-target="{{ hx_target|default('#narrative-content') }}">

{% if status == 'queued' %}