chat history with the NPC modal

This commit is contained in:
2025-11-25 21:16:01 -06:00
parent 20cb279793
commit 196346165f
8 changed files with 244 additions and 46 deletions

View File

@@ -29,7 +29,7 @@ logger = get_logger(__file__)
chat_bp = Blueprint('chat', __name__)
@chat_bp.route('/characters/<character_id>/chats', methods=['GET'])
@chat_bp.route('/api/v1/characters/<character_id>/chats', methods=['GET'])
@require_auth
def get_conversations_summary(character_id: str):
"""
@@ -76,7 +76,7 @@ def get_conversations_summary(character_id: str):
return error_response(f"Failed to retrieve conversations: {str(e)}", 500)
@chat_bp.route('/characters/<character_id>/chats/<npc_id>', methods=['GET'])
@chat_bp.route('/api/v1/characters/<character_id>/chats/<npc_id>', methods=['GET'])
@require_auth
def get_conversation_history(character_id: str, npc_id: str):
"""
@@ -160,7 +160,7 @@ def get_conversation_history(character_id: str, npc_id: str):
return error_response(f"Failed to retrieve conversation: {str(e)}", 500)
@chat_bp.route('/characters/<character_id>/chats/search', methods=['GET'])
@chat_bp.route('/api/v1/characters/<character_id>/chats/search', methods=['GET'])
@require_auth
def search_messages(character_id: str):
"""
@@ -265,7 +265,7 @@ def search_messages(character_id: str):
return error_response(f"Search failed: {str(e)}", 500)
@chat_bp.route('/characters/<character_id>/chats/<message_id>', methods=['DELETE'])
@chat_bp.route('/api/v1/characters/<character_id>/chats/<message_id>', methods=['DELETE'])
@require_auth
def delete_message(character_id: str, message_id: str):
"""

View File

@@ -12,6 +12,7 @@ import structlog
import yaml
import os
from pathlib import Path
from datetime import datetime, timezone
logger = structlog.get_logger(__name__)
@@ -61,6 +62,34 @@ def create_app():
app.register_blueprint(character_bp)
app.register_blueprint(game_bp)
# Register Jinja filters
def format_timestamp(iso_string: str) -> str:
"""Convert ISO timestamp to relative time (e.g., '2 mins ago')"""
if not iso_string:
return ""
try:
timestamp = datetime.fromisoformat(iso_string.replace('Z', '+00:00'))
now = datetime.now(timezone.utc)
diff = now - timestamp
seconds = diff.total_seconds()
if seconds < 60:
return "Just now"
elif seconds < 3600:
mins = int(seconds / 60)
return f"{mins} min{'s' if mins != 1 else ''} ago"
elif seconds < 86400:
hours = int(seconds / 3600)
return f"{hours} hr{'s' if hours != 1 else ''} ago"
else:
days = int(seconds / 86400)
return f"{days} day{'s' if days != 1 else ''} ago"
except Exception as e:
logger.warning("timestamp_format_failed", iso_string=iso_string, error=str(e))
return iso_string
app.jinja_env.filters['format_timestamp'] = format_timestamp
# Register dev blueprint only in development
env = os.getenv("FLASK_ENV", "development")
if env == "development":

View File

@@ -748,6 +748,40 @@ def npc_chat_modal(session_id: str, npc_id: str):
'''
@game_bp.route('/session/<session_id>/npc/<npc_id>/history')
@require_auth
def npc_chat_history(session_id: str, npc_id: str):
"""Get last 5 chat messages for history sidebar."""
client = get_api_client()
try:
# Get session to find character_id
session_response = client.get(f'/api/v1/sessions/{session_id}')
session_data = session_response.get('result', {})
character_id = session_data.get('character_id')
# Fetch last 5 messages from chat service
# API endpoint: GET /api/v1/characters/{character_id}/chats/{npc_id}?limit=5
history_response = client.get(
f'/api/v1/characters/{character_id}/chats/{npc_id}',
params={'limit': 5, 'offset': 0}
)
result_data = history_response.get('result', {})
messages = result_data.get('messages', []) # Extract messages array from result
# Render history partial
return render_template(
'game/partials/npc_chat_history.html',
messages=messages,
session_id=session_id,
npc_id=npc_id
)
except APIError as e:
logger.error("failed_to_load_chat_history", session_id=session_id, npc_id=npc_id, error=str(e))
return '<div class="history-empty">Failed to load history</div>', 500
@game_bp.route('/session/<session_id>/npc/<npc_id>/talk', methods=['POST'])
@require_auth
def talk_to_npc(session_id: str, npc_id: str):

View File

@@ -1033,7 +1033,7 @@
.modal-content--sm { max-width: 400px; }
.modal-content--md { max-width: 500px; }
.modal-content--lg { max-width: 700px; }
.modal-content--xl { max-width: 900px; }
.modal-content--xl { max-width: 1000px; } /* Expanded for 3-column NPC chat */
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
@@ -1434,6 +1434,14 @@
min-height: 400px;
}
/* 3-column layout for chat modal with history sidebar */
.npc-modal-body--three-col {
display: grid;
grid-template-columns: 200px 1fr 280px; /* Profile | Chat | History */
gap: 1rem;
max-height: 70vh;
}
/* NPC Profile (Left Column) */
.npc-profile {
width: 200px;
@@ -1587,18 +1595,20 @@
font-weight: 600;
}
/* NPC Conversation (Right Column) */
/* NPC Conversation (Middle Column) */
.npc-conversation {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0; /* Important for grid child to enable scrolling */
}
.npc-conversation .chat-history {
flex: 1;
min-height: 250px;
max-height: none;
max-height: 500px; /* Set max height to enable scrolling */
overflow-y: auto; /* Enable vertical scroll */
}
.chat-empty-state {
@@ -1608,6 +1618,87 @@
font-style: italic;
}
/* ===== NPC CHAT HISTORY SIDEBAR ===== */
.npc-history-panel {
display: flex;
flex-direction: column;
border-left: 1px solid var(--play-border);
padding-left: 1rem;
overflow-y: auto;
max-height: 70vh;
}
.history-header {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 0.75rem;
letter-spacing: 0.05em;
}
.history-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Compact history cards */
.history-card {
background: var(--bg-secondary);
border: 1px solid var(--play-border);
border-radius: 4px;
padding: 0.5rem;
font-size: 0.8rem;
transition: background 0.2s;
}
.history-card:hover {
background: var(--bg-tertiary);
}
.history-timestamp {
font-size: 0.7rem;
color: var(--text-muted);
margin-bottom: 0.25rem;
font-style: italic;
}
.history-player {
color: var(--text-primary);
margin-bottom: 0.125rem;
line-height: 1.4;
}
.history-player strong {
color: var(--accent-gold);
}
.history-npc {
color: var(--text-primary);
line-height: 1.4;
}
.history-npc strong {
color: var(--accent-gold);
}
.history-empty {
text-align: center;
color: var(--text-muted);
font-size: 0.875rem;
padding: 2rem 0;
font-style: italic;
}
.loading-state-small {
text-align: center;
color: var(--text-muted);
font-size: 0.875rem;
padding: 1rem 0;
font-style: italic;
}
/* Responsive NPC Modal */
@media (max-width: 700px) {
.npc-modal-body {
@@ -1650,6 +1741,31 @@
}
}
/* Responsive: 3-column modal stacks on smaller screens */
@media (max-width: 1024px) {
.npc-modal-body--three-col {
grid-template-columns: 1fr; /* Single column */
grid-template-rows: auto 1fr auto;
}
.npc-profile {
order: 1;
}
.npc-conversation {
order: 2;
}
.npc-history-panel {
order: 3;
border-left: none;
border-top: 1px solid var(--play-border);
padding-left: 0;
padding-top: 1rem;
max-height: 200px; /* Shorter on mobile */
}
}
/* ===== UTILITY CLASSES FOR PLAY SCREEN ===== */
.play-hidden {
display: none !important;

View File

@@ -16,6 +16,8 @@
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- HTMX JSON encoding extension -->
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/json-enc.js"></script>
<!-- Hyperscript for custom events -->
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
{% block extra_head %}{% endblock %}
</head>

View File

@@ -0,0 +1,29 @@
{#
NPC Chat History Sidebar
Shows last 5 messages in compact cards with timestamps
#}
<div class="chat-history-sidebar">
<h4 class="history-header">Recent Messages</h4>
{% if messages %}
<div class="history-list">
{% for msg in messages|reverse %}
<div class="history-card">
<div class="history-timestamp">
{{ msg.timestamp|format_timestamp }}
</div>
<div class="history-player">
<strong>You:</strong> {{ msg.player_message|truncate(60) }}
</div>
<div class="history-npc">
<strong>NPC:</strong> {{ msg.npc_response|truncate(60) }}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="history-empty">
No previous messages
</div>
{% endif %}
</div>

View File

@@ -3,15 +3,15 @@ NPC Chat Modal (Expanded)
Shows NPC profile with portrait, relationship meter, and conversation interface
#}
<div class="modal-overlay" onclick="if(event.target === this) closeModal()">
<div class="modal-content modal-content--lg">
<div class="modal-content modal-content--xl">
{# Modal Header #}
<div class="modal-header">
<h3 class="modal-title">{{ npc.name }}</h3>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
{# Modal Body - Two Column Layout #}
<div class="modal-body npc-modal-body">
{# Modal Body - Three Column Layout #}
<div class="modal-body npc-modal-body npc-modal-body--three-col">
{# Left Column: NPC Profile #}
<div class="npc-profile">
{# NPC Portrait #}
@@ -115,6 +115,17 @@ Shows NPC profile with portrait, relationship meter, and conversation interface
</button>
</form>
</div>
{# Right Column: Message History Sidebar #}
<aside class="npc-history-panel"
id="npc-history-{{ npc.npc_id }}"
hx-get="{{ url_for('game.npc_chat_history', session_id=session_id, npc_id=npc.npc_id) }}"
hx-trigger="load, newMessage from:body"
hx-swap="innerHTML"
hx-indicator=".history-loading">
{# History loaded via HTMX #}
<div class="loading-state-small history-loading">Loading history...</div>
</aside>
</div>
{# Modal Footer #}

View File

@@ -11,46 +11,23 @@ Expected context:
- session_id: For any follow-up actions
#}
<div class="npc-dialogue-response">
<div class="npc-dialogue-header">
<span class="npc-dialogue-title">{{ npc_name }} says:</span>
</div>
<div class="npc-dialogue-content">
{# Show conversation history if present #}
{% if conversation_history %}
<div class="conversation-history">
{% for exchange in conversation_history[-3:] %}
<div class="history-exchange">
<div class="history-player">
<span class="speaker player">{{ character_name }}:</span>
<span class="text">{{ exchange.player_line }}</span>
</div>
<div class="history-npc">
<span class="speaker npc">{{ npc_name }}:</span>
<span class="text">{{ exchange.npc_response }}</span>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{# Show current exchange #}
{# Only show CURRENT exchange (removed conversation_history loop) #}
<div class="current-exchange">
{% if player_line %}
<div class="player-message">
<span class="speaker player">{{ character_name }}:</span>
<span class="text">{{ player_line }}</span>
<div class="chat-message chat-message--player">
<strong>{{ character_name }}:</strong> {{ player_line }}
</div>
{% endif %}
<div class="npc-message">
<span class="speaker npc">{{ npc_name }}:</span>
<span class="text">{{ dialogue }}</span>
</div>
</div>
<div class="chat-message chat-message--npc">
<strong>{{ npc_name }}:</strong> {{ dialogue }}
</div>
</div>
{# Trigger history refresh after new message #}
<div hx-trigger="load"
_="on load trigger newMessage on body"
style="display: none;"></div>
{# Trigger sidebar refreshes after NPC dialogue #}
<div hx-get="{{ url_for('game.npcs_accordion', session_id=session_id) }}"
hx-trigger="load"