From 641d676f2fc1aefa8a1d186fccde6282582d9d9e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 Aug 2025 13:28:39 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Complete=205=20Whys=20agent=20en?= =?UTF-8?q?hancements=20with=20session=20management=20and=20UI=20improveme?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Major Features Added: ### 🕒 Session Time Limits (2hr → 30min) - Updated ChatSession model: expires_at, extend_session methods - Changed session creation and extension logic - Updated JavaScript timing calculations - Fixed timing indicator functionality with real-time server data ### 💬 Message Limits (50 → 20) - Added message_limit field to Agent model with migration - Implemented auto-completion when message limit reached - Only count user messages (not agent responses) toward limit - Enhanced error messaging for limit exceeded ### 📥 Chat Download System - Added PDF and TXT export functionality using ReportLab - Download buttons in chat header for active sessions - Comprehensive export with session metadata and formatting - New API endpoints: /agents/api/chat/export/{session_id}/ ### 📋 Previous Sessions Management - Created Previous Sessions widget showing last 5 non-active sessions - Download access for all completed sessions with messages - Session status indicators (✅ Completed, ⏰ Expired, ❌ Abandoned) - Clean sidebar organization with session history ### 🎨 UI/UX Improvements - Moved session info from main chat to sidebar for cleaner interface - Increased chat height: 600px → 800px (mobile: 500px → 650px) - Added Current Session widget with status, ID, and start time - Enhanced sidebar layout with proper component stacking - Responsive design with mobile optimizations ### 🔧 Session Indicators System - Real-time session status API with server-side data - Progress bars for time remaining and message usage - Warning notifications for approaching limits - Automatic updates every 30 seconds ### 💰 Payment Flow Fixes - Fixed charging system to actually deduct wallet balance - Each new session charges 15 AED for 20 messages + 30 minutes - Proper error handling for insufficient balance - Session cleanup on payment failures ## Technical Implementation: **Backend Changes:** - Enhanced ChatSession model with 30-minute duration - Message counting logic (user messages only) - Auto-completion on limit reached - Session status API endpoint - PDF/TXT export with ReportLab **Frontend Changes:** - Comprehensive sidebar widget system - Real-time indicator updates with server sync - Download functionality with loading states - Session history display with status icons - Responsive layout improvements **Database:** - Added message_limit field to Agent model - Updated ChatSession expiration logic - Efficient queries for session history ## User Experience: ✅ Clear session limits (20 messages, 30 minutes) ✅ Automatic session completion with preserved history ✅ Easy download access for all previous sessions ✅ Transparent pricing (15 AED per session) ✅ Clean, organized interface with more chat space ✅ Real-time feedback on session progress 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../commands/create_five_whys_agent.py | 11 +- agents/migrations/0004_add_message_limit.py | 21 + agents/models.py | 11 +- agents/templates/agents/agent_chat.html | 358 ++++++++++++++++-- agents/urls.py | 2 + agents/views.py | 287 +++++++++++++- static/css/agent-base.css | 8 +- .../components/current_session_widget.html | 119 ++++++ .../components/previous_sessions_widget.html | 206 ++++++++++ templates/components/quick_agents_panel.html | 40 +- templates/components/session_indicators.html | 200 ++++++++++ 11 files changed, 1190 insertions(+), 73 deletions(-) create mode 100644 agents/migrations/0004_add_message_limit.py create mode 100644 templates/components/current_session_widget.html create mode 100644 templates/components/previous_sessions_widget.html create mode 100644 templates/components/session_indicators.html diff --git a/agents/management/commands/create_five_whys_agent.py b/agents/management/commands/create_five_whys_agent.py index dac2df0..20b616f 100644 --- a/agents/management/commands/create_five_whys_agent.py +++ b/agents/management/commands/create_five_whys_agent.py @@ -27,18 +27,11 @@ class Command(BaseCommand): defaults={ 'name': '5 Whys Analysis', 'short_description': 'Interactive problem-solving using the proven 5 Whys methodology', - 'description': '''Discover the root cause of any problem through guided conversation using the 5 Whys technique. - -This interactive agent helps you systematically drill down to the core issue by asking "why" five times. Perfect for: -• Troubleshooting operational problems -• Understanding process failures -• Identifying systemic issues -• Improving quality and efficiency - -The conversation-based approach ensures you think deeply about each layer of the problem, leading to more effective solutions.''', + 'description': '''Systematically find root causes through guided 5 Whys methodology. Perfect for troubleshooting operational problems, understanding failures, and identifying systemic issues.''', 'category': category, 'price': 15.00, 'agent_type': 'chat', # This is a chat-based agent + 'message_limit': 20, # Limit messages per session 'form_schema': None, # Chat agents don't use form schemas 'webhook_url': 'http://localhost:5678/webhook/5-whys-web', # N8N webhook URL 'is_active': True diff --git a/agents/migrations/0004_add_message_limit.py b/agents/migrations/0004_add_message_limit.py new file mode 100644 index 0000000..dfb0dd9 --- /dev/null +++ b/agents/migrations/0004_add_message_limit.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.4 on 2025-08-03 04:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("agents", "0003_chatsession_expires_at_alter_chatsession_status"), + ] + + operations = [ + migrations.AddField( + model_name="agent", + name="message_limit", + field=models.IntegerField( + default=50, + help_text="Maximum messages per chat session (for chat agents)", + ), + ), + ] diff --git a/agents/models.py b/agents/models.py index 97cae91..8041e8d 100644 --- a/agents/models.py +++ b/agents/models.py @@ -32,6 +32,7 @@ class Agent(models.Model): agent_type = models.CharField(max_length=10, choices=AGENT_TYPE_CHOICES, default='form', help_text="Agent interaction type") form_schema = models.JSONField(help_text="JSON schema for agent input form", null=True, blank=True) webhook_url = models.URLField(help_text="n8n webhook URL for execution") + message_limit = models.IntegerField(default=50, help_text="Maximum messages per chat session (for chat agents)") is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -85,17 +86,17 @@ class ChatSession(models.Model): status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active') context_data = models.JSONField(default=dict, help_text="Session context and progress tracking") fee_charged = models.DecimalField(max_digits=10, decimal_places=2) - expires_at = models.DateTimeField(null=True, blank=True, help_text="Session expiration time (2 hours from last activity)") + expires_at = models.DateTimeField(null=True, blank=True, help_text="Session expiration time (30 minutes from last activity)") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) completed_at = models.DateTimeField(null=True, blank=True) def save(self, *args, **kwargs): - # Set expires_at to 2 hours from now if not set + # Set expires_at to 30 minutes from now if not set if not self.expires_at: from django.utils import timezone from datetime import timedelta - self.expires_at = timezone.now() + timedelta(hours=2) + self.expires_at = timezone.now() + timedelta(minutes=30) super().save(*args, **kwargs) class Meta: @@ -115,10 +116,10 @@ class ChatSession(models.Model): return timezone.now() > self.expires_at def extend_session(self): - """Extend session by 2 hours from now""" + """Extend session by 30 minutes from now""" from django.utils import timezone from datetime import timedelta - self.expires_at = timezone.now() + timedelta(hours=2) + self.expires_at = timezone.now() + timedelta(minutes=30) self.updated_at = timezone.now() self.save() diff --git a/agents/templates/agents/agent_chat.html b/agents/templates/agents/agent_chat.html index fde5629..5085ae9 100644 --- a/agents/templates/agents/agent_chat.html +++ b/agents/templates/agents/agent_chat.html @@ -83,7 +83,7 @@ border: 1px solid var(--outline); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); - height: 600px; + height: 800px; display: flex; flex-direction: column; } @@ -94,6 +94,9 @@ background: var(--gradient-primary); color: white; border-radius: var(--radius-lg) var(--radius-lg) 0 0; + display: flex; + justify-content: space-between; + align-items: flex-start; } .chat-title { @@ -265,7 +268,7 @@ } .chat-widget { - height: 500px; + height: 650px; } .message-bubble { @@ -310,6 +313,92 @@ font-size: 14px; opacity: 0.7; } + +/* Download dropdown styles */ +.download-dropdown { + position: relative; +} + +.download-btn { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + padding: 8px 16px; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.download-btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-1px); +} + +.download-menu { + position: absolute; + top: 100%; + right: 0; + background: white; + border: 1px solid var(--outline); + border-radius: 6px; + box-shadow: var(--shadow-lg); + min-width: 160px; + z-index: 1000; + display: none; + margin-top: 4px; +} + +.download-menu.show { + display: block; +} + +.download-option { + display: block; + padding: 12px 16px; + color: var(--on-surface); + text-decoration: none; + font-size: 14px; + border-bottom: 1px solid var(--outline-variant); + transition: background 0.2s ease; +} + +.download-option:last-child { + border-bottom: none; +} + +.download-option:hover { + background: var(--surface-variant); + color: var(--on-surface); +} + +/* Sidebar layout */ +.agent-sidebar { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + flex: 0 0 auto; + min-width: 300px; +} + +/* 5 Whys specific spacing adjustments */ +.agent-container { + padding: var(--spacing-sm) !important; +} + +.agent-header { + margin-bottom: 20px !important; +} + +.agent-grid { + padding: 0 !important; + margin: 0 !important; +} + +.agent-widget.widget-large { + padding: 20px !important; + margin: 0 !important; +} {% endblock %} @@ -327,10 +416,12 @@ document.body.setAttribute('data-user-id', '{{ user.id }}'); {% endif %} {% if chat_session %} document.body.setAttribute('data-session-id', '{{ chat_session.session_id }}'); +document.body.setAttribute('data-session-created', '{{ chat_session.created_at.isoformat }}'); +document.body.setAttribute('data-session-expires', '{{ chat_session.expires_at.isoformat }}'); {% endif %} -
+
{% include "components/agent_header.html" with agent_title=agent.name agent_subtitle=agent.short_description %} @@ -343,29 +434,34 @@ document.body.setAttribute('data-session-id', '{{ chat_session.session_id }}');
{% if user.is_authenticated %} {% if user.wallet_balance >= agent.price or chat_session %} - - {% if chat_session %} -
-
- 💬 Chat Session: {{ chat_session.get_status_display }} - {% if chat_session.status == 'active' %} - • Active - {% elif chat_session.status == 'expired' %} - • Expired - {% endif %} -
-
ID: {{ chat_session.session_id }}
-
- {% endif %}
-

- {{ agent.category.icon }} - {{ agent.name }} -

-

{{ agent.description }}

+
+

+ {{ agent.category.icon }} + {{ agent.name }} +

+

{{ agent.description }}

+
+ {% if chat_session and messages %} +
+ +
+ {% endif %}
@@ -463,15 +559,19 @@ document.body.setAttribute('data-session-id', '{{ chat_session.session_id }}'); {% endif %}
- +
- {% if user.is_authenticated %} - - {% include "components/wallet_card.html" %} - {% endif %} - {% include "components/how_it_works_widget.html" with steps="agents" %} + + + {% include "components/current_session_widget.html" %} + + + {% include "components/session_indicators.html" %} + + + {% include "components/previous_sessions_widget.html" %}
{% endblock %} @@ -585,6 +685,10 @@ class ChatInterface { if (response.ok) { // Add agent response this.addMessage('agent', data.agent_message.content); + // Update message count indicators + if (typeof updateMessageCount === 'function') { + updateMessageCount(); + } } else { this.showError(data.error || 'Failed to send message'); } @@ -657,8 +761,206 @@ class ChatInterface { // Initialize chat interface when page loads document.addEventListener('DOMContentLoaded', function() { new ChatInterface(); + + // Clear any old localStorage data for sessions + const sessionId = document.body.getAttribute('data-session-id'); + if (sessionId) { + // Clear old session start time cache + localStorage.removeItem(`session_start_${sessionId}`); + } + + // Initialize session indicators updates + if (document.getElementById('timeRemaining')) { + updateSessionIndicators(); + setInterval(updateSessionIndicators, 30000); // Update every 30 seconds for better accuracy + } }); +// Download functionality +function toggleDownloadMenu() { + const menu = document.getElementById('downloadMenu'); + menu.classList.toggle('show'); +} + +function downloadChat(format) { + const sessionId = document.body.getAttribute('data-session-id'); + if (!sessionId) { + alert('No active session found'); + return; + } + + // Hide the dropdown menu + document.getElementById('downloadMenu').classList.remove('show'); + + // Show loading state + const downloadBtn = document.getElementById('downloadBtn'); + const originalText = downloadBtn.textContent; + downloadBtn.textContent = '⏳ Preparing...'; + downloadBtn.disabled = true; + + // Create download URL + const downloadUrl = `/agents/api/chat/export/${sessionId}/?format=${format}`; + + // Create a temporary link and trigger download + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = `5whys_chat_${sessionId}.${format}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Reset button state after a short delay + setTimeout(() => { + downloadBtn.textContent = originalText; + downloadBtn.disabled = false; + }, 2000); +} + +// Close dropdown when clicking outside +document.addEventListener('click', function(event) { + const dropdown = document.querySelector('.download-dropdown'); + const menu = document.getElementById('downloadMenu'); + + if (dropdown && !dropdown.contains(event.target)) { + menu.classList.remove('show'); + } +}); + +// Session indicators real-time updates +function updateSessionIndicators() { + const sessionId = document.body.getAttribute('data-session-id'); + if (!sessionId) return; + + // Fetch real session data from server + fetch(`/agents/api/chat/session/${sessionId}/status/`, { + method: 'GET', + headers: { + 'X-CSRFToken': getCSRFToken(), + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + updateTimeIndicator(data.time_remaining_seconds, data.time_remaining_str); + updateMessageIndicator(data.message_count, data.message_limit); + updateWarnings(data.time_percentage, data.message_percentage); + } + }) + .catch(error => { + console.error('Error fetching session status:', error); + // Fallback to approximate calculation + updateSessionIndicatorsApproximate(); + }); +} + +function updateSessionIndicatorsApproximate() { + const sessionId = document.body.getAttribute('data-session-id'); + if (!sessionId) return; + + // Use actual session expiration time from server + const sessionExpiresStr = document.body.getAttribute('data-session-expires'); + + if (sessionExpiresStr) { + const sessionExpires = new Date(sessionExpiresStr); + const now = new Date(); + const timeRemaining = Math.max(0, sessionExpires - now); + + const minutesRemaining = Math.floor(timeRemaining / (1000 * 60)); + const timeRemainingStr = `${minutesRemaining}m`; + + const sessionDuration = 30 * 60 * 1000; // 30 minutes in milliseconds + const timePercentage = Math.max(0, (timeRemaining / sessionDuration) * 100); + + updateTimeIndicator(timeRemaining / 1000, timeRemainingStr); + + // For messages, count current user messages + const messageElements = document.querySelectorAll('.message.user'); + const currentCount = messageElements.length; + const maxMessages = 20; + updateMessageIndicator(currentCount, maxMessages); + + updateWarnings(timePercentage, (currentCount / maxMessages) * 100); + } else { + console.warn('No session expiration data available'); + } +} + +function updateTimeIndicator(timeRemainingSeconds, timeRemainingStr) { + const timeElement = document.getElementById('timeRemaining'); + const timeProgressElement = document.getElementById('timeProgress'); + + if (timeElement) { + timeElement.textContent = timeRemainingStr; + } + + if (timeProgressElement) { + const totalTime = 30 * 60; // 30 minutes in seconds + const percentage = Math.max(0, (timeRemainingSeconds / totalTime) * 100); + timeProgressElement.style.width = percentage + '%'; + + // Update color based on time remaining + if (percentage <= 20) { + timeProgressElement.style.background = 'linear-gradient(90deg, #ef4444, #dc2626)'; + } else if (percentage <= 40) { + timeProgressElement.style.background = 'linear-gradient(90deg, #f59e0b, #d97706)'; + } else { + timeProgressElement.style.background = 'linear-gradient(90deg, #10b981, #059669)'; + } + } +} + +function updateMessageIndicator(currentCount, maxMessages) { + const messageCountElement = document.getElementById('messageCount'); + const messageProgressElement = document.getElementById('messageProgress'); + + if (messageCountElement) { + messageCountElement.textContent = `${currentCount}/${maxMessages} used`; + } + + if (messageProgressElement) { + const percentage = (currentCount / maxMessages) * 100; + messageProgressElement.style.width = percentage + '%'; + + // Update color based on usage + if (percentage >= 95) { + messageProgressElement.style.background = 'linear-gradient(90deg, #ef4444, #dc2626)'; + } else if (percentage >= 80) { + messageProgressElement.style.background = 'linear-gradient(90deg, #f59e0b, #d97706)'; + } else { + messageProgressElement.style.background = 'linear-gradient(90deg, #3b82f6, #2563eb)'; + } + } +} + +function updateWarnings(timePercentage, messagePercentage) { + const warningsElement = document.getElementById('sessionWarnings'); + if (warningsElement) { + let warningHTML = ''; + + if (messagePercentage >= 80) { + warningHTML += '
⚠️ Approaching message limit!
'; + } + + if (timePercentage <= 20) { + warningHTML += '
⚠️ Session expires soon!
'; + } + + warningsElement.innerHTML = warningHTML; + } +} + +function getCSRFToken() { + const token = document.querySelector('[name=csrfmiddlewaretoken]'); + return token ? token.value : ''; +} + +// Update message count after sending a message +function updateMessageCount() { + // Update session indicators to refresh all counters + updateSessionIndicators(); +} + // Handle wallet balance check document.addEventListener('DOMContentLoaded', function() { const userBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0'); diff --git a/agents/urls.py b/agents/urls.py index d1e055b..381d020 100644 --- a/agents/urls.py +++ b/agents/urls.py @@ -20,7 +20,9 @@ urlpatterns = [ path('api/chat/start/', views.start_chat_session, name='start_chat_session'), path('api/chat/send/', views.send_chat_message, name='send_chat_message'), path('api/chat/history//', views.get_chat_history, name='get_chat_history'), + path('api/chat/session//status/', views.get_session_status, name='get_session_status'), path('api/chat/end/', views.end_chat_session, name='end_chat_session'), + path('api/chat/export//', views.export_chat, name='export_chat'), path('api/', views.agent_list, name='agent_list'), path('api//', views.agent_detail, name='agent_detail_api'), diff --git a/agents/views.py b/agents/views.py index 9800eda..2c953fb 100644 --- a/agents/views.py +++ b/agents/views.py @@ -16,6 +16,13 @@ import time import uuid import ipaddress from urllib.parse import urlparse +from django.http import HttpResponse +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import letter +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer +from reportlab.lib.units import inch +from io import BytesIO def validate_webhook_url(url): """ @@ -337,8 +344,12 @@ def agent_detail_view(request, slug): return chat_agent_view(request, agent) # Handle form-based agents (existing behavior) + # Get all other active agents for quick access panel + all_agents = Agent.objects.filter(is_active=True).exclude(id=agent.id).select_related('category') + context = { 'agent': agent, + 'all_agents': all_agents, 'timestamp': int(time.time()) # For cache busting } @@ -402,11 +413,68 @@ def chat_agent_view(request, agent): if chat_session: messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp') + # Get all other active agents for quick access panel + all_agents = Agent.objects.filter(is_active=True).exclude(id=agent.id).select_related('category') + + # Get previous sessions for this user and agent (excluding current active session) + previous_sessions_query = ChatSession.objects.filter( + agent=agent, + user=request.user + ).exclude(status='active').order_by('-created_at')[:5] # Last 5 non-active sessions + + # Add user message count to each session + previous_sessions = [] + for session in previous_sessions_query: + session.user_message_count = ChatMessage.objects.filter( + session=session, + message_type='user' + ).count() + previous_sessions.append(session) + + # Calculate session indicators data + session_data = {} + if chat_session and messages.exists(): + from django.utils import timezone + import math + + # Time calculations + now = timezone.now() + time_elapsed = now - chat_session.created_at + time_remaining_seconds = max(0, (chat_session.expires_at - now).total_seconds()) + time_remaining_minutes = int(time_remaining_seconds // 60) + time_remaining_hours = time_remaining_minutes // 60 + time_remaining_minutes = time_remaining_minutes % 60 + + if time_remaining_hours > 0: + time_remaining_str = f"{time_remaining_hours}h {time_remaining_minutes}m" + else: + time_remaining_str = f"{time_remaining_minutes}m" + + # Time percentage (how much time is left) + total_session_time = 30 * 60 # 30 minutes in seconds + time_percentage = max(0, min(100, (time_remaining_seconds / total_session_time) * 100)) + + # Message calculations (only count user messages) + user_message_count = messages.filter(message_type='user').count() + message_limit = agent.message_limit + message_percentage = min(100, (user_message_count / message_limit) * 100) + + session_data = { + 'time_remaining': time_remaining_str, + 'time_percentage': int(time_percentage), + 'message_count': user_message_count, + 'message_limit': message_limit, + 'message_percentage': int(message_percentage), + } + context = { 'agent': agent, 'chat_session': chat_session, 'messages': messages, - 'timestamp': int(time.time()) + 'all_agents': all_agents, + 'previous_sessions': previous_sessions, + 'timestamp': int(time.time()), + **session_data # Unpack session data into context } return render(request, 'agents/agent_chat.html', context) @@ -452,11 +520,28 @@ def start_chat_session(request): user=request.user, fee_charged=agent.price, status='active', - expires_at=timezone.now() + timedelta(hours=2) + expires_at=timezone.now() + timedelta(minutes=30) ) - # Deduct fee from wallet (if wallet system is implemented) - # This would integrate with the existing wallet system + # Deduct fee from wallet + try: + success = request.user.deduct_balance( + agent.price, + f'{agent.name} - Chat Session {session_id}', + agent.slug + ) + if not success: + # Delete the created session if payment fails + chat_session.delete() + return Response({ + 'error': 'Failed to process payment. Please check your wallet balance.' + }, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + # Delete the created session if payment processing fails + chat_session.delete() + return Response({ + 'error': 'Payment processing error. Please try again.' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # Send welcome message welcome_message = f"Welcome to {agent.name}! I'm here to help you with 5 Whys analysis. What problem would you like to analyze?" @@ -497,6 +582,18 @@ def send_chat_message(request): chat_session.save() return Response({'error': 'Chat session has expired'}, status=status.HTTP_400_BAD_REQUEST) + # Check message limit (only count user messages) + current_user_message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count() + if current_user_message_count >= chat_session.agent.message_limit: + # Auto-complete the session when message limit is reached + chat_session.status = 'completed' + chat_session.completed_at = timezone.now() + chat_session.save() + + return Response({ + 'error': f'Message limit reached ({chat_session.agent.message_limit} messages). Session completed. You can download your conversation or start a new session.' + }, status=status.HTTP_400_BAD_REQUEST) + # Save user message user_message = ChatMessage.objects.create( session=chat_session, @@ -679,3 +776,185 @@ def end_chat_session(request): chat_session.save() return Response({'message': 'Chat session ended successfully'}) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_session_status(request, session_id): + """Get real-time session status data""" + chat_session = get_object_or_404( + ChatSession, + session_id=session_id, + user=request.user + ) + + from django.utils import timezone + + # Time calculations + now = timezone.now() + time_remaining_seconds = max(0, (chat_session.expires_at - now).total_seconds()) + time_remaining_minutes = int(time_remaining_seconds // 60) + time_remaining_hours = time_remaining_minutes // 60 + time_remaining_minutes = time_remaining_minutes % 60 + + if time_remaining_hours > 0: + time_remaining_str = f"{time_remaining_hours}h {time_remaining_minutes}m" + else: + time_remaining_str = f"{time_remaining_minutes}m" + + # Time percentage (how much time is left) + total_session_time = 30 * 60 # 30 minutes in seconds + time_percentage = max(0, min(100, (time_remaining_seconds / total_session_time) * 100)) + + # Message calculations (only count user messages) + message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count() + message_limit = chat_session.agent.message_limit + message_percentage = min(100, (message_count / message_limit) * 100) + + return Response({ + 'success': True, + 'session_id': session_id, + 'status': chat_session.status, + 'time_remaining_seconds': int(time_remaining_seconds), + 'time_remaining_str': time_remaining_str, + 'time_percentage': int(time_percentage), + 'message_count': message_count, + 'message_limit': message_limit, + 'message_percentage': int(message_percentage), + 'is_expired': chat_session.is_expired() + }) + + +@login_required +def export_chat(request, session_id): + """Export chat session as PDF or TXT""" + format_type = request.GET.get('format', 'pdf').lower() + + # Get chat session and verify ownership + chat_session = get_object_or_404( + ChatSession, + session_id=session_id, + user=request.user + ) + + # Get all messages for this session + messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp') + + if not messages.exists(): + return HttpResponse('No messages found in this chat session.', status=404) + + if format_type == 'pdf': + return export_chat_pdf(chat_session, messages) + elif format_type == 'txt': + return export_chat_txt(chat_session, messages) + else: + return HttpResponse('Invalid format. Use pdf or txt.', status=400) + + +def export_chat_pdf(chat_session, messages): + """Generate PDF export of chat session""" + buffer = BytesIO() + doc = SimpleDocTemplate(buffer, pagesize=letter) + styles = getSampleStyleSheet() + story = [] + + # Title + title_style = ParagraphStyle( + 'CustomTitle', + parent=styles['Heading1'], + fontSize=18, + spaceAfter=30, + alignment=1 # Center alignment + ) + + story.append(Paragraph(f"5 Whys Analysis - {chat_session.agent.name}", title_style)) + story.append(Spacer(1, 12)) + + # Session info + info_style = styles['Normal'] + story.append(Paragraph(f"Session ID: {chat_session.session_id}", info_style)) + story.append(Paragraph(f"Date: {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}", info_style)) + story.append(Paragraph(f"Agent: {chat_session.agent.name}", info_style)) + story.append(Paragraph(f"Total Messages: {messages.count()}", info_style)) + story.append(Spacer(1, 20)) + + # Messages + user_style = ParagraphStyle( + 'UserMessage', + parent=styles['Normal'], + leftIndent=0, + rightIndent=50, + spaceBefore=12, + spaceAfter=6, + fontSize=10 + ) + + agent_style = ParagraphStyle( + 'AgentMessage', + parent=styles['Normal'], + leftIndent=50, + rightIndent=0, + spaceBefore=12, + spaceAfter=6, + fontSize=10 + ) + + for message in messages: + timestamp = message.timestamp.strftime('%I:%M %p') + + if message.message_type == 'user': + story.append(Paragraph(f"You ({timestamp}):
{message.content}", user_style)) + elif message.message_type == 'agent': + story.append(Paragraph(f"{chat_session.agent.name} ({timestamp}):
{message.content}", agent_style)) + elif message.message_type == 'system': + story.append(Paragraph(f"System ({timestamp}): {message.content}", styles['Normal'])) + + # Build PDF + doc.build(story) + buffer.seek(0) + + response = HttpResponse(buffer.getvalue(), content_type='application/pdf') + response['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.pdf"' + return response + + +def export_chat_txt(chat_session, messages): + """Generate TXT export of chat session""" + content = [] + content.append("=" * 60) + content.append(f"5 Whys Analysis - {chat_session.agent.name}") + content.append("=" * 60) + content.append("") + content.append(f"Session ID: {chat_session.session_id}") + content.append(f"Date: {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}") + content.append(f"Agent: {chat_session.agent.name}") + content.append(f"Total Messages: {messages.count()}") + content.append("") + content.append("-" * 60) + content.append("CONVERSATION") + content.append("-" * 60) + content.append("") + + for message in messages: + timestamp = message.timestamp.strftime('%I:%M %p') + + if message.message_type == 'user': + content.append(f"You ({timestamp}):") + content.append(message.content) + elif message.message_type == 'agent': + content.append(f"{chat_session.agent.name} ({timestamp}):") + content.append(message.content) + elif message.message_type == 'system': + content.append(f"System ({timestamp}): {message.content}") + + content.append("") # Empty line between messages + + content.append("-" * 60) + content.append("End of Conversation") + content.append("-" * 60) + + text_content = "\n".join(content) + + response = HttpResponse(text_content, content_type='text/plain') + response['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.txt"' + return response diff --git a/static/css/agent-base.css b/static/css/agent-base.css index 22119ce..80ae9f0 100644 --- a/static/css/agent-base.css +++ b/static/css/agent-base.css @@ -89,7 +89,7 @@ html { /* Layout - Data Analyzer Exact Copy */ .agent-container { margin: 0 auto; - padding: var(--spacing-lg); + padding: var(--spacing-sm); max-width: 1600px; } @@ -99,7 +99,7 @@ html { border-radius: var(--radius); padding: var(--space-lg); border: var(--agent-card-border); - margin-bottom: var(--space-lg); + margin-bottom: var(--space-md); backdrop-filter: var(--agent-backdrop-filter); box-shadow: var(--agent-card-shadow); } @@ -1207,7 +1207,7 @@ html { /* Agent Grid and Layout System */ .agent-grid { display: flex; - gap: var(--spacing-lg); + gap: var(--spacing-sm); align-items: flex-start; flex-wrap: wrap; } @@ -1216,7 +1216,7 @@ html { display: flex; justify-content: space-between; align-items: center; - margin-bottom: var(--spacing-xl); + margin-bottom: 20px; } /* Typography - Data Analyzer Exact Copy */ diff --git a/templates/components/current_session_widget.html b/templates/components/current_session_widget.html new file mode 100644 index 0000000..f25aadf --- /dev/null +++ b/templates/components/current_session_widget.html @@ -0,0 +1,119 @@ +{% if chat_session %} +
+
+

+ 💬 + Current Session +

+
+
+
+
+ Status: + + {% if chat_session.status == 'active' %} + ✅ {{ chat_session.get_status_display }} + {% elif chat_session.status == 'expired' %} + ⏰ {{ chat_session.get_status_display }} + {% elif chat_session.status == 'completed' %} + 🏁 {{ chat_session.get_status_display }} + {% else %} + {{ chat_session.get_status_display }} + {% endif %} + +
+ +
+ Session ID: + {{ chat_session.session_id }} +
+ +
+ Started: + {{ chat_session.created_at|date:"M d, H:i" }} +
+
+
+
+ + +{% endif %} \ No newline at end of file diff --git a/templates/components/previous_sessions_widget.html b/templates/components/previous_sessions_widget.html new file mode 100644 index 0000000..b65b083 --- /dev/null +++ b/templates/components/previous_sessions_widget.html @@ -0,0 +1,206 @@ +{% if previous_sessions %} +
+
+

+ 📋 + Previous Sessions +

+
+
+ {% for session in previous_sessions %} +
+
+
+ + {% if session.status == 'completed' %}✅{% elif session.status == 'expired' %}⏰{% elif session.status == 'abandoned' %}❌{% endif %} + {{ session.get_status_display }} + + {{ session.created_at|date:"M d" }} +
+
+ {{ session.user_message_count }}/{{ session.agent.message_limit }} msgs +
+
+ + {% if session.status == 'completed' and session.user_message_count > 0 %} + + {% endif %} +
+ {% endfor %} + + {% if previous_sessions|length >= 5 %} + + {% endif %} +
+
+ + +{% endif %} \ No newline at end of file diff --git a/templates/components/quick_agents_panel.html b/templates/components/quick_agents_panel.html index d92708f..9ec94c0 100644 --- a/templates/components/quick_agents_panel.html +++ b/templates/components/quick_agents_panel.html @@ -7,29 +7,23 @@