🎉 Complete 5 Whys agent enhancements with session management and UI improvements

## 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 <noreply@anthropic.com>
This commit is contained in:
Claude 2025-08-03 13:28:39 +05:30
parent e6ab0d0f37
commit 641d676f2f
11 changed files with 1190 additions and 73 deletions

View File

@ -27,18 +27,11 @@ class Command(BaseCommand):
defaults={ defaults={
'name': '5 Whys Analysis', 'name': '5 Whys Analysis',
'short_description': 'Interactive problem-solving using the proven 5 Whys methodology', '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. 'description': '''Systematically find root causes through guided 5 Whys methodology. Perfect for troubleshooting operational problems, understanding failures, and identifying systemic issues.''',
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.''',
'category': category, 'category': category,
'price': 15.00, 'price': 15.00,
'agent_type': 'chat', # This is a chat-based agent '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 'form_schema': None, # Chat agents don't use form schemas
'webhook_url': 'http://localhost:5678/webhook/5-whys-web', # N8N webhook URL 'webhook_url': 'http://localhost:5678/webhook/5-whys-web', # N8N webhook URL
'is_active': True 'is_active': True

View File

@ -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)",
),
),
]

View File

@ -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") 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) 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") 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) is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=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') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
context_data = models.JSONField(default=dict, help_text="Session context and progress tracking") context_data = models.JSONField(default=dict, help_text="Session context and progress tracking")
fee_charged = models.DecimalField(max_digits=10, decimal_places=2) 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) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
completed_at = models.DateTimeField(null=True, blank=True) completed_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs): 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: if not self.expires_at:
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
self.expires_at = timezone.now() + timedelta(hours=2) self.expires_at = timezone.now() + timedelta(minutes=30)
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:
@ -115,10 +116,10 @@ class ChatSession(models.Model):
return timezone.now() > self.expires_at return timezone.now() > self.expires_at
def extend_session(self): def extend_session(self):
"""Extend session by 2 hours from now""" """Extend session by 30 minutes from now"""
from django.utils import timezone from django.utils import timezone
from datetime import timedelta 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.updated_at = timezone.now()
self.save() self.save()

View File

@ -83,7 +83,7 @@
border: 1px solid var(--outline); border: 1px solid var(--outline);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
height: 600px; height: 800px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -94,6 +94,9 @@
background: var(--gradient-primary); background: var(--gradient-primary);
color: white; color: white;
border-radius: var(--radius-lg) var(--radius-lg) 0 0; border-radius: var(--radius-lg) var(--radius-lg) 0 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
} }
.chat-title { .chat-title {
@ -265,7 +268,7 @@
} }
.chat-widget { .chat-widget {
height: 500px; height: 650px;
} }
.message-bubble { .message-bubble {
@ -310,6 +313,92 @@
font-size: 14px; font-size: 14px;
opacity: 0.7; 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;
}
</style> </style>
{% endblock %} {% endblock %}
@ -327,10 +416,12 @@ document.body.setAttribute('data-user-id', '{{ user.id }}');
{% endif %} {% endif %}
{% if chat_session %} {% if chat_session %}
document.body.setAttribute('data-session-id', '{{ chat_session.session_id }}'); 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 %} {% endif %}
</script> </script>
<div class="chat-container"> <div class="agent-container">
<!-- Agent Header Component --> <!-- Agent Header Component -->
{% include "components/agent_header.html" with agent_title=agent.name agent_subtitle=agent.short_description %} {% include "components/agent_header.html" with agent_title=agent.name agent_subtitle=agent.short_description %}
@ -343,30 +434,35 @@ document.body.setAttribute('data-session-id', '{{ chat_session.session_id }}');
<div class="agent-widget widget-large" style="flex: 1; margin-right: clamp(0px, var(--spacing-lg), 2vw);"> <div class="agent-widget widget-large" style="flex: 1; margin-right: clamp(0px, var(--spacing-lg), 2vw);">
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if user.wallet_balance >= agent.price or chat_session %} {% if user.wallet_balance >= agent.price or chat_session %}
<!-- Session Info -->
{% if chat_session %}
<div class="session-info">
<div class="session-status">
💬 Chat Session: <strong>{{ chat_session.get_status_display }}</strong>
{% if chat_session.status == 'active' %}
<span style="color: #10b981; font-size: 12px;">• Active</span>
{% elif chat_session.status == 'expired' %}
<span style="color: #f59e0b; font-size: 12px;">• Expired</span>
{% endif %}
</div>
<div class="session-id">ID: {{ chat_session.session_id }}</div>
</div>
{% endif %}
<!-- Chat Widget --> <!-- Chat Widget -->
<div class="chat-widget"> <div class="chat-widget">
<div class="chat-header"> <div class="chat-header">
<div>
<h3 class="chat-title"> <h3 class="chat-title">
<span>{{ agent.category.icon }}</span> <span>{{ agent.category.icon }}</span>
{{ agent.name }} {{ agent.name }}
</h3> </h3>
<p class="chat-subtitle">{{ agent.description }}</p> <p class="chat-subtitle">{{ agent.description }}</p>
</div> </div>
{% if chat_session and messages %}
<div class="chat-actions">
<div class="download-dropdown">
<button class="download-btn" onclick="toggleDownloadMenu()" id="downloadBtn">
📥 Download
</button>
<div class="download-menu" id="downloadMenu">
<a href="#" onclick="downloadChat('pdf')" class="download-option">
📄 Download PDF
</a>
<a href="#" onclick="downloadChat('txt')" class="download-option">
📝 Download TXT
</a>
</div>
</div>
</div>
{% endif %}
</div>
<div class="chat-messages" id="chatMessages"> <div class="chat-messages" id="chatMessages">
{% if messages %} {% if messages %}
@ -463,15 +559,19 @@ document.body.setAttribute('data-session-id', '{{ chat_session.session_id }}');
{% endif %} {% endif %}
</div> </div>
<!-- Sidebar with Wallet and How It Works --> <!-- Sidebar -->
<div class="agent-sidebar"> <div class="agent-sidebar">
{% if user.is_authenticated %}
<!-- Wallet Card -->
{% include "components/wallet_card.html" %}
{% endif %}
<!-- How It Works Widget --> <!-- How It Works Widget -->
{% include "components/how_it_works_widget.html" with steps="agents" %} {% include "components/how_it_works_widget.html" with steps="agents" %}
<!-- Current Session Info -->
{% include "components/current_session_widget.html" %}
<!-- Session Indicators -->
{% include "components/session_indicators.html" %}
<!-- Previous Sessions Widget -->
{% include "components/previous_sessions_widget.html" %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@ -585,6 +685,10 @@ class ChatInterface {
if (response.ok) { if (response.ok) {
// Add agent response // Add agent response
this.addMessage('agent', data.agent_message.content); this.addMessage('agent', data.agent_message.content);
// Update message count indicators
if (typeof updateMessageCount === 'function') {
updateMessageCount();
}
} else { } else {
this.showError(data.error || 'Failed to send message'); this.showError(data.error || 'Failed to send message');
} }
@ -657,8 +761,206 @@ class ChatInterface {
// Initialize chat interface when page loads // Initialize chat interface when page loads
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
new ChatInterface(); 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 += '<div class="warning-item message-warning">⚠️ Approaching message limit!</div>';
}
if (timePercentage <= 20) {
warningHTML += '<div class="warning-item time-warning">⚠️ Session expires soon!</div>';
}
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 // Handle wallet balance check
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const userBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0'); const userBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0');

View File

@ -20,7 +20,9 @@ urlpatterns = [
path('api/chat/start/', views.start_chat_session, name='start_chat_session'), 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/send/', views.send_chat_message, name='send_chat_message'),
path('api/chat/history/<str:session_id>/', views.get_chat_history, name='get_chat_history'), path('api/chat/history/<str:session_id>/', views.get_chat_history, name='get_chat_history'),
path('api/chat/session/<str:session_id>/status/', views.get_session_status, name='get_session_status'),
path('api/chat/end/', views.end_chat_session, name='end_chat_session'), path('api/chat/end/', views.end_chat_session, name='end_chat_session'),
path('api/chat/export/<str:session_id>/', views.export_chat, name='export_chat'),
path('api/', views.agent_list, name='agent_list'), path('api/', views.agent_list, name='agent_list'),
path('api/<slug:slug>/', views.agent_detail, name='agent_detail_api'), path('api/<slug:slug>/', views.agent_detail, name='agent_detail_api'),

View File

@ -16,6 +16,13 @@ import time
import uuid import uuid
import ipaddress import ipaddress
from urllib.parse import urlparse 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): def validate_webhook_url(url):
""" """
@ -337,8 +344,12 @@ def agent_detail_view(request, slug):
return chat_agent_view(request, agent) return chat_agent_view(request, agent)
# Handle form-based agents (existing behavior) # 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 = { context = {
'agent': agent, 'agent': agent,
'all_agents': all_agents,
'timestamp': int(time.time()) # For cache busting 'timestamp': int(time.time()) # For cache busting
} }
@ -402,11 +413,68 @@ def chat_agent_view(request, agent):
if chat_session: if chat_session:
messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp') 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 = { context = {
'agent': agent, 'agent': agent,
'chat_session': chat_session, 'chat_session': chat_session,
'messages': messages, '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) return render(request, 'agents/agent_chat.html', context)
@ -452,11 +520,28 @@ def start_chat_session(request):
user=request.user, user=request.user,
fee_charged=agent.price, fee_charged=agent.price,
status='active', 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) # Deduct fee from wallet
# This would integrate with the existing wallet system 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 # 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?" 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() chat_session.save()
return Response({'error': 'Chat session has expired'}, status=status.HTTP_400_BAD_REQUEST) 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 # Save user message
user_message = ChatMessage.objects.create( user_message = ChatMessage.objects.create(
session=chat_session, session=chat_session,
@ -679,3 +776,185 @@ def end_chat_session(request):
chat_session.save() chat_session.save()
return Response({'message': 'Chat session ended successfully'}) 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"<b>Session ID:</b> {chat_session.session_id}", info_style))
story.append(Paragraph(f"<b>Date:</b> {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}", info_style))
story.append(Paragraph(f"<b>Agent:</b> {chat_session.agent.name}", info_style))
story.append(Paragraph(f"<b>Total Messages:</b> {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"<b>You ({timestamp}):</b><br/>{message.content}", user_style))
elif message.message_type == 'agent':
story.append(Paragraph(f"<b>{chat_session.agent.name} ({timestamp}):</b><br/>{message.content}", agent_style))
elif message.message_type == 'system':
story.append(Paragraph(f"<i>System ({timestamp}): {message.content}</i>", 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

View File

@ -89,7 +89,7 @@ html {
/* Layout - Data Analyzer Exact Copy */ /* Layout - Data Analyzer Exact Copy */
.agent-container { .agent-container {
margin: 0 auto; margin: 0 auto;
padding: var(--spacing-lg); padding: var(--spacing-sm);
max-width: 1600px; max-width: 1600px;
} }
@ -99,7 +99,7 @@ html {
border-radius: var(--radius); border-radius: var(--radius);
padding: var(--space-lg); padding: var(--space-lg);
border: var(--agent-card-border); border: var(--agent-card-border);
margin-bottom: var(--space-lg); margin-bottom: var(--space-md);
backdrop-filter: var(--agent-backdrop-filter); backdrop-filter: var(--agent-backdrop-filter);
box-shadow: var(--agent-card-shadow); box-shadow: var(--agent-card-shadow);
} }
@ -1207,7 +1207,7 @@ html {
/* Agent Grid and Layout System */ /* Agent Grid and Layout System */
.agent-grid { .agent-grid {
display: flex; display: flex;
gap: var(--spacing-lg); gap: var(--spacing-sm);
align-items: flex-start; align-items: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
} }
@ -1216,7 +1216,7 @@ html {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--spacing-xl); margin-bottom: 20px;
} }
/* Typography - Data Analyzer Exact Copy */ /* Typography - Data Analyzer Exact Copy */

View File

@ -0,0 +1,119 @@
{% if chat_session %}
<div class="agent-widget widget-small" style="min-width: min(280px, 100%); max-width: min(280px, 100%); margin-left: auto;">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">💬</span>
Current Session
</h3>
</div>
<div class="widget-content">
<div class="current-session-info">
<div class="session-status-row">
<span class="status-label">Status:</span>
<span class="session-status status-{{ chat_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 %}
</span>
</div>
<div class="session-id-row">
<span class="id-label">Session ID:</span>
<span class="session-id">{{ chat_session.session_id }}</span>
</div>
<div class="session-time-row">
<span class="time-label">Started:</span>
<span class="session-time">{{ chat_session.created_at|date:"M d, H:i" }}</span>
</div>
</div>
</div>
</div>
<style>
.current-session-info {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.session-status-row,
.session-id-row,
.session-time-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-xs) 0;
border-bottom: 1px solid var(--outline-variant);
}
.session-status-row:last-child,
.session-id-row:last-child,
.session-time-row:last-child {
border-bottom: none;
}
.status-label,
.id-label,
.time-label {
font-size: 13px;
font-weight: 500;
color: var(--on-surface-variant);
}
.session-status {
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
}
.session-status.status-active {
color: #10b981;
}
.session-status.status-expired {
color: #f59e0b;
}
.session-status.status-completed {
color: #6366f1;
}
.session-id {
font-family: var(--font-mono);
font-size: 11px;
color: var(--on-surface-variant);
background: var(--surface-variant);
padding: 2px 6px;
border-radius: 3px;
word-break: break-all;
}
.session-time {
font-size: 12px;
color: var(--on-surface);
}
@media (max-width: 768px) {
.session-id-row {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.session-id {
font-size: 10px;
align-self: stretch;
text-align: center;
}
}
</style>
{% endif %}

View File

@ -0,0 +1,206 @@
{% if previous_sessions %}
<div class="agent-widget widget-small" style="min-width: min(280px, 100%); max-width: min(280px, 100%); margin-left: auto;">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">📋</span>
Previous Sessions
</h3>
</div>
<div class="widget-content">
{% for session in previous_sessions %}
<div class="session-item">
<div class="session-header">
<div class="session-info">
<span class="session-status status-{{ session.status }}">
{% if session.status == 'completed' %}✅{% elif session.status == 'expired' %}⏰{% elif session.status == 'abandoned' %}❌{% endif %}
{{ session.get_status_display }}
</span>
<span class="session-date">{{ session.created_at|date:"M d" }}</span>
</div>
<div class="session-stats">
<span class="message-count">{{ session.user_message_count }}/{{ session.agent.message_limit }} msgs</span>
</div>
</div>
{% if session.status == 'completed' and session.user_message_count > 0 %}
<div class="session-actions">
<div class="download-actions">
<a href="{% url 'agents:export_chat' session.session_id %}?format=pdf"
class="download-link pdf-link"
title="Download PDF">
📄 PDF
</a>
<a href="{% url 'agents:export_chat' session.session_id %}?format=txt"
class="download-link txt-link"
title="Download TXT">
📝 TXT
</a>
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% if previous_sessions|length >= 5 %}
<div class="view-all-sessions">
<a href="#" class="view-all-link" onclick="alert('Full session history coming soon!')">
View All Sessions →
</a>
</div>
{% endif %}
</div>
</div>
<style>
.session-item {
padding: var(--spacing-sm);
border: 1px solid var(--outline-variant);
border-radius: var(--radius-sm);
margin-bottom: var(--spacing-sm);
background: var(--surface);
transition: all 0.2s ease;
}
.session-item:hover {
background: var(--surface-variant);
border-color: var(--primary);
}
.session-item:last-child {
margin-bottom: 0;
}
.session-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-xs);
}
.session-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.session-status {
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
}
.session-status.status-completed {
color: var(--success);
}
.session-status.status-expired {
color: var(--warning);
}
.session-status.status-abandoned {
color: var(--error);
}
.session-date {
font-size: 11px;
color: var(--on-surface-variant);
}
.session-stats {
text-align: right;
}
.message-count {
font-size: 11px;
color: var(--on-surface-variant);
background: var(--surface-variant);
padding: 2px 6px;
border-radius: 3px;
}
.session-actions {
border-top: 1px solid var(--outline-variant);
padding-top: var(--spacing-xs);
}
.download-actions {
display: flex;
gap: var(--spacing-sm);
justify-content: center;
}
.download-link {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: var(--primary);
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
transition: all 0.2s ease;
flex: 1;
justify-content: center;
}
.download-link:hover {
background: var(--primary-dark);
transform: translateY(-1px);
color: white;
text-decoration: none;
}
.download-link.pdf-link {
background: #dc2626;
}
.download-link.pdf-link:hover {
background: #b91c1c;
}
.download-link.txt-link {
background: var(--primary);
}
.download-link.txt-link:hover {
background: var(--primary-dark);
}
.view-all-sessions {
margin-top: var(--spacing-sm);
text-align: center;
border-top: 1px solid var(--outline-variant);
padding-top: var(--spacing-sm);
}
.view-all-link {
font-size: 12px;
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.view-all-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.session-header {
flex-direction: column;
gap: var(--spacing-xs);
}
.session-stats {
text-align: left;
}
.download-actions {
flex-direction: column;
}
}
</style>
{% endif %}

View File

@ -7,29 +7,23 @@
</div> </div>
<div class="quick-agents-grid"> <div class="quick-agents-grid">
<a href="/agents/social-ads-generator/" class="quick-agent-card"> {% for agent in all_agents %}
<div class="agent-icon">📢</div> {% if agent.slug == 'cybersec-career-navigator' %}
<a href="{% url 'agents:career_navigator_access' %}" class="quick-agent-card">
{% else %}
<a href="{% url 'agents:detail' agent.slug %}" class="quick-agent-card">
{% endif %}
<div class="agent-icon">{{ agent.category.icon }}</div>
<div class="agent-info"> <div class="agent-info">
<h4>Social Ads Generator</h4> <h4>{{ agent.name }}</h4>
<p>Create social media ads</p> <div class="agent-price">{{ agent.price }} AED</div>
</div>
</a>
<a href="/agents/job-posting-generator/" class="quick-agent-card">
<div class="agent-icon">💼</div>
<div class="agent-info">
<h4>Job Posting Generator</h4>
<p>Create professional job posts</p>
</div>
</a>
<a href="/agents/pdf-summarizer/" class="quick-agent-card">
<div class="agent-icon">📄</div>
<div class="agent-info">
<h4>PDF Summarizer</h4>
<p>Analyze and summarize PDFs</p>
</div> </div>
</a> </a>
{% empty %}
<p style="text-align: center; color: var(--on-surface-variant); padding: 20px;">
No other agents available
</p>
{% endfor %}
</div> </div>
<div class="quick-agents-footer"> <div class="quick-agents-footer">

View File

@ -0,0 +1,200 @@
{% if chat_session and messages %}
<div class="session-indicators">
<h4 class="session-indicators-title">Session Status</h4>
<!-- Time Remaining Indicator -->
<div class="indicator-item">
<div class="indicator-header">
<span class="indicator-icon"></span>
<span class="indicator-label">Time Remaining</span>
<span class="indicator-value" id="timeRemaining">
{% if time_remaining %}
{{ time_remaining }}
{% else %}
Calculating...
{% endif %}
</span>
</div>
<div class="progress-bar">
<div class="progress-fill time-progress" id="timeProgress" style="width: {{ time_percentage|default:100 }}%"></div>
</div>
</div>
<!-- Messages Remaining Indicator -->
<div class="indicator-item">
<div class="indicator-header">
<span class="indicator-icon">💬</span>
<span class="indicator-label">Messages</span>
<span class="indicator-value" id="messageCount">
{{ message_count|default:0 }}/{{ message_limit }} used
</span>
</div>
<div class="progress-bar">
<div class="progress-fill message-progress" id="messageProgress" style="width: {{ message_percentage|default:0 }}%"></div>
</div>
</div>
<!-- Warning Messages -->
<div class="session-warnings" id="sessionWarnings">
{% if time_percentage <= 20 %}
<div class="warning-item time-warning">
⚠️ Session expires soon!
</div>
{% endif %}
{% if message_percentage >= 80 %}
<div class="warning-item message-warning">
⚠️ Approaching message limit!
</div>
{% endif %}
</div>
</div>
<style>
.session-indicators {
background: var(--surface);
border: 1px solid var(--outline-variant);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-md);
box-shadow: var(--shadow-sm);
min-width: min(280px, 100%);
max-width: min(280px, 100%);
margin-left: auto;
}
.session-indicators-title {
font-size: 16px;
font-weight: 600;
color: var(--on-surface);
margin: 0 0 var(--spacing-md) 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.indicator-item {
margin-bottom: var(--spacing-md);
}
.indicator-item:last-of-type {
margin-bottom: 0;
}
.indicator-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-xs);
}
.indicator-icon {
font-size: 16px;
}
.indicator-label {
font-size: 14px;
font-weight: 500;
color: var(--on-surface);
flex: 1;
margin-left: var(--spacing-sm);
}
.indicator-value {
font-size: 13px;
font-weight: 600;
color: var(--on-surface-variant);
}
.progress-bar {
width: 100%;
height: 6px;
background: var(--surface-variant);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease, background-color 0.3s ease;
}
.time-progress {
background: linear-gradient(90deg, #10b981, #059669);
}
.time-progress[style*="width: 0"],
.time-progress[style*="width: 1"],
.time-progress[style*="width: 2"] {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.time-progress[style*="width: 3"],
.time-progress[style*="width: 4"],
.time-progress[style*="width: 5"],
.time-progress[style*="width: 10"],
.time-progress[style*="width: 15"],
.time-progress[style*="width: 20"] {
background: linear-gradient(90deg, #f59e0b, #d97706);
}
.message-progress {
background: linear-gradient(90deg, #3b82f6, #2563eb);
}
.message-progress[style*="width: 8"],
.message-progress[style*="width: 9"] {
background: linear-gradient(90deg, #f59e0b, #d97706);
}
.message-progress[style*="width: 95"],
.message-progress[style*="width: 96"],
.message-progress[style*="width: 97"],
.message-progress[style*="width: 98"],
.message-progress[style*="width: 99"],
.message-progress[style*="width: 100"] {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.session-warnings {
margin-top: var(--spacing-md);
}
.warning-item {
background: #fef3c7;
color: #92400e;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
margin-bottom: var(--spacing-xs);
border-left: 3px solid #f59e0b;
}
.warning-item:last-child {
margin-bottom: 0;
}
.time-warning {
background: #fee2e2;
color: #991b1b;
border-left-color: #ef4444;
}
@media (max-width: 768px) {
.session-indicators {
padding: var(--spacing-md);
}
.indicator-header {
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.indicator-value {
flex-basis: 100%;
text-align: right;
}
}
</style>
{% endif %}