mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 10:13:00 +00:00
🎉 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:
parent
e6ab0d0f37
commit
641d676f2f
@ -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
|
||||||
|
|||||||
21
agents/migrations/0004_add_message_limit.py
Normal file
21
agents/migrations/0004_add_message_limit.py
Normal 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)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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,29 +434,34 @@ 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">
|
||||||
<h3 class="chat-title">
|
<div>
|
||||||
<span>{{ agent.category.icon }}</span>
|
<h3 class="chat-title">
|
||||||
{{ agent.name }}
|
<span>{{ agent.category.icon }}</span>
|
||||||
</h3>
|
{{ agent.name }}
|
||||||
<p class="chat-subtitle">{{ agent.description }}</p>
|
</h3>
|
||||||
|
<p class="chat-subtitle">{{ agent.description }}</p>
|
||||||
|
</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>
|
||||||
|
|
||||||
<div class="chat-messages" id="chatMessages">
|
<div class="chat-messages" id="chatMessages">
|
||||||
@ -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');
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
287
agents/views.py
287
agents/views.py
@ -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
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
119
templates/components/current_session_widget.html
Normal file
119
templates/components/current_session_widget.html
Normal 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 %}
|
||||||
206
templates/components/previous_sessions_widget.html
Normal file
206
templates/components/previous_sessions_widget.html
Normal 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 %}
|
||||||
@ -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' %}
|
||||||
<div class="agent-info">
|
<a href="{% url 'agents:career_navigator_access' %}" class="quick-agent-card">
|
||||||
<h4>Social Ads Generator</h4>
|
{% else %}
|
||||||
<p>Create social media ads</p>
|
<a href="{% url 'agents:detail' agent.slug %}" class="quick-agent-card">
|
||||||
</div>
|
{% endif %}
|
||||||
</a>
|
<div class="agent-icon">{{ agent.category.icon }}</div>
|
||||||
|
<div class="agent-info">
|
||||||
<a href="/agents/job-posting-generator/" class="quick-agent-card">
|
<h4>{{ agent.name }}</h4>
|
||||||
<div class="agent-icon">💼</div>
|
<div class="agent-price">{{ agent.price }} AED</div>
|
||||||
<div class="agent-info">
|
</div>
|
||||||
<h4>Job Posting Generator</h4>
|
</a>
|
||||||
<p>Create professional job posts</p>
|
{% empty %}
|
||||||
</div>
|
<p style="text-align: center; color: var(--on-surface-variant); padding: 20px;">
|
||||||
</a>
|
No other agents available
|
||||||
|
</p>
|
||||||
<a href="/agents/pdf-summarizer/" class="quick-agent-card">
|
{% endfor %}
|
||||||
<div class="agent-icon">📄</div>
|
|
||||||
<div class="agent-info">
|
|
||||||
<h4>PDF Summarizer</h4>
|
|
||||||
<p>Analyze and summarize PDFs</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quick-agents-footer">
|
<div class="quick-agents-footer">
|
||||||
|
|||||||
200
templates/components/session_indicators.html
Normal file
200
templates/components/session_indicators.html
Normal 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 %}
|
||||||
Loading…
Reference in New Issue
Block a user