Standardize agent architecture and update comprehensive documentation

- Implement agent-specific JavaScript utilities for container-like isolation
- Standardize HTML structure across all 5 agents with agent-container grid layout
- Fix Data Analyzer wallet positioning by moving wallet-section to separate grid column
- Reduce Data Analyzer custom CSS from 400+ lines to ~78 lines essential styles
- Apply unified theme system (professional/creative/minimal) consistently
- Add agent isolation with DataAnalyzerUtils, WeatherUtils, SocialAdsUtils, etc.
- Update CLAUDE.md with comprehensive agent architecture documentation
- Document standardization improvements, layout rules, and JavaScript isolation patterns

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2025-07-14 16:28:37 +05:30
parent 2e25b96807
commit 49b5b2a6aa
8 changed files with 2110 additions and 695 deletions

View File

@ -13,484 +13,369 @@
<link rel="stylesheet" href="{% static 'css/themes.css' %}"> <link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}"> <link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Shared JavaScript Utilities --> <!-- Data Analyzer Specific Utilities -->
<script src="{% static 'js/agent-utils.js' %}"></script> <script>
// Data Analyzer - Self-contained utilities (no shared dependencies)
const DataAnalyzerUtils = {
/**
* Update wallet balance display - Data Analyzer specific
*/
updateWalletBalance(newBalance) {
// Update header balance (anchor tag with emoji)
const headerBalance = document.querySelector('a[data-wallet-balance]');
if (headerBalance) {
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
}
// Update page balance (div without emoji)
const pageBalance = document.querySelector('div[data-wallet-balance]');
if (pageBalance) {
pageBalance.textContent = `${newBalance.toFixed(2)} AED`;
}
window.currentWalletBalance = newBalance;
},
/**
* Show toast notification with duplicate prevention
*/
showToast(message, type = 'info') {
// Prevent duplicate toasts
const existingToast = document.querySelector('.data-analyzer-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'data-analyzer-toast';
toast.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
padding: 8px 12px;
border-radius: 4px;
color: white;
font-size: 13px;
z-index: 1000;
max-width: 300px;
font-weight: 500;
${type === 'success' ? 'background: #10b981;' : 'background: #ef4444;'}
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 2000);
},
/**
* Generate text for copy/download functionality
*/
generateTextForExport(contentElementId) {
const content = document.getElementById(contentElementId);
if (content) {
return content.innerText || content.textContent || '';
}
return 'No content available';
},
/**
* Copy content to clipboard
*/
copyToClipboard(text, successMessage = 'Content copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
this.showToast(`📋 ${successMessage}`, 'success');
}).catch(() => {
this.showToast('Failed to copy content', 'error');
});
},
/**
* Download content as text file
*/
downloadAsFile(text, filename, successMessage = 'File downloaded!') {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `content-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
this.showToast(`💾 ${successMessage}`, 'success');
}
};
// For backward compatibility, create AgentUtils alias
const AgentUtils = DataAnalyzerUtils;
</script>
<style> <style>
* { /* Data Analyzer specific styles only */
box-sizing: border-box; .file-upload-zone {
} border: 2px dashed var(--agent-border);
border-radius: var(--radius);
/* Override main-container for full-width sections */ padding: var(--space-xl);
.main-container { text-align: center;
max-width: none; transition: all var(--transition);
padding: 0; background: var(--agent-card-bg);
margin-top: 0; cursor: pointer;
} margin-bottom: var(--space-lg);
}
/* Page background */
.data-analyzer-page { .file-upload-zone.dragover,
background: var(--gradient-hero); .file-upload-zone:hover {
min-height: calc(100vh - 80px); border-color: var(--agent-primary);
padding: clamp(20px, 5vw, 40px); background: var(--hover-color);
width: 100vw; }
margin-left: calc(-50vw + 50%);
} .file-preview {
background: var(--agent-card-bg);
.container { border: 1px solid var(--agent-border);
max-width: 1280px; border-radius: var(--radius);
margin: 0 auto; padding: var(--space-md);
padding: 0 clamp(16px, 4vw, 24px); margin-top: var(--space-md);
} display: flex;
align-items: center;
/* Main grid layout */ gap: var(--space-md);
.main-grid { }
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(350px, 100%), 1fr)); .radio-grid {
gap: clamp(16px, 4vw, 24px); display: grid;
align-items: start; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
} gap: var(--space-md);
}
/* Card styles with glassmorphism */
.card { .radio-option {
background: rgba(255, 255, 255, 0.9); display: flex;
border-radius: clamp(12px, 3vw, 16px); align-items: center;
padding: clamp(16px, 4vw, 24px); gap: var(--space-md);
border: 1px solid rgba(255, 255, 255, 0.3); padding: var(--space-lg);
backdrop-filter: blur(20px); border: 1px solid var(--agent-border);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); border-radius: var(--radius);
margin-bottom: clamp(16px, 4vw, 24px); cursor: pointer;
} background: white;
transition: all var(--transition);
.section-title { min-height: 60px;
font-size: clamp(16px, 4vw, 18px); }
font-weight: 600;
color: var(--text-primary); .radio-option.selected {
margin-bottom: clamp(12px, 3vw, 16px); border-color: var(--agent-primary);
} background: var(--agent-card-bg);
}
/* Form inputs */
.form-input { .radio-option:hover {
width: 100%; border-color: var(--agent-primary);
padding: clamp(12px, 3vw, 16px) clamp(16px, 4vw, 20px); background: var(--hover-color);
border: 2px solid var(--border-medium); }
border-radius: clamp(8px, 2vw, 12px);
font-size: clamp(14px, 3.5vw, 16px); .radio-option input[type="radio"] {
transition: border-color 0.2s ease; margin: 0;
min-height: 48px; }
margin-bottom: clamp(12px, 3vw, 16px);
} .processing-status {
display: none;
.form-input:focus { text-align: center;
outline: none; padding: var(--space-lg);
border-color: var(--success-green); margin: var(--space-lg) 0;
} background: var(--agent-card-bg);
border: 1px solid var(--agent-border);
.help-text { border-radius: var(--radius);
font-size: clamp(12px, 3vw, 14px); }
color: var(--text-secondary);
} .results-card {
display: none;
/* File upload zone */ margin-top: var(--space-lg);
.file-upload-zone { }
border: 2px dashed var(--border-medium);
border-radius: clamp(12px, 3vw, 16px); /* Ensure grid layout works */
padding: clamp(24px, 6vw, 40px); div.agent-page div.agent-container {
text-align: center; display: grid !important;
transition: all 0.3s ease; grid-template-columns: 1fr 350px !important;
background: var(--background-light); gap: 24px !important;
cursor: pointer; max-width: 1280px !important;
margin-bottom: clamp(12px, 3vw, 16px); margin: 0 auto !important;
} align-items: start !important;
}
.file-upload-zone.dragover {
border-color: var(--success-green); @media (max-width: 768px) {
background: rgba(16, 185, 129, 0.1); div.agent-page div.agent-container {
} grid-template-columns: 1fr !important;
.file-upload-zone:hover {
border-color: var(--success-green);
background: rgba(16, 185, 129, 0.1);
}
.file-preview {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.5);
border-radius: 8px;
padding: 12px;
margin-top: 12px;
display: flex;
align-items: center;
gap: 12px;
}
/* Radio button grids */
.radio-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
gap: clamp(8px, 2vw, 12px);
}
.radio-option {
display: flex;
align-items: center;
gap: clamp(8px, 2vw, 12px);
padding: clamp(12px, 3vw, 16px);
border: 2px solid var(--border-light);
border-radius: clamp(8px, 2vw, 12px);
cursor: pointer;
background: white;
transition: all 0.2s ease;
min-height: 44px;
}
.radio-option.selected {
border-color: var(--success-green);
background: rgba(16, 185, 129, 0.1);
}
.radio-option input[type="radio"] {
margin: 0;
}
/* Processing status */
.processing-status {
padding: clamp(16px, 4vw, 20px);
background: rgba(16, 185, 129, 0.1);
border: 1px solid var(--success-green);
border-radius: clamp(8px, 2vw, 12px);
color: var(--success-dark);
font-weight: 600;
text-align: center;
margin-bottom: clamp(16px, 4vw, 24px);
}
/* Results card */
.results-card {
background: rgba(255, 255, 255, 0.9);
border-radius: 16px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
margin-top: 24px;
}
.results-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.results-content {
background: var(--background-page);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
white-space: pre-line;
line-height: 1.7;
color: var(--text-primary);
font-size: 15px;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
flex-wrap: wrap;
}
/* Button styles */
.btn {
padding: clamp(12px, 3vw, 16px) clamp(20px, 5vw, 32px);
border: none;
border-radius: clamp(8px, 2vw, 12px);
font-weight: 600;
cursor: pointer;
font-size: clamp(14px, 3.5vw, 16px);
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: transform 0.1s ease;
text-decoration: none;
}
.btn-primary {
background: var(--gradient-success);
color: white;
flex: 1;
min-width: 120px;
}
.btn-secondary {
background: var(--background-subtle);
color: var(--text-primary);
border: 2px solid var(--border-medium);
flex: 1;
min-width: 120px;
}
.btn:hover {
transform: translateY(-1px);
}
.btn:disabled {
background: var(--border-strong);
color: white;
cursor: not-allowed;
transform: none;
opacity: 0.6;
}
/* Wallet sidebar */
.wallet-section {
position: sticky;
top: 20px;
}
.wallet-balance {
font-size: clamp(24px, 6vw, 28px);
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.balance-label {
font-size: clamp(14px, 3.5vw, 16px);
color: var(--text-secondary);
margin-bottom: clamp(16px, 4vw, 20px);
}
.process-btn {
width: 100%;
margin-bottom: 12px;
}
.usage-info {
padding: 16px;
background: rgba(16, 185, 129, 0.1);
border-radius: 12px;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.usage-info h4 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: var(--success-dark);
}
.usage-info ul {
margin: 0;
font-size: 12px;
color: var(--text-primary);
line-height: 1.4;
list-style: none;
padding-left: 0;
}
.usage-info li {
margin: 4px 0;
padding-left: 16px;
position: relative;
}
.usage-info li::before {
content: "•";
position: absolute;
left: 0;
color: var(--success-green);
}
/* Responsive */
@media (max-width: 768px) {
.main-grid {
grid-template-columns: 1fr;
}
.radio-grid {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
} }
}
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="agent-page theme-professional"> <div class="agent-page theme-professional">
<div class="agent-container"> <div class="agent-container">
<!-- Messages -->
{% if messages %}
{% for message in messages %}
<div class="{% if message.tags == 'error' %}error-message{% else %}success-message{% endif %}" style="grid-column: 1 / -1;">
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- Main Content --> <!-- Main Content -->
<div> <div>
<form method="POST" id="dataAnalyzerForm"> <form method="POST" id="dataAnalyzerForm">
{% csrf_token %} {% csrf_token %}
<!-- File Upload Section --> <!-- File Upload Section -->
<div class="card"> <div class="card">
<h3 class="section-title">📁 Upload Your Data File</h3> <h3 class="section-title">📁 Upload Your Data File</h3>
<div class="file-upload-zone" id="fileUploadZone" onclick="document.getElementById('fileInput').click()"> <div class="file-upload-zone" id="fileUploadZone" onclick="document.getElementById('fileInput').click()">
<div style="font-size: clamp(32px, 8vw, 48px); margin-bottom: 12px;">📊</div> <div style="font-size: 48px; margin-bottom: 12px;">📊</div>
<div style="font-size: clamp(16px, 4vw, 18px); font-weight: 600; color: var(--text-primary); margin-bottom: 8px;"> <div style="font-size: var(--text-lg); font-weight: 600; color: var(--text-color); margin-bottom: 8px;">
Choose or drag your data file here Choose or drag your data file here
</div>
<div style="font-size: clamp(14px, 3.5vw, 16px); color: var(--text-secondary);">
Supports PDF, CSV, Excel files (up to 10MB)
</div>
</div>
<input
type="file"
id="fileInput"
accept=".pdf,.csv,.xlsx,.xls"
style="display: none;"
/>
<div id="filePreview" class="file-preview" style="display: none;">
<div style="font-size: 24px;">📄</div>
<div style="flex: 1;">
<div style="font-weight: 600; color: var(--text-primary);" id="fileName"></div>
<div style="font-size: 14px; color: var(--text-secondary);" id="fileSize"></div>
</div>
<button type="button" onclick="removeFile()" style="
background: var(--error-red);
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
">
Remove
</button>
</div>
</div>
<!-- Analysis Type Selection -->
<div class="card">
<h3 class="section-title">🔍 Analysis Type</h3>
<div class="radio-grid">
<label class="radio-option selected" data-value="summary">
<input type="radio" name="analysisType" value="summary" checked />
<div style="font-size: clamp(16px, 4vw, 20px);">📋</div>
<div>
<div style="font-weight: 600; margin-bottom: clamp(2px, 1vw, 4px); font-size: clamp(14px, 3.5vw, 16px);">
Summary Analysis
</div>
<div style="font-size: clamp(12px, 3vw, 14px); color: var(--text-secondary);">
Quick overview and key insights
</div>
</div> </div>
</label> <div style="font-size: var(--text-base); color: var(--accent-color);">
Supports PDF, CSV, Excel files (up to 10MB)
<label class="radio-option" data-value="detailed">
<input type="radio" name="analysisType" value="detailed" />
<div style="font-size: clamp(16px, 4vw, 20px);">📈</div>
<div>
<div style="font-weight: 600; margin-bottom: clamp(2px, 1vw, 4px); font-size: clamp(14px, 3.5vw, 16px);">
Detailed Analysis
</div>
<div style="font-size: clamp(12px, 3vw, 14px); color: var(--text-secondary);">
Comprehensive statistical analysis
</div>
</div> </div>
</label> </div>
<label class="radio-option" data-value="statistical"> <input
<input type="radio" name="analysisType" value="statistical" /> type="file"
<div style="font-size: clamp(16px, 4vw, 20px);">📊</div> id="fileInput"
<div> accept=".pdf,.csv,.xlsx,.xls"
<div style="font-weight: 600; margin-bottom: clamp(2px, 1vw, 4px); font-size: clamp(14px, 3.5vw, 16px);"> style="display: none;"
Statistical Analysis />
</div>
<div style="font-size: clamp(12px, 3vw, 14px); color: var(--text-secondary);"> <div id="filePreview" class="file-preview" style="display: none;">
Advanced statistics and trends <div style="font-size: 24px;">📄</div>
</div> <div style="flex: 1;">
<div style="font-weight: 600; color: var(--text-primary);" id="fileName"></div>
<div style="font-size: 14px; color: var(--text-secondary);" id="fileSize"></div>
</div> </div>
</label> <button type="button" onclick="removeFile()" class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;">
</div> Remove
</div> </button>
</form>
<!-- Processing Status -->
<div id="processingStatus" class="processing-status" style="display: none;">
<div style="font-size: clamp(16px, 4vw, 18px); margin-bottom: 8px;">
⏳ Processing...
</div>
<div style="font-size: clamp(14px, 3.5vw, 16px);" id="statusText">
Analyzing your data file...
</div>
</div>
<!-- Results -->
<div id="analysisResults" class="results-card" style="display: none;">
<div class="results-header">
<div style="font-size: 24px;"></div>
<h3 style="font-size: 20px; font-weight: 600; color: var(--text-primary); margin: 0;">
Data Analysis Results
</h3>
<div style="
background: var(--success-green);
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
margin-left: auto;
">
✅ Complete
</div> </div>
</div> </div>
<div class="results-content" id="analysisContent"> <!-- Analysis Type Selection -->
<!-- Analysis data will be displayed here --> <div class="card">
</div> <h3 class="section-title">🔍 Analysis Type</h3>
<div class="action-buttons"> <div class="radio-grid">
<button onclick="copyAnalysisReport()" class="btn btn-primary"> <label class="radio-option selected" data-value="summary">
📋 Copy Report <input type="radio" name="analysisType" value="summary" checked />
</button> <div style="font-size: 20px;">📋</div>
<button onclick="downloadAnalysisReport()" class="btn btn-secondary"> <div>
💾 Download Report <div style="font-weight: 600; margin-bottom: 4px; font-size: var(--text-lg);">
</button> Summary Analysis
</div> </div>
</div> <div style="font-size: var(--text-sm); color: var(--accent-color);">
</div> Quick overview and key insights
</div>
</div>
</label>
<!-- Wallet Sidebar --> <label class="radio-option" data-value="detailed">
<input type="radio" name="analysisType" value="detailed" />
<div style="font-size: 20px;">📈</div>
<div>
<div style="font-weight: 600; margin-bottom: 4px; font-size: var(--text-lg);">
Detailed Analysis
</div>
<div style="font-size: var(--text-sm); color: var(--accent-color);">
Comprehensive statistical analysis
</div>
</div>
</label>
<label class="radio-option" data-value="statistical">
<input type="radio" name="analysisType" value="statistical" />
<div style="font-size: 20px;">📊</div>
<div>
<div style="font-weight: 600; margin-bottom: 4px; font-size: var(--text-lg);">
Statistical Analysis
</div>
<div style="font-size: var(--text-sm); color: var(--accent-color);">
Advanced statistics and trends
</div>
</div>
</label>
</div>
</div>
</form>
</div>
<!-- Wallet Sidebar -->
<div class="wallet-section"> <div class="wallet-section">
<div class="card"> <div class="card">
<h3 class="section-title">💳 Your Wallet</h3> <h3 class="section-title">💳 Your Wallet</h3>
<div class="wallet-balance" id="walletBalance">{{ user.wallet_balance|floatformat:2 }} AED</div> <div class="wallet-balance" data-wallet-balance>
{% if user.is_authenticated %}
{{ user.wallet_balance|floatformat:2 }} AED
{% else %}
0.00 AED
{% endif %}
</div>
<div class="balance-label">Available Balance</div> <div class="balance-label">Available Balance</div>
<button {% if user.is_authenticated %}
type="submit" {% if user.wallet_balance >= 5.00 %}
form="dataAnalyzerForm" <button type="submit" form="dataAnalyzerForm" class="btn btn-primary process-btn" id="processButton">
class="btn btn-primary process-btn" 📊 Analyze Data (5.00 AED)
id="processButton" </button>
> {% else %}
📊 Analyze Data (5.00 AED) <div class="insufficient-balance">
</button> Insufficient balance! You need 5.00 AED.
</div>
<a href="{% url 'core:wallet' %}" class="btn btn-primary process-btn" style="text-decoration: none;">
💰 Top Up Wallet
</a>
{% endif %}
{% else %}
<a href="{% url 'authentication:login' %}" class="btn btn-primary process-btn" style="text-decoration: none;">
🔑 Login to Continue
</a>
{% endif %}
</div> </div>
<div class="usage-info"> <div class="usage-info">
<h4>💡 How it works</h4> <h4>💡 How it works</h4>
<ul> <ul>
<li>Upload PDF, CSV, or Excel files</li> <li>Upload your data file (PDF, CSV, Excel)</li>
<li>Choose your analysis depth</li> <li>Choose analysis type</li>
<li>Get AI-powered insights</li> <li>Get comprehensive insights</li>
<li>Download comprehensive reports</li> <li>Download detailed report</li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Processing Status -->
<div id="processingStatus" class="processing-status">
<div class="status-icon">📊</div>
<div style="font-weight: 600; color: var(--primary-color);">Analyzing Your Data...</div>
<div style="font-size: 14px; color: var(--text-secondary); margin-top: 8px;" id="statusText">Processing data file...</div>
</div>
<!-- Results -->
<div id="analysisResults" class="results-card">
<div class="results-header">
<div style="font-size: 24px;"></div>
<h3 style="font-size: 20px; font-weight: 600; color: var(--text-primary); margin: 0;">Data Analysis Complete</h3>
<div style="background: var(--primary-color); color: white; padding: 6px 12px; border-radius: 6px; font-size: 14px; font-weight: 600; margin-left: auto;">✅ Complete</div>
</div>
<div class="results-content" id="analysisContent">
<!-- Analysis results will be displayed here -->
</div>
<div class="action-buttons">
<button onclick="copyAnalysisReport()" class="btn btn-primary">📋 Copy Analysis</button>
<button onclick="downloadAnalysisReport()" class="btn btn-secondary">💾 Download Report</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,952 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Data Analyzer Agent - NetCop AI Hub{% endblock %}
{% block extra_css %}
<!-- Optimized Font Loading -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<!-- External Stylesheets -->
<link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Data Analyzer Specific Utilities -->
<script>
// Data Analyzer - Self-contained utilities (no shared dependencies)
const DataAnalyzerUtils = {
/**
* Update wallet balance display - Data Analyzer specific
*/
updateWalletBalance(newBalance) {
// Update header balance (anchor tag with emoji)
const headerBalance = document.querySelector('a[data-wallet-balance]');
if (headerBalance) {
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
}
// Update page balance (div without emoji)
const pageBalance = document.querySelector('div[data-wallet-balance]');
if (pageBalance) {
pageBalance.textContent = `${newBalance.toFixed(2)} AED`;
}
window.currentWalletBalance = newBalance;
},
/**
* Show toast notification with duplicate prevention
*/
showToast(message, type = 'info') {
// Prevent duplicate toasts
const existingToast = document.querySelector('.data-analyzer-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'data-analyzer-toast';
toast.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
padding: 8px 12px;
border-radius: 4px;
color: white;
font-size: 13px;
z-index: 1000;
max-width: 300px;
font-weight: 500;
${type === 'success' ? 'background: #10b981;' : 'background: #ef4444;'}
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 2000);
},
/**
* Generate text for copy/download functionality
*/
generateTextForExport(contentElementId) {
const content = document.getElementById(contentElementId);
if (content) {
return content.innerText || content.textContent || '';
}
return 'No content available';
},
/**
* Copy content to clipboard
*/
copyToClipboard(text, successMessage = 'Content copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
this.showToast(`📋 ${successMessage}`, 'success');
}).catch(() => {
this.showToast('Failed to copy content', 'error');
});
},
/**
* Download content as text file
*/
downloadAsFile(text, filename, successMessage = 'File downloaded!') {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `content-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
this.showToast(`💾 ${successMessage}`, 'success');
}
};
// For backward compatibility, create AgentUtils alias
const AgentUtils = DataAnalyzerUtils;
</script>
{% endblock %}
{% block content %}
.main-container {
max-width: none;
padding: 0;
margin-top: 0;
}
/* Page background */
.data-analyzer-page {
background: var(--gradient-hero);
min-height: calc(100vh - 80px);
padding: clamp(20px, 5vw, 40px);
width: 100vw;
margin-left: calc(-50vw + 50%);
}
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 clamp(16px, 4vw, 24px);
}
/* Main grid layout */
.main-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(350px, 100%), 1fr));
gap: clamp(16px, 4vw, 24px);
align-items: start;
}
/* Card styles with glassmorphism */
.card {
background: rgba(255, 255, 255, 0.9);
border-radius: clamp(12px, 3vw, 16px);
padding: clamp(16px, 4vw, 24px);
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
margin-bottom: clamp(16px, 4vw, 24px);
}
.section-title {
font-size: clamp(16px, 4vw, 18px);
font-weight: 600;
color: var(--text-primary);
margin-bottom: clamp(12px, 3vw, 16px);
}
/* Form inputs */
.form-input {
width: 100%;
padding: clamp(12px, 3vw, 16px) clamp(16px, 4vw, 20px);
border: 2px solid var(--border-medium);
border-radius: clamp(8px, 2vw, 12px);
font-size: clamp(14px, 3.5vw, 16px);
transition: border-color 0.2s ease;
min-height: 48px;
margin-bottom: clamp(12px, 3vw, 16px);
}
.form-input:focus {
outline: none;
border-color: var(--success-green);
}
.help-text {
font-size: clamp(12px, 3vw, 14px);
color: var(--text-secondary);
}
/* File upload zone */
.file-upload-zone {
border: 2px dashed var(--border-medium);
border-radius: clamp(12px, 3vw, 16px);
padding: clamp(24px, 6vw, 40px);
text-align: center;
transition: all 0.3s ease;
background: var(--background-light);
cursor: pointer;
margin-bottom: clamp(12px, 3vw, 16px);
}
.file-upload-zone.dragover {
border-color: var(--success-green);
background: rgba(16, 185, 129, 0.1);
}
.file-upload-zone:hover {
border-color: var(--success-green);
background: rgba(16, 185, 129, 0.1);
}
.file-preview {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.5);
border-radius: 8px;
padding: 12px;
margin-top: 12px;
display: flex;
align-items: center;
gap: 12px;
}
/* Radio button grids */
.radio-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
gap: clamp(8px, 2vw, 12px);
}
.radio-option {
display: flex;
align-items: center;
gap: clamp(8px, 2vw, 12px);
padding: clamp(12px, 3vw, 16px);
border: 2px solid var(--border-light);
border-radius: clamp(8px, 2vw, 12px);
cursor: pointer;
background: white;
transition: all 0.2s ease;
min-height: 44px;
}
.radio-option.selected {
border-color: var(--success-green);
background: rgba(16, 185, 129, 0.1);
}
.radio-option input[type="radio"] {
margin: 0;
}
/* Processing status */
.processing-status {
padding: clamp(16px, 4vw, 20px);
background: rgba(16, 185, 129, 0.1);
border: 1px solid var(--success-green);
border-radius: clamp(8px, 2vw, 12px);
color: var(--success-dark);
font-weight: 600;
text-align: center;
margin-bottom: clamp(16px, 4vw, 24px);
}
/* Results card */
.results-card {
background: rgba(255, 255, 255, 0.9);
border-radius: 16px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
margin-top: 24px;
}
.results-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.results-content {
background: var(--background-page);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
white-space: pre-line;
line-height: 1.7;
color: var(--text-primary);
font-size: 15px;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
flex-wrap: wrap;
}
/* Button styles */
.btn {
padding: clamp(12px, 3vw, 16px) clamp(20px, 5vw, 32px);
border: none;
border-radius: clamp(8px, 2vw, 12px);
font-weight: 600;
cursor: pointer;
font-size: clamp(14px, 3.5vw, 16px);
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: transform 0.1s ease;
text-decoration: none;
}
.btn-primary {
background: var(--gradient-success);
color: white;
flex: 1;
min-width: 120px;
}
.btn-secondary {
background: var(--background-subtle);
color: var(--text-primary);
border: 2px solid var(--border-medium);
flex: 1;
min-width: 120px;
}
.btn:hover {
transform: translateY(-1px);
}
.btn:disabled {
background: var(--border-strong);
color: white;
cursor: not-allowed;
transform: none;
opacity: 0.6;
}
/* Wallet sidebar */
.wallet-section {
position: sticky;
top: 20px;
}
.wallet-balance {
font-size: clamp(24px, 6vw, 28px);
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.balance-label {
font-size: clamp(14px, 3.5vw, 16px);
color: var(--text-secondary);
margin-bottom: clamp(16px, 4vw, 20px);
}
.process-btn {
width: 100%;
margin-bottom: 12px;
}
.usage-info {
padding: 16px;
background: rgba(16, 185, 129, 0.1);
border-radius: 12px;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.usage-info h4 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: var(--success-dark);
}
.usage-info ul {
margin: 0;
font-size: 12px;
color: var(--text-primary);
line-height: 1.4;
list-style: none;
padding-left: 0;
}
.usage-info li {
margin: 4px 0;
padding-left: 16px;
position: relative;
}
.usage-info li::before {
content: "•";
position: absolute;
left: 0;
color: var(--success-green);
}
/* Responsive */
@media (max-width: 768px) {
.main-grid {
grid-template-columns: 1fr;
}
.radio-grid {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
}
</style>
{% endblock %}
{% block content %}
<div class="agent-page theme-professional">
<div class="agent-container">
<!-- Main Content -->
<div>
<form method="POST" id="dataAnalyzerForm">
{% csrf_token %}
<!-- File Upload Section -->
<div class="card">
<h3 class="section-title">📁 Upload Your Data File</h3>
<div class="file-upload-zone" id="fileUploadZone" onclick="document.getElementById('fileInput').click()">
<div style="font-size: clamp(32px, 8vw, 48px); margin-bottom: 12px;">📊</div>
<div style="font-size: clamp(16px, 4vw, 18px); font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">
Choose or drag your data file here
</div>
<div style="font-size: clamp(14px, 3.5vw, 16px); color: var(--text-secondary);">
Supports PDF, CSV, Excel files (up to 10MB)
</div>
</div>
<input
type="file"
id="fileInput"
accept=".pdf,.csv,.xlsx,.xls"
style="display: none;"
/>
<div id="filePreview" class="file-preview" style="display: none;">
<div style="font-size: 24px;">📄</div>
<div style="flex: 1;">
<div style="font-weight: 600; color: var(--text-primary);" id="fileName"></div>
<div style="font-size: 14px; color: var(--text-secondary);" id="fileSize"></div>
</div>
<button type="button" onclick="removeFile()" style="
background: var(--error-red);
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
">
Remove
</button>
</div>
</div>
<!-- Analysis Type Selection -->
<div class="card">
<h3 class="section-title">🔍 Analysis Type</h3>
<div class="radio-grid">
<label class="radio-option selected" data-value="summary">
<input type="radio" name="analysisType" value="summary" checked />
<div style="font-size: clamp(16px, 4vw, 20px);">📋</div>
<div>
<div style="font-weight: 600; margin-bottom: clamp(2px, 1vw, 4px); font-size: clamp(14px, 3.5vw, 16px);">
Summary Analysis
</div>
<div style="font-size: clamp(12px, 3vw, 14px); color: var(--text-secondary);">
Quick overview and key insights
</div>
</div>
</label>
<label class="radio-option" data-value="detailed">
<input type="radio" name="analysisType" value="detailed" />
<div style="font-size: clamp(16px, 4vw, 20px);">📈</div>
<div>
<div style="font-weight: 600; margin-bottom: clamp(2px, 1vw, 4px); font-size: clamp(14px, 3.5vw, 16px);">
Detailed Analysis
</div>
<div style="font-size: clamp(12px, 3vw, 14px); color: var(--text-secondary);">
Comprehensive statistical analysis
</div>
</div>
</label>
<label class="radio-option" data-value="statistical">
<input type="radio" name="analysisType" value="statistical" />
<div style="font-size: clamp(16px, 4vw, 20px);">📊</div>
<div>
<div style="font-weight: 600; margin-bottom: clamp(2px, 1vw, 4px); font-size: clamp(14px, 3.5vw, 16px);">
Statistical Analysis
</div>
<div style="font-size: clamp(12px, 3vw, 14px); color: var(--text-secondary);">
Advanced statistics and trends
</div>
</div>
</label>
</div>
</div>
</form>
<!-- Processing Status -->
<div id="processingStatus" class="processing-status" style="display: none;">
<div style="font-size: clamp(16px, 4vw, 18px); margin-bottom: 8px;">
⏳ Processing...
</div>
<div style="font-size: clamp(14px, 3.5vw, 16px);" id="statusText">
Analyzing your data file...
</div>
</div>
<!-- Results -->
<div id="analysisResults" class="results-card" style="display: none;">
<div class="results-header">
<div style="font-size: 24px;"></div>
<h3 style="font-size: 20px; font-weight: 600; color: var(--text-primary); margin: 0;">
Data Analysis Results
</h3>
<div style="
background: var(--success-green);
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
margin-left: auto;
">
✅ Complete
</div>
</div>
<div class="results-content" id="analysisContent">
<!-- Analysis data will be displayed here -->
</div>
<div class="action-buttons">
<button onclick="copyAnalysisReport()" class="btn btn-primary">
📋 Copy Report
</button>
<button onclick="downloadAnalysisReport()" class="btn btn-secondary">
💾 Download Report
</button>
</div>
</div>
</div>
<!-- Wallet Sidebar -->
<div class="wallet-section">
<div class="card">
<h3 class="section-title">💳 Your Wallet</h3>
<div class="wallet-balance" data-wallet-balance>{{ user.wallet_balance|floatformat:2 }} AED</div>
<div class="balance-label">Available Balance</div>
<button
type="submit"
form="dataAnalyzerForm"
class="btn btn-primary process-btn"
id="processButton"
>
📊 Analyze Data (5.00 AED)
</button>
</div>
<div class="usage-info">
<h4>💡 How it works</h4>
<ul>
<li>Upload PDF, CSV, or Excel files</li>
<li>Choose your analysis depth</li>
<li>Get AI-powered insights</li>
<li>Download comprehensive reports</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let selectedFile = null;
// File upload handling
const fileInput = document.getElementById('fileInput');
const fileUploadZone = document.getElementById('fileUploadZone');
const filePreview = document.getElementById('filePreview');
// Handle file input change
fileInput.addEventListener('change', function(e) {
if (e.target.files.length > 0) {
selectedFile = e.target.files[0];
showFilePreview(selectedFile);
}
});
// Drag and drop functionality
fileUploadZone.addEventListener('dragover', function(e) {
e.preventDefault();
fileUploadZone.classList.add('dragover');
});
fileUploadZone.addEventListener('dragleave', function(e) {
e.preventDefault();
fileUploadZone.classList.remove('dragover');
});
fileUploadZone.addEventListener('drop', function(e) {
e.preventDefault();
fileUploadZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (isValidFile(file)) {
selectedFile = file;
fileInput.files = files;
showFilePreview(file);
} else {
AgentUtils.showToast('Please select a valid data file (PDF, CSV, Excel)', 'error');
}
}
});
// Show file preview
function showFilePreview(file) {
document.getElementById('fileName').textContent = file.name;
document.getElementById('fileSize').textContent = formatFileSize(file.size);
filePreview.style.display = 'flex';
fileUploadZone.style.display = 'none';
}
// Remove file
function removeFile() {
selectedFile = null;
fileInput.value = '';
filePreview.style.display = 'none';
fileUploadZone.style.display = 'block';
}
// Validate file type
function isValidFile(file) {
const validTypes = [
'application/pdf',
'text/csv',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
return validTypes.includes(file.type) || file.name.match(/\.(pdf|csv|xlsx|xls)$/i);
}
// Format file size
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Radio button handling
document.querySelectorAll('.radio-option').forEach(option => {
option.addEventListener('click', function() {
document.querySelectorAll('.radio-option').forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
this.querySelector('input[type="radio"]').checked = true;
});
});
// Handle form submission
document.getElementById('dataAnalyzerForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!selectedFile) {
AgentUtils.showToast('Please select a data file', 'error');
return;
}
// Check user authentication
{% if not user.is_authenticated %}
window.location.href = "{% url 'authentication:login' %}";
return;
{% endif %}
// Check wallet balance
const balance = {{ user.wallet_balance|default:0 }};
if (balance < 5.00) {
AgentUtils.showToast('Insufficient balance! You need 5.00 AED.', 'error');
setTimeout(() => {
window.location.href = "{% url 'core:wallet' %}";
}, 2000);
return;
}
// Show processing status
document.getElementById('processingStatus').style.display = 'block';
document.getElementById('processButton').disabled = true;
document.getElementById('processButton').innerHTML = '⏳ Processing...';
document.getElementById('analysisResults').style.display = 'none';
// Processing steps for user feedback
const steps = [
'Reading file structure...',
'Extracting data patterns...',
'Performing statistical analysis...',
'Generating insights...',
'Finalizing report...'
];
let currentStep = 0;
const stepInterval = setInterval(() => {
if (currentStep < steps.length) {
document.getElementById('statusText').textContent = steps[currentStep];
currentStep++;
} else {
clearInterval(stepInterval);
}
}, 1000);
// Submit form data to backend
const formData = new FormData(this);
if (selectedFile) {
formData.append('file', selectedFile);
}
fetch(window.location.href, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
clearInterval(stepInterval);
if (result.success && result.request_id) {
// Start polling for results
pollForResults(result.request_id);
} else {
// Handle immediate response
document.getElementById('processingStatus').style.display = 'none';
document.getElementById('processButton').disabled = false;
document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)';
if (result.error) {
AgentUtils.showToast(`❌ ${result.error}`, 'error');
} else {
displayResults(result);
}
}
})
.catch(error => {
clearInterval(stepInterval);
console.error('Error:', error);
document.getElementById('processingStatus').style.display = 'none';
document.getElementById('processButton').disabled = false;
document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)';
AgentUtils.showToast('❌ Network error - please try again', 'error');
});
});
// Display analysis results with custom Data Analyzer formatting
function displayResults(result) {
const resultsContainer = document.getElementById('analysisResults');
const contentElement = document.getElementById('analysisContent');
if (!resultsContainer || !contentElement) {
console.error('Results elements not found');
return;
}
// Update wallet balance if provided
if (result.wallet_balance !== undefined) {
AgentUtils.updateWalletBalance(result.wallet_balance);
}
// Handle errors
if (result.error) {
AgentUtils.showToast(`❌ ${result.error}`, 'error');
return;
}
// Extract content with priority: report_text > insights_summary > raw_response.analysis
let content = '';
if (result.report_text && typeof result.report_text === 'string') {
content = result.report_text;
} else if (result.insights_summary && typeof result.insights_summary === 'string') {
content = result.insights_summary;
} else if (result.raw_response && result.raw_response.analysis && typeof result.raw_response.analysis === 'string') {
content = result.raw_response.analysis;
} else {
content = 'Data analysis completed successfully!';
}
// Parse markdown content to HTML
const formattedContent = parseMarkdownToHTML(content);
// Update content
contentElement.innerHTML = formattedContent;
// Show results
resultsContainer.style.display = 'block';
resultsContainer.scrollIntoView({ behavior: 'smooth' });
// Show success message
const successMessage = result.success ? '✅ Data analysis completed and payment processed!' : '✅ Data analysis completed!';
AgentUtils.showToast(successMessage, 'success');
}
// Enhanced markdown parser for Data Analyzer results
function parseMarkdownToHTML(markdown) {
if (!markdown || typeof markdown !== 'string') {
return '<p>No content available.</p>';
}
let html = markdown;
// Convert headers (### Header, ## Header, # Header)
html = html.replace(/^### (.*$)/gm, '<h3 style="font-size: 18px; font-weight: 600; color: var(--text-primary); margin: 20px 0 12px 0; border-bottom: 2px solid var(--success-green); padding-bottom: 8px;">$1</h3>');
html = html.replace(/^## (.*$)/gm, '<h2 style="font-size: 20px; font-weight: 600; color: var(--text-primary); margin: 24px 0 16px 0; border-bottom: 2px solid var(--success-green); padding-bottom: 8px;">$1</h2>');
html = html.replace(/^# (.*$)/gm, '<h1 style="font-size: 22px; font-weight: 600; color: var(--text-primary); margin: 28px 0 18px 0; border-bottom: 2px solid var(--success-green); padding-bottom: 10px;">$1</h1>');
// Convert bold text (**text** or __text__)
html = html.replace(/\*\*(.*?)\*\*/g, '<strong style="font-weight: 600; color: var(--text-primary);">$1</strong>');
html = html.replace(/__(.*?)__/g, '<strong style="font-weight: 600; color: var(--text-primary);">$1</strong>');
// Convert italic text (*text* or _text_)
html = html.replace(/\*(.*?)\*/g, '<em style="font-style: italic; color: var(--text-secondary);">$1</em>');
html = html.replace(/_(.*?)_/g, '<em style="font-style: italic; color: var(--text-secondary);">$1</em>');
// Convert bullet points (- item or * item)
html = html.replace(/^[\s]*[-\*]\s+(.*)$/gm, '<li style="margin: 8px 0; padding-left: 8px; color: var(--text-primary);">$1</li>');
// Wrap consecutive list items in ul tags
html = html.replace(/(<li[^>]*>.*?<\/li>\s*)+/gs, function(match) {
return `<ul style="margin: 16px 0; padding-left: 20px; list-style-type: disc; color: var(--success-green);">${match}</ul>`;
});
// Convert numbered lists (1. item, 2. item)
html = html.replace(/^\s*\d+\.\s+(.*)$/gm, '<li style="margin: 8px 0; padding-left: 8px; color: var(--text-primary);">$1</li>');
// Wrap consecutive numbered list items in ol tags
html = html.replace(/(<li[^>]*>.*?<\/li>\s*)+/gs, function(match) {
if (match.includes('ul style')) return match; // Skip if already wrapped in ul
return `<ol style="margin: 16px 0; padding-left: 20px; list-style-type: decimal; color: var(--success-green);">${match}</ol>`;
});
// Convert line breaks to paragraphs
const paragraphs = html.split(/\n\s*\n/);
html = paragraphs.map(p => {
const trimmed = p.trim();
if (trimmed === '') return '';
// Skip if already wrapped in HTML tags
if (trimmed.startsWith('<h') || trimmed.startsWith('<ul') || trimmed.startsWith('<ol') || trimmed.startsWith('<li')) {
return trimmed;
}
return `<p style="margin: 12px 0; line-height: 1.6; color: var(--text-primary);">${trimmed}</p>`;
}).filter(p => p !== '').join('');
return html;
}
// Track polling and results to prevent duplicates
let resultsDisplayed = false;
let currentPollInterval = null;
// Poll for results
function pollForResults(requestId) {
let pollCount = 0;
const maxPolls = 60; // 60 seconds maximum for data analysis
resultsDisplayed = false; // Reset flag
// Clear any existing polling
if (currentPollInterval) {
clearInterval(currentPollInterval);
currentPollInterval = null;
}
currentPollInterval = setInterval(() => {
pollCount++;
fetch(`/agents/data-analyzer/status/${requestId}/`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(result => {
if (result.status === 'completed' || result.status === 'failed') {
// Stop polling immediately
clearInterval(currentPollInterval);
currentPollInterval = null;
// Reset UI
document.getElementById('processingStatus').style.display = 'none';
document.getElementById('processButton').disabled = false;
document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)';
// Display results only once
if (!resultsDisplayed) {
resultsDisplayed = true;
displayResults(result);
}
} else if (pollCount >= maxPolls) {
clearInterval(currentPollInterval);
currentPollInterval = null;
document.getElementById('processingStatus').style.display = 'none';
document.getElementById('processButton').disabled = false;
document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)';
AgentUtils.showToast('❌ Processing timeout - please try again', 'error');
}
})
.catch(error => {
console.error('Error polling results:', error);
clearInterval(currentPollInterval);
currentPollInterval = null;
document.getElementById('processingStatus').style.display = 'none';
document.getElementById('processButton').disabled = false;
document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)';
AgentUtils.showToast('❌ Network error during processing - please try again', 'error');
});
}, 1000);
}
function copyAnalysisReport() {
const reportText = AgentUtils.generateTextForExport('analysisContent');
AgentUtils.copyToClipboard(reportText, 'Analysis report copied to clipboard!');
}
function downloadAnalysisReport() {
const reportText = AgentUtils.generateTextForExport('analysisContent');
AgentUtils.downloadAsFile(reportText, `data-analysis-report-${Date.now()}.txt`, 'Analysis report downloaded!');
}
</script>
{% endblock %}

View File

@ -190,6 +190,8 @@ The system uses Django templates in `agent_base/templates/agent_generator/` to g
#### Example Agents (Production Ready) #### Example Agents (Production Ready)
All agents now feature consistent architecture with standardized themes, unified CSS, and isolated JavaScript utilities for container-like functionality.
**Data Analysis Agent** (Price: 5.00 AED): **Data Analysis Agent** (Price: 5.00 AED):
- **N8N Integration**: PDF analysis webhook processor - **N8N Integration**: PDF analysis webhook processor
- **File Upload**: PDF, CSV, Excel files with drag-and-drop interface - **File Upload**: PDF, CSV, Excel files with drag-and-drop interface
@ -198,6 +200,8 @@ The system uses Django templates in `agent_base/templates/agent_generator/` to g
- **Form Submission Pattern**: Uses unified form submission (not button click) - **Form Submission Pattern**: Uses unified form submission (not button click)
- **Unified CSS**: Uses agent-base.css with professional theme - **Unified CSS**: Uses agent-base.css with professional theme
- **Text Display**: Simple text formatting (no complex markdown parsing) - **Text Display**: Simple text formatting (no complex markdown parsing)
- **Architecture**: Standard agent-container grid layout (1fr 350px) with proper wallet positioning
- **JavaScript Isolation**: DataAnalyzerUtils with agent-specific functionality
**Weather Reporter Agent** (Price: 2.00 AED): **Weather Reporter Agent** (Price: 2.00 AED):
- **API Integration**: OpenWeatherMap API with direct calls - **API Integration**: OpenWeatherMap API with direct calls
@ -205,7 +209,9 @@ The system uses Django templates in `agent_base/templates/agent_generator/` to g
- **Formatted Reports**: Both current and detailed weather reports - **Formatted Reports**: Both current and detailed weather reports
- **Real-time Results**: Dynamic display below form - **Real-time Results**: Dynamic display below form
- **Error Handling**: API failures and invalid locations - **Error Handling**: API failures and invalid locations
- **Unified CSS**: Uses agent-base.css with minimal theme - **Unified CSS**: Uses agent-base.css with professional theme
- **Architecture**: Standard agent-container grid layout with proper structure
- **JavaScript Isolation**: WeatherUtils with agent-specific functionality
**Social Ads Generator Agent** (Price: 7.00 AED): **Social Ads Generator Agent** (Price: 7.00 AED):
- **N8N Integration**: Social media ad generation via webhook - **N8N Integration**: Social media ad generation via webhook
@ -213,6 +219,8 @@ The system uses Django templates in `agent_base/templates/agent_generator/` to g
- **Multi-language**: English, Arabic, Spanish, French, German, Chinese - **Multi-language**: English, Arabic, Spanish, French, German, Chinese
- **Real-time Results**: Dynamic content generation and display - **Real-time Results**: Dynamic content generation and display
- **Unified CSS**: Uses agent-base.css with creative theme (glassmorphism) - **Unified CSS**: Uses agent-base.css with creative theme (glassmorphism)
- **Architecture**: Standard agent-container grid layout with glassmorphism styling
- **JavaScript Isolation**: SocialAdsUtils with agent-specific functionality
**Job Posting Generator Agent** (Price: 4.00 AED): **Job Posting Generator Agent** (Price: 4.00 AED):
- **N8N Integration**: Professional job posting creation - **N8N Integration**: Professional job posting creation
@ -220,6 +228,17 @@ The system uses Django templates in `agent_base/templates/agent_generator/` to g
- **Multi-language Support**: Multiple output languages - **Multi-language Support**: Multiple output languages
- **Enhanced UX**: Progressive form validation and real-time feedback - **Enhanced UX**: Progressive form validation and real-time feedback
- **Unified CSS**: Uses agent-base.css with professional theme - **Unified CSS**: Uses agent-base.css with professional theme
- **Architecture**: Standard agent-container grid layout with professional styling
- **JavaScript Isolation**: JobPostingUtils with agent-specific functionality
**Five Whys Analysis Agent** (Price: 3.00 AED):
- **N8N Integration**: Problem analysis using Five Whys methodology
- **Comprehensive UX**: Enhanced UI with styled cards and professional layout
- **Multi-language Support**: Multiple output languages
- **Real-time Results**: Dynamic analysis generation and display
- **Unified CSS**: Uses agent-base.css with professional theme
- **Architecture**: Standard agent-container grid layout with consistent styling
- **JavaScript Isolation**: FiveWhysUtils with agent-specific functionality
### Management Commands ### Management Commands
@ -537,7 +556,32 @@ All agents now use a unified CSS system for consistent user experience and maint
#### Core Files #### Core Files
- **`/static/css/agent-base.css`**: Unified component library for all agents - **`/static/css/agent-base.css`**: Unified component library for all agents
- **`/static/css/themes.css`**: Global color variables and themes - **`/static/css/themes.css`**: Global color variables and themes
- **`/static/js/agent-utils.js`**: Shared JavaScript utilities for all agents - **Agent-specific JavaScript utilities**: Each agent has isolated JavaScript functions for container-like functionality
### Agent Isolation Architecture
#### Container-like Functionality
All agents now implement true isolation to prevent cross-agent interference:
**JavaScript Isolation Pattern:**
```javascript
// Each agent has its own utility namespace
const DataAnalyzerUtils = { /* agent-specific functions */ };
const WeatherUtils = { /* agent-specific functions */ };
const SocialAdsUtils = { /* agent-specific functions */ };
const JobPostingUtils = { /* agent-specific functions */ };
const FiveWhysUtils = { /* agent-specific functions */ };
// For backward compatibility, each agent creates AgentUtils alias
const AgentUtils = DataAnalyzerUtils; // or appropriate agent utils
```
**Benefits of Isolation:**
- No shared dependencies between agents
- Changes to one agent don't affect others
- Agent-specific functionality can be customized
- Easier debugging and maintenance
- Container-like isolation without containerization complexity
#### Theme System #### Theme System
The unified CSS supports multiple themes via CSS custom properties: The unified CSS supports multiple themes via CSS custom properties:
@ -562,19 +606,59 @@ The unified CSS supports multiple themes via CSS custom properties:
<div class="agent-page theme-professional"> <!-- or theme-creative, theme-minimal --> <div class="agent-page theme-professional"> <!-- or theme-creative, theme-minimal -->
<div class="agent-container"> <div class="agent-container">
<div> <div>
<!-- Main content area --> <!-- Main content area (first grid column) -->
<div class="card"> <div class="card">
<h3 class="section-title">Agent Title</h3> <h3 class="section-title">Agent Title</h3>
<!-- Agent form and content --> <!-- Agent form and content -->
</div> </div>
<!-- Processing status and results stay within first grid column -->
</div> </div>
<div class="wallet-section"> <div class="wallet-section">
<!-- Wallet sidebar --> <!-- Wallet sidebar (second grid column) -->
<div class="card">
<h3 class="section-title">💳 Your Wallet</h3>
<!-- Wallet content -->
</div>
</div> </div>
</div> </div>
</div> </div>
``` ```
#### Standardized Layout Architecture
**Grid Layout System:**
- `agent-container`: CSS Grid with `grid-template-columns: 1fr 350px`
- **First column**: Main content, forms, processing status, results
- **Second column**: Wallet sidebar (350px width)
- **Mobile responsive**: Single column on screens < 768px
**Critical Structure Rules:**
1. **Wallet positioning**: `wallet-section` must be a direct child of `agent-container` (separate grid column)
2. **Content hierarchy**: All agent content stays in first grid column
3. **Processing status**: Displays below form, spans full width of first column
4. **Results display**: Shows below processing status in first column
**Data Analyzer Wallet Fix Example:**
```html
<!-- ❌ INCORRECT: wallet-section inside main content -->
<div class="agent-container">
<div>
<form>...</form>
<div class="wallet-section">...</div> <!-- Wrong placement -->
</div>
</div>
<!-- ✅ CORRECT: wallet-section as separate grid column -->
<div class="agent-container">
<div>
<form>...</form>
<!-- Processing Status -->
<!-- Results -->
</div>
<div class="wallet-section">...</div> <!-- Correct placement -->
</div>
```
### Text Display Standardization ### Text Display Standardization
#### Simple Text Formatting Approach #### Simple Text Formatting Approach
@ -675,6 +759,27 @@ The project uses a clean, modular individual agent architecture:
- **Data Attributes**: Add `data-wallet-balance` to all balance elements for easy targeting - **Data Attributes**: Add `data-wallet-balance` to all balance elements for easy targeting
- **Continuous Workflow**: Allow multiple requests without page refresh ("Get Another" functionality) - **Continuous Workflow**: Allow multiple requests without page refresh ("Get Another" functionality)
- **Clear User Feedback**: Show "payment processed" vs "no charge applied" messages - **Clear User Feedback**: Show "payment processed" vs "no charge applied" messages
- **Standardized Layout**: Use agent-container grid layout (1fr 350px) with proper wallet positioning
- **Theme Consistency**: Apply unified CSS themes across all agents
- **JavaScript Isolation**: Agent-specific utilities for container-like functionality
#### Recent Standardization Improvements (2024)
**Agent Architecture Consistency:**
All 5 production agents now follow standardized patterns:
1. **Data Analyzer**: Reduced custom CSS from 400+ lines to ~78 lines, standardized HTML structure
2. **Job Posting Generator**: Enhanced with professional theme and proper grid layout
3. **Five Whys Analyzer**: Applied black and white theme with consistent styling
4. **Social Ads Generator**: Maintained creative theme while standardizing structure
5. **Weather Reporter**: Professional theme with clean weather data presentation
**Key Improvements Made:**
- **HTML Structure**: All agents use standard `agent-container` grid layout
- **CSS Consolidation**: Removed duplicate styles, standardized on `agent-base.css`
- **Wallet Positioning**: Fixed wallet appearing at bottom vs. right side across all agents
- **JavaScript Isolation**: Each agent has isolated utilities (DataAnalyzerUtils, WeatherUtils, etc.)
- **Theme Application**: Consistent theme implementation across all agents
- **Code Reduction**: Eliminated 400+ lines of redundant CSS code
#### Frontend JavaScript Requirements #### Frontend JavaScript Requirements
```javascript ```javascript

View File

@ -1,146 +1,240 @@
{% extends "base.html" %} {% extends "base.html" %}
{% csrf_token %} {% load static %}
{% block title %}5 Whys Analysis Agent - NetCop AI Hub{% endblock %} {% block title %}5 Whys Analysis Agent - NetCop AI Hub{% endblock %}
{% block extra_css %}
<!-- Optimized Font Loading -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<!-- External Stylesheets -->
<link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Five Whys Analyzer Specific Utilities -->
<script>
// Five Whys Analyzer - Self-contained utilities (no shared dependencies)
const FiveWhysUtils = {
/**
* Update wallet balance display - Five Whys specific
*/
updateWalletBalance(newBalance) {
// Update header balance (anchor tag with emoji)
const headerBalance = document.querySelector('a[data-wallet-balance]');
if (headerBalance) {
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
}
// Update page balance (div without emoji)
const pageBalance = document.querySelector('div[data-wallet-balance]');
if (pageBalance) {
pageBalance.textContent = `${newBalance.toFixed(2)} AED`;
}
window.currentWalletBalance = newBalance;
},
/**
* Show toast notification with duplicate prevention
*/
showToast(message, type = 'info') {
// Prevent duplicate toasts
const existingToast = document.querySelector('.five-whys-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'five-whys-toast';
toast.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
padding: 8px 12px;
border-radius: 4px;
color: white;
font-size: 13px;
z-index: 1000;
max-width: 300px;
font-weight: 500;
${type === 'success' ? 'background: #10b981;' : 'background: #ef4444;'}
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 2000);
},
/**
* Generate text for copy/download functionality
*/
generateTextForExport(contentElementId) {
const content = document.getElementById(contentElementId);
if (content) {
return content.innerText || content.textContent || '';
}
return 'No content available';
},
/**
* Copy content to clipboard
*/
copyToClipboard(text, successMessage = 'Content copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
this.showToast(`📋 ${successMessage}`, 'success');
}).catch(() => {
this.showToast('Failed to copy content', 'error');
});
},
/**
* Download content as text file
*/
downloadAsFile(text, filename, successMessage = 'File downloaded!') {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `content-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
this.showToast(`💾 ${successMessage}`, 'success');
}
};
// For backward compatibility, create AgentUtils alias
const AgentUtils = FiveWhysUtils;
</script>
{% endblock %}
{% block content %} {% block content %}
<div class="container" style="max-width: 1280px; margin: 0 auto; padding: clamp(20px, 5vw, 40px) clamp(16px, 4vw, 24px);"> <div class="agent-page theme-professional">
<!-- Main Content Grid --> <div class="agent-container">
<div class="main-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(min(350px, 100%), 1fr)); gap: clamp(16px, 4vw, 24px); align-items: start;"> <!-- Main Content -->
<!-- Chat Interface -->
<div> <div>
<!-- Agent Header --> <div class="card">
<div class="card" style="background: rgba(255, 255, 255, 0.9); border-radius: clamp(12px, 3vw, 16px); padding: clamp(16px, 4vw, 24px); border: 1px solid rgba(255, 255, 255, 0.3); backdrop-filter: blur(20px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); margin-bottom: clamp(16px, 4vw, 24px);"> <h3 class="section-title">🔍 {{ agent.name }}</h3>
<h1 style="font-size: clamp(20px, 5vw, 24px); font-weight: 700; color: #1f2937; margin: 0 0 8px 0; display: flex; align-items: center; gap: 12px;"> <p class="form-help">{{ agent.description }}</p>
🔍 {{ agent.name }}
</h1>
<p style="font-size: clamp(14px, 3.5vw, 16px); color: #6b7280; margin: 0;">
{{ agent.description }}
</p>
</div> </div>
<!-- Chat Messages Container --> <!-- Chat Messages Container -->
<div class="card" id="chatContainer" style="background: rgba(255, 255, 255, 0.9); border-radius: clamp(12px, 3vw, 16px); padding: clamp(16px, 4vw, 24px); border: 1px solid rgba(255, 255, 255, 0.3); backdrop-filter: blur(20px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); margin-bottom: clamp(16px, 4vw, 24px); min-height: 400px; max-height: 600px; overflow-y: auto;"> <div class="card">
<h3 style="font-size: clamp(16px, 4vw, 18px); font-weight: 600; color: #1f2937; margin: 0 0 16px 0;">💬 Chat with 5 Whys Analyst</h3> <div id="chatContainer" class="chat-container">
<h4 class="section-subtitle">💬 Chat with 5 Whys Analyst</h4>
<!-- Welcome Message --> <!-- Welcome Message -->
<div class="message assistant-message" style="margin-bottom: 16px; padding: 12px 16px; background: #f8fafc; border-radius: 12px; border-left: 4px solid #6366f1;"> <div class="welcome-message">
<div style="font-weight: 600; color: #4338ca; margin-bottom: 4px;">5 Whys Analyst</div> <div class="message-header">5 Whys Analyst</div>
<div style="color: #374151; line-height: 1.5;"> <div class="message-content">
Hello! I'm here to help you with root cause analysis using the 5 Whys methodology. Hello! I'm here to help you with root cause analysis using the 5 Whys methodology.
You can ask me questions, describe your problem, and I'll guide you through the analysis process. When you're ready, I can generate a comprehensive report for 8.00 AED. You can ask me questions, describe your problem, and I'll guide you through the analysis process. When you're ready, I can generate a comprehensive report for {{ agent.price }} AED.
How can I help you today? How can I help you today?
</div>
</div> </div>
<!-- Chat messages will be dynamically added here -->
<div id="chatMessages"></div>
</div> </div>
<!-- Chat messages will be dynamically added here --> <!-- Chat Input Form -->
<div id="chatMessages"></div> <form id="chatForm" class="chat-form">
{% csrf_token %}
<div class="input-group">
<input
type="text"
id="chatInput"
name="message"
class="form-input"
placeholder="Ask me about your problem or describe what you'd like to analyze..."
required
/>
<button
id="sendChatBtn"
type="submit"
class="btn btn-primary"
>
📤 Send
</button>
</div>
</form>
</div> </div>
<!-- Chat Input --> <!-- Report Generation Section -->
<div class="card" style="background: rgba(255, 255, 255, 0.9); border-radius: clamp(12px, 3vw, 16px); padding: clamp(16px, 4vw, 24px); border: 1px solid rgba(255, 255, 255, 0.3); backdrop-filter: blur(20px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); margin-bottom: clamp(16px, 4vw, 24px);"> <div class="card" id="reportSection">
<div style="display: flex; gap: 12px; align-items: flex-end;"> <h4 class="section-subtitle">📋 Generate Final Report</h4>
<textarea
id="chatInput"
placeholder="Ask me about your problem or describe what you'd like to analyze..."
style="flex: 1; padding: 12px 16px; border: 2px solid #e5e7eb; border-radius: 12px; font-size: 14px; resize: vertical; min-height: 48px; max-height: 120px; font-family: inherit;"
rows="2"
></textarea>
<button
id="sendChatBtn"
onclick="sendChatMessage()"
style="padding: 12px 20px; background: linear-gradient(135deg, #6366f1 0%, #4338ca 100%); color: white; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; min-height: 48px; transition: transform 0.1s ease;"
>
Send
</button>
</div>
</div>
<!-- Report Generation Form --> <div id="reportNotReady" class="info-message">
<div class="card" id="reportForm" style="background: rgba(255, 255, 255, 0.9); border-radius: clamp(12px, 3vw, 16px); padding: clamp(16px, 4vw, 24px); border: 1px solid rgba(255, 255, 255, 0.3); backdrop-filter: blur(20px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); margin-bottom: clamp(16px, 4vw, 24px);">
<h3 style="font-size: clamp(16px, 4vw, 18px); font-weight: 600; color: #1f2937; margin: 0 0 16px 0;">📋 Generate Final Report</h3>
<div id="reportNotReady" style="padding: 16px; background: #f3f4f6; border-radius: 12px; text-align: center; color: #6b7280; font-size: 14px; margin-bottom: 16px;">
💬 Ask 2-3 questions about your problem first, then I'll generate a comprehensive report 💬 Ask 2-3 questions about your problem first, then I'll generate a comprehensive report
</div> </div>
<div id="reportReady" style="display: none; padding: 16px; background: #ecfdf5; border-radius: 12px; text-align: center; color: #059669; font-size: 14px; margin-bottom: 16px;"> <div id="reportReady" class="success-message" style="display: none;">
✅ Ready! I can now generate a detailed 5 Whys analysis report based on our conversation ✅ Ready! I can now generate a detailed 5 Whys analysis report based on our conversation
</div> </div>
<button <button
id="generateReportBtn" id="generateReportBtn"
onclick="generateReport()" onclick="generateReport()"
class="btn btn-primary"
disabled disabled
style="width: 100%; padding: 16px 20px; background: #9ca3af; color: white; border: none; border-radius: 12px; font-weight: 600; cursor: not-allowed; font-size: 16px; transition: all 0.3s ease;" style="width: 100%;"
> >
🔍 Generate Report ({{ agent.price }} AED) 🔍 Generate Report ({{ agent.price }} AED)
</button> </button>
</div> </div>
<!-- Generated Report Display --> <!-- Generated Report Display -->
<div id="reportResults" class="card" style="background: rgba(255, 255, 255, 0.9); border-radius: 16px; padding: 24px; border: 1px solid rgba(255, 255, 255, 0.3); backdrop-filter: blur(20px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); margin-top: 24px; display: none;"> <div id="reportResults" class="results-card" style="display: none;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;"> <div class="results-header">
<div style="font-size: 24px;"></div> <div style="font-size: 24px;">📊</div>
<h3 style="font-size: 20px; font-weight: 600; color: #1f2937; margin: 0;">5 Whys Analysis Report</h3> <h3 style="font-size: 20px; font-weight: 600; color: var(--text-primary); margin: 0;">5 Whys Analysis Report</h3>
<div style="background: #6366f1; color: white; padding: 6px 12px; border-radius: 6px; font-size: 14px; font-weight: 600; margin-left: auto;"> <div style="background: var(--primary-color); color: white; padding: 6px 12px; border-radius: 6px; font-size: 14px; font-weight: 600; margin-left: auto;">✅ Complete</div>
✅ Complete
</div>
</div> </div>
<div id="reportContent" style="background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 32px; margin-bottom: 20px; line-height: 1.7; color: #374151; font-size: 15px; box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05);"> <div class="results-content" id="reportContent">
<!-- Report content will be displayed here --> <!-- Report content will be displayed here -->
</div> </div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;"> <div class="action-buttons">
<button onclick="copyReport()" style="padding: 12px 20px; background: linear-gradient(135deg, #6366f1 0%, #4338ca 100%); color: white; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; transition: transform 0.1s ease;"> <button onclick="copyReport()" class="btn btn-primary">📋 Copy Report</button>
📋 Copy Report <button onclick="downloadReport()" class="btn btn-secondary">💾 Download Report</button>
</button>
<button onclick="downloadReport()" style="padding: 12px 20px; background: white; color: #374151; border: 2px solid #e5e7eb; border-radius: 12px; font-weight: 600; cursor: pointer; transition: transform 0.1s ease;">
💾 Download Report
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Wallet Sidebar --> <!-- Wallet Sidebar -->
<div style="position: sticky; top: 20px;"> <div class="wallet-section">
<div class="card" style="background: rgba(255, 255, 255, 0.9); border-radius: clamp(12px, 3vw, 16px); padding: clamp(16px, 4vw, 24px); border: 1px solid rgba(255, 255, 255, 0.3); backdrop-filter: blur(20px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); margin-bottom: clamp(16px, 4vw, 24px);"> <div class="card">
<h3 style="font-size: clamp(16px, 4vw, 18px); font-weight: 600; color: #1f2937; margin: 0 0 16px 0;">💳 Your Wallet</h3> <h3 class="section-title">💳 Your Wallet</h3>
<div style="font-size: clamp(24px, 6vw, 28px); font-weight: 700; color: #1f2937; margin-bottom: 8px;" data-wallet-balance> <div class="wallet-balance" data-wallet-balance>{% if user.is_authenticated %}{{ user.wallet_balance|floatformat:2 }} AED{% else %}0.00 AED{% endif %}</div>
{{ user.wallet_balance|floatformat:2 }} AED <div class="balance-label">Available Balance</div>
</div>
<div style="font-size: clamp(14px, 3.5vw, 16px); color: #6b7280; margin-bottom: 20px;">
Available Balance
</div>
<a href="{% url 'core:wallet_topup' %}" style="display: block; width: 100%; padding: 16px 20px; background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; border-radius: 12px; font-weight: 600; text-decoration: none; text-align: center; margin-bottom: 12px;"> {% if user.is_authenticated %}
💳 Top Up Wallet <a href="{% url 'core:wallet' %}" class="btn btn-primary" style="text-decoration: none; margin-top: 16px;">
</a> 💰 Top Up Wallet
</a>
{% else %}
<a href="{% url 'authentication:login' %}" class="btn btn-primary process-btn" style="text-decoration: none;">
🔑 Login to Continue
</a>
{% endif %}
</div> </div>
<div style="padding: 16px; background: rgba(99, 102, 241, 0.1); border-radius: 12px; border: 1px solid rgba(99, 102, 241, 0.2);"> <div class="usage-info">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600; color: #4338ca;">💡 How it works</h4> <h4>💡 How it works</h4>
<ul style="margin: 0; font-size: 12px; color: #374151; line-height: 1.4; list-style: none; padding-left: 0;"> <ul>
<li style="margin: 4px 0; padding-left: 16px; position: relative;"> <li>Chat freely to explore your problem</li>
<span style="position: absolute; left: 0; color: #6366f1;"></span> <li>Get guidance and ask questions</li>
Chat freely to explore your problem <li>Generate final report when ready</li>
</li> <li>Pay only for the final report</li>
<li style="margin: 4px 0; padding-left: 16px; position: relative;">
<span style="position: absolute; left: 0; color: #6366f1;"></span>
Get guidance and ask questions
</li>
<li style="margin: 4px 0; padding-left: 16px; position: relative;">
<span style="position: absolute; left: 0; color: #6366f1;"></span>
Generate final report when ready
</li>
<li style="margin: 4px 0; padding-left: 16px; position: relative;">
<span style="position: absolute; left: 0; color: #6366f1;"></span>
Pay only for the final report
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -148,138 +242,186 @@
</div> </div>
<style> <style>
.container { .chat-container {
background: linear-gradient(135deg, #f6f8ff 0%, #e8f0fe 50%, #f0f7ff 100%); background: var(--background-subtle);
min-height: 100vh; border-radius: 8px;
color: #1f2937; padding: 16px;
} margin: 16px 0;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
}
.card:hover { .welcome-message {
transform: translateY(-1px); margin-bottom: 16px;
transition: transform 0.2s ease; padding: 16px 20px;
} background: var(--background-subtle);
border-radius: 16px 16px 16px 4px;
border-left: 4px solid var(--primary-color);
line-height: 1.6;
}
button:hover { .message-header {
transform: translateY(-1px); font-weight: 600;
} color: var(--primary-color);
margin-bottom: 8px;
}
button:disabled { .message-content {
background: #9ca3af !important; color: var(--text-primary);
cursor: not-allowed !important; line-height: 1.6;
transform: none !important; }
}
.message { .chat-form {
margin-bottom: 16px; border-top: 1px solid var(--border-color);
animation: fadeIn 0.3s ease; padding-top: 16px;
} margin-top: 16px;
}
.user-message { .input-group {
margin-left: 20%; display: flex;
padding: 12px 16px; gap: 12px;
background: #6366f1; align-items: stretch;
color: white; }
border-radius: 16px 16px 4px 16px;
}
.assistant-message { .input-group .form-input {
margin-right: 20%; width: 80%;
padding: 16px 20px; height: 40px;
background: #f8fafc; border: 2px solid var(--border-color);
border-radius: 16px 16px 16px 4px; border-radius: 8px;
border-left: 4px solid #6366f1; padding: 8px 12px;
line-height: 1.6; font-family: inherit;
} font-size: 14px;
background: var(--background-primary);
color: var(--text-primary);
box-sizing: border-box;
}
.assistant-message .message-content { .input-group .form-input:focus {
color: #374151; outline: none;
line-height: 1.6; border-color: var(--primary-color);
} box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.assistant-message .message-content h3 { .input-group .btn {
color: #1f2937; height: 40px;
font-size: 16px; padding: 8px 12px;
font-weight: 600; width: 20%;
margin: 16px 0 8px 0; display: flex;
} align-items: center;
justify-content: center;
flex-shrink: 0;
box-sizing: border-box;
font-size: 13px;
}
.assistant-message .message-content h3:first-child { .info-message {
margin-top: 0; padding: 16px;
} background: var(--background-subtle);
border-radius: 8px;
text-align: center;
color: var(--text-secondary);
margin-bottom: 16px;
}
.assistant-message .message-content ul { .success-message {
margin: 8px 0; padding: 16px;
padding-left: 20px; background: #ecfdf5;
} border-radius: 8px;
text-align: center;
color: #059669;
margin-bottom: 16px;
}
.assistant-message .message-content li { .message {
margin: 4px 0; margin-bottom: 16px;
} animation: fadeIn 0.3s ease;
}
.assistant-message .message-content p { .user-message {
margin: 8px 0; margin-left: 20%;
} padding: 12px 16px;
background: var(--primary-color);
color: white;
border-radius: 16px 16px 4px 16px;
}
.assistant-message .message-content p:first-child { .assistant-message {
margin-top: 0; margin-right: 20%;
} padding: 16px 20px;
background: var(--background-subtle);
border-radius: 16px 16px 16px 4px;
border-left: 4px solid var(--primary-color);
line-height: 1.6;
}
.assistant-message .message-content p:last-child { .assistant-message .message-content {
margin-bottom: 0; color: var(--text-primary);
} line-height: 1.6;
}
.assistant-message .message-content strong { .assistant-message .message-content h3 {
color: #1f2937; color: var(--text-primary);
font-weight: 600; font-size: 16px;
} font-weight: 600;
margin: 16px 0 8px 0;
}
@keyframes fadeIn { .assistant-message .message-content h3:first-child {
from { opacity: 0; transform: translateY(10px); } margin-top: 0;
to { opacity: 1; transform: translateY(0); } }
}
.typing-dots { .assistant-message .message-content ul {
display: flex; margin: 8px 0;
gap: 4px; padding-left: 20px;
align-items: center; }
}
.typing-dots span { .assistant-message .message-content li {
width: 6px; margin: 4px 0;
height: 6px; }
border-radius: 50%;
background: #6366f1;
animation: typingDots 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(1) { .assistant-message .message-content p {
animation-delay: 0s; margin: 8px 0;
} }
.typing-dots span:nth-child(2) { .assistant-message .message-content p:first-child {
animation-delay: 0.2s; margin-top: 0;
} }
.typing-dots span:nth-child(3) { .assistant-message .message-content p:last-child {
animation-delay: 0.4s; margin-bottom: 0;
} }
@keyframes typingDots { .assistant-message .message-content strong {
0%, 80%, 100% { color: var(--text-primary);
transform: scale(0); font-weight: 600;
opacity: 0.5; }
} @keyframes fadeIn {
40% { from { opacity: 0; transform: translateY(10px); }
transform: scale(1); to { opacity: 1; transform: translateY(0); }
opacity: 1; }
}
}
@media (max-width: 768px) { .typing-dots {
.main-grid { display: flex;
grid-template-columns: 1fr !important; gap: 4px;
} align-items: center;
} }
.typing-dots span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary-color);
animation: typing 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(1) { animation-delay: -0.32s; }
.typing-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
</style> </style>
<script> <script>
@ -292,8 +434,14 @@
// Generate new session ID // Generate new session ID
currentSessionId = generateSessionId(); currentSessionId = generateSessionId();
// Add Enter key support for chat input // Handle chat form submission
document.getElementById('chatInput').addEventListener('keypress', function(e) { document.getElementById('chatForm').addEventListener('submit', function(e) {
e.preventDefault();
sendChatMessage();
});
// Add Enter key support for chat input (Shift+Enter for new line)
document.getElementById('chatInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
sendChatMessage(); sendChatMessage();
@ -507,8 +655,6 @@
const btn = document.getElementById('generateReportBtn'); const btn = document.getElementById('generateReportBtn');
btn.disabled = false; btn.disabled = false;
btn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)';
btn.style.cursor = 'pointer';
} }
} }
@ -559,8 +705,10 @@
// Display the generated report // Display the generated report
displayReport(data.report); displayReport(data.report);
// Update wallet balance // Update wallet balance if provided
AgentUtils.updateWalletBalance(data.wallet_balance); if (data.wallet_balance !== undefined) {
AgentUtils.updateWalletBalance(data.wallet_balance);
}
AgentUtils.showToast('✅ Report generated and payment processed!', 'success'); AgentUtils.showToast('✅ Report generated and payment processed!', 'success');
} else { } else {

View File

@ -13,8 +13,104 @@
<link rel="stylesheet" href="{% static 'css/themes.css' %}"> <link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}"> <link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Shared JavaScript Utilities --> <!-- Job Posting Generator Specific Utilities -->
<script src="{% static 'js/agent-utils.js' %}"></script> <script>
// Job Posting Generator - Self-contained utilities (no shared dependencies)
const JobPostingUtils = {
/**
* Update wallet balance display - Job Posting Generator specific
*/
updateWalletBalance(newBalance) {
// Update header balance (anchor tag with emoji)
const headerBalance = document.querySelector('a[data-wallet-balance]');
if (headerBalance) {
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
}
// Update page balance (div without emoji)
const pageBalance = document.querySelector('div[data-wallet-balance]');
if (pageBalance) {
pageBalance.textContent = `${newBalance.toFixed(2)} AED`;
}
window.currentWalletBalance = newBalance;
},
/**
* Show toast notification with duplicate prevention
*/
showToast(message, type = 'info') {
// Prevent duplicate toasts
const existingToast = document.querySelector('.job-posting-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'job-posting-toast';
toast.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
padding: 8px 12px;
border-radius: 4px;
color: white;
font-size: 13px;
z-index: 1000;
max-width: 300px;
font-weight: 500;
${type === 'success' ? 'background: #10b981;' : 'background: #ef4444;'}
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 2000);
},
/**
* Generate text for copy/download functionality
*/
generateTextForExport(contentElementId) {
const content = document.getElementById(contentElementId);
if (content) {
return content.innerText || content.textContent || '';
}
return 'No content available';
},
/**
* Copy content to clipboard
*/
copyToClipboard(text, successMessage = 'Content copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
this.showToast(`📋 ${successMessage}`, 'success');
}).catch(() => {
this.showToast('Failed to copy content', 'error');
});
},
/**
* Download content as text file
*/
downloadAsFile(text, filename, successMessage = 'File downloaded!') {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `content-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
this.showToast(`💾 ${successMessage}`, 'success');
}
};
// For backward compatibility, create AgentUtils alias
const AgentUtils = JobPostingUtils;
</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -13,8 +13,129 @@
<link rel="stylesheet" href="{% static 'css/themes.css' %}"> <link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}"> <link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Shared JavaScript Utilities --> <!-- Social Ads Generator Specific Utilities -->
<script src="{% static 'js/agent-utils.js' %}"></script> <script>
// Social Ads Generator - Self-contained utilities (no shared dependencies)
const SocialAdsUtils = {
/**
* Update wallet balance display - Social Ads specific
*/
updateWalletBalance(newBalance) {
// Update header balance (anchor tag with emoji)
const headerBalance = document.querySelector('a[data-wallet-balance]');
if (headerBalance) {
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
}
// Update page balance (div without emoji)
const pageBalance = document.querySelector('div[data-wallet-balance]');
if (pageBalance) {
pageBalance.textContent = `${newBalance.toFixed(2)} AED`;
}
window.currentWalletBalance = newBalance;
},
/**
* Show toast notification with duplicate prevention
*/
showToast(message, type = 'info') {
// Prevent duplicate toasts
const existingToast = document.querySelector('.social-ads-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'social-ads-toast';
toast.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
padding: 8px 12px;
border-radius: 4px;
color: white;
font-size: 13px;
z-index: 1000;
max-width: 300px;
font-weight: 500;
${type === 'success' ? 'background: #10b981;' : 'background: #ef4444;'}
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 2000);
},
/**
* Generate text for copy/download functionality
*/
generateTextForExport(contentElementId) {
const content = document.getElementById(contentElementId);
if (content) {
return content.innerText || content.textContent || '';
}
return 'No content available';
},
/**
* Copy content to clipboard
*/
copyToClipboard(text, successMessage = 'Content copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
this.showToast(`📋 ${successMessage}`, 'success');
}).catch(() => {
this.showToast('Failed to copy content', 'error');
});
},
/**
* Download content as text file
*/
downloadAsFile(text, filename, successMessage = 'File downloaded!') {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `content-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
this.showToast(`💾 ${successMessage}`, 'success');
},
/**
* Reset UI to initial state - Social Ads specific
*/
resetUI(config) {
const elements = {
processingStatus: document.getElementById(config.processingStatusId || 'processingStatus'),
processButton: document.getElementById(config.processButtonId || 'processButton'),
results: document.getElementById(config.resultsId)
};
if (elements.processingStatus) {
elements.processingStatus.style.display = 'none';
}
if (elements.processButton) {
elements.processButton.disabled = false;
elements.processButton.innerHTML = config.buttonText || 'Process';
elements.processButton.classList.remove('loading');
}
if (elements.results) {
elements.results.style.display = 'none';
}
}
};
// For backward compatibility, create AgentUtils alias
const AgentUtils = SocialAdsUtils;
</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -80,11 +80,23 @@ window.AgentUtils = {
* Update wallet balance display across all agents * Update wallet balance display across all agents
*/ */
updateWalletBalance(newBalance) { updateWalletBalance(newBalance) {
const balanceElement = document.querySelector('[data-wallet-balance]') || // Update all wallet balance elements
document.getElementById('walletBalance'); document.querySelectorAll('[data-wallet-balance]').forEach(element => {
if (balanceElement) { if (element.tagName === 'A') {
balanceElement.textContent = `${newBalance.toFixed(2)} AED`; // Header balance with emoji (anchor tag)
element.textContent = `💰 ${newBalance.toFixed(2)} AED`;
} else {
// Page balance without emoji (div or other elements)
element.textContent = `${newBalance.toFixed(2)} AED`;
}
});
// Fallback for old ID-based elements
const legacyElement = document.getElementById('walletBalance');
if (legacyElement) {
legacyElement.textContent = `${newBalance.toFixed(2)} AED`;
} }
window.currentWalletBalance = newBalance; window.currentWalletBalance = newBalance;
}, },

View File

@ -13,8 +13,104 @@
<link rel="stylesheet" href="{% static 'css/themes.css' %}"> <link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}"> <link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Shared JavaScript Utilities --> <!-- Weather Reporter Specific Utilities -->
<script src="{% static 'js/agent-utils.js' %}"></script> <script>
// Weather Reporter - Self-contained utilities (no shared dependencies)
const WeatherUtils = {
/**
* Update wallet balance display - Weather Reporter specific
*/
updateWalletBalance(newBalance) {
// Update header balance (anchor tag with emoji)
const headerBalance = document.querySelector('a[data-wallet-balance]');
if (headerBalance) {
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
}
// Update page balance (div without emoji)
const pageBalance = document.querySelector('div[data-wallet-balance]');
if (pageBalance) {
pageBalance.textContent = `${newBalance.toFixed(2)} AED`;
}
window.currentWalletBalance = newBalance;
},
/**
* Show toast notification with duplicate prevention
*/
showToast(message, type = 'info') {
// Prevent duplicate toasts
const existingToast = document.querySelector('.weather-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'weather-toast';
toast.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
padding: 8px 12px;
border-radius: 4px;
color: white;
font-size: 13px;
z-index: 1000;
max-width: 300px;
font-weight: 500;
${type === 'success' ? 'background: #10b981;' : 'background: #ef4444;'}
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 2000);
},
/**
* Generate text for copy/download functionality
*/
generateTextForExport(contentElementId) {
const content = document.getElementById(contentElementId);
if (content) {
return content.innerText || content.textContent || '';
}
return 'No content available';
},
/**
* Copy content to clipboard
*/
copyToClipboard(text, successMessage = 'Content copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
this.showToast(`📋 ${successMessage}`, 'success');
}).catch(() => {
this.showToast('Failed to copy content', 'error');
});
},
/**
* Download content as text file
*/
downloadAsFile(text, filename, successMessage = 'File downloaded!') {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `content-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
this.showToast(`💾 ${successMessage}`, 'success');
}
};
// For backward compatibility, create AgentUtils alias
const AgentUtils = WeatherUtils;
</script>
<style> <style>
/* Custom styles for weather reporter radio grid */ /* Custom styles for weather reporter radio grid */