diff --git a/api/app/api/chat.py b/api/app/api/chat.py index e8a8a0d..ab21139 100644 --- a/api/app/api/chat.py +++ b/api/app/api/chat.py @@ -29,7 +29,7 @@ logger = get_logger(__file__) chat_bp = Blueprint('chat', __name__) -@chat_bp.route('/characters//chats', methods=['GET']) +@chat_bp.route('/api/v1/characters//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//chats/', methods=['GET']) +@chat_bp.route('/api/v1/characters//chats/', 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//chats/search', methods=['GET']) +@chat_bp.route('/api/v1/characters//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//chats/', methods=['DELETE']) +@chat_bp.route('/api/v1/characters//chats/', methods=['DELETE']) @require_auth def delete_message(character_id: str, message_id: str): """ diff --git a/public_web/app/__init__.py b/public_web/app/__init__.py index 46d552d..1f5c8a6 100644 --- a/public_web/app/__init__.py +++ b/public_web/app/__init__.py @@ -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": diff --git a/public_web/app/views/game_views.py b/public_web/app/views/game_views.py index 65f7f1d..60cd446 100644 --- a/public_web/app/views/game_views.py +++ b/public_web/app/views/game_views.py @@ -748,6 +748,40 @@ def npc_chat_modal(session_id: str, npc_id: str): ''' +@game_bp.route('/session//npc//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 '
Failed to load history
', 500 + + @game_bp.route('/session//npc//talk', methods=['POST']) @require_auth def talk_to_npc(session_id: str, npc_id: str): diff --git a/public_web/static/css/play.css b/public_web/static/css/play.css index 6f133c3..55cc7be 100644 --- a/public_web/static/css/play.css +++ b/public_web/static/css/play.css @@ -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; diff --git a/public_web/templates/base.html b/public_web/templates/base.html index 733e50b..da0ce13 100644 --- a/public_web/templates/base.html +++ b/public_web/templates/base.html @@ -16,6 +16,8 @@ + + {% block extra_head %}{% endblock %} diff --git a/public_web/templates/game/partials/npc_chat_history.html b/public_web/templates/game/partials/npc_chat_history.html new file mode 100644 index 0000000..3a8fa3b --- /dev/null +++ b/public_web/templates/game/partials/npc_chat_history.html @@ -0,0 +1,29 @@ +{# +NPC Chat History Sidebar +Shows last 5 messages in compact cards with timestamps +#} +
+

Recent Messages

+ + {% if messages %} +
+ {% for msg in messages|reverse %} +
+
+ {{ msg.timestamp|format_timestamp }} +
+
+ You: {{ msg.player_message|truncate(60) }} +
+
+ NPC: {{ msg.npc_response|truncate(60) }} +
+
+ {% endfor %} +
+ {% else %} +
+ No previous messages +
+ {% endif %} +
diff --git a/public_web/templates/game/partials/npc_chat_modal.html b/public_web/templates/game/partials/npc_chat_modal.html index 8e59bc4..8bbb574 100644 --- a/public_web/templates/game/partials/npc_chat_modal.html +++ b/public_web/templates/game/partials/npc_chat_modal.html @@ -3,15 +3,15 @@ NPC Chat Modal (Expanded) Shows NPC profile with portrait, relationship meter, and conversation interface #}