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/agent-base.css' %}">
<!-- Shared JavaScript Utilities -->
<script src="{% static 'js/agent-utils.js' %}"></script>
<!-- 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>
<style>
* {
box-sizing: border-box;
}
/* Override main-container for full-width sections */
.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;
}
/* Data Analyzer specific styles only */
.file-upload-zone {
border: 2px dashed var(--agent-border);
border-radius: var(--radius);
padding: var(--space-xl);
text-align: center;
transition: all var(--transition);
background: var(--agent-card-bg);
cursor: pointer;
margin-bottom: var(--space-lg);
}
.file-upload-zone.dragover,
.file-upload-zone:hover {
border-color: var(--agent-primary);
background: var(--hover-color);
}
.file-preview {
background: var(--agent-card-bg);
border: 1px solid var(--agent-border);
border-radius: var(--radius);
padding: var(--space-md);
margin-top: var(--space-md);
display: flex;
align-items: center;
gap: var(--space-md);
}
.radio-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-md);
}
.radio-option {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-lg);
border: 1px solid var(--agent-border);
border-radius: var(--radius);
cursor: pointer;
background: white;
transition: all var(--transition);
min-height: 60px;
}
.radio-option.selected {
border-color: var(--agent-primary);
background: var(--agent-card-bg);
}
.radio-option:hover {
border-color: var(--agent-primary);
background: var(--hover-color);
}
.radio-option input[type="radio"] {
margin: 0;
}
.processing-status {
display: none;
text-align: center;
padding: var(--space-lg);
margin: var(--space-lg) 0;
background: var(--agent-card-bg);
border: 1px solid var(--agent-border);
border-radius: var(--radius);
}
.results-card {
display: none;
margin-top: var(--space-lg);
}
/* Ensure grid layout works */
div.agent-page div.agent-container {
display: grid !important;
grid-template-columns: 1fr 350px !important;
gap: 24px !important;
max-width: 1280px !important;
margin: 0 auto !important;
align-items: start !important;
}
@media (max-width: 768px) {
div.agent-page div.agent-container {
grid-template-columns: 1fr !important;
}
}
</style>
{% endblock %}
{% block content %}
<div class="agent-page theme-professional">
<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 -->
<div>
<form method="POST" id="dataAnalyzerForm">
{% csrf_token %}
<form method="POST" id="dataAnalyzerForm">
{% csrf_token %}
<!-- File Upload Section -->
<div class="card">
<h3 class="section-title">📁 Upload Your Data File</h3>
<!-- 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 class="file-upload-zone" id="fileUploadZone" onclick="document.getElementById('fileInput').click()">
<div style="font-size: 48px; margin-bottom: 12px;">📊</div>
<div style="font-size: var(--text-lg); font-weight: 600; color: var(--text-color); margin-bottom: 8px;">
Choose or drag your data file here
</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 style="font-size: var(--text-base); color: var(--accent-color);">
Supports PDF, CSV, Excel files (up to 10MB)
</div>
</label>
</div>
<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>
<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>
</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
<button type="button" onclick="removeFile()" class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;">
Remove
</button>
</div>
</div>
<div class="results-content" id="analysisContent">
<!-- Analysis data will be displayed here -->
</div>
<!-- Analysis Type Selection -->
<div class="card">
<h3 class="section-title">🔍 Analysis Type</h3>
<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>
<div class="radio-grid">
<label class="radio-option selected" data-value="summary">
<input type="radio" name="analysisType" value="summary" checked />
<div style="font-size: 20px;">📋</div>
<div>
<div style="font-weight: 600; margin-bottom: 4px; font-size: var(--text-lg);">
Summary Analysis
</div>
<div style="font-size: var(--text-sm); color: var(--accent-color);">
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="card">
<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>
<button
type="submit"
form="dataAnalyzerForm"
class="btn btn-primary process-btn"
id="processButton"
>
📊 Analyze Data (5.00 AED)
</button>
{% if user.is_authenticated %}
{% if user.wallet_balance >= 5.00 %}
<button type="submit" form="dataAnalyzerForm" class="btn btn-primary process-btn" id="processButton">
📊 Analyze Data (5.00 AED)
</button>
{% else %}
<div class="insufficient-balance">
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 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>
<li>Upload your data file (PDF, CSV, Excel)</li>
<li>Choose analysis type</li>
<li>Get comprehensive insights</li>
<li>Download detailed report</li>
</ul>
</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>
{% 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)
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):
- **N8N Integration**: PDF analysis webhook processor
- **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)
- **Unified CSS**: Uses agent-base.css with professional theme
- **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):
- **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
- **Real-time Results**: Dynamic display below form
- **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):
- **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
- **Real-time Results**: Dynamic content generation and display
- **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):
- **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
- **Enhanced UX**: Progressive form validation and real-time feedback
- **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
@ -537,7 +556,32 @@ All agents now use a unified CSS system for consistent user experience and maint
#### Core Files
- **`/static/css/agent-base.css`**: Unified component library for all agents
- **`/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
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-container">
<div>
<!-- Main content area -->
<!-- Main content area (first grid column) -->
<div class="card">
<h3 class="section-title">Agent Title</h3>
<!-- Agent form and content -->
</div>
<!-- Processing status and results stay within first grid column -->
</div>
<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>
```
#### 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
#### 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
- **Continuous Workflow**: Allow multiple requests without page refresh ("Get Another" functionality)
- **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
```javascript

View File

@ -1,146 +1,240 @@
{% extends "base.html" %}
{% csrf_token %}
{% load static %}
{% 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 %}
<div class="container" style="max-width: 1280px; margin: 0 auto; padding: clamp(20px, 5vw, 40px) clamp(16px, 4vw, 24px);">
<!-- Main Content Grid -->
<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;">
<!-- Chat Interface -->
<div class="agent-page theme-professional">
<div class="agent-container">
<!-- Main Content -->
<div>
<!-- Agent Header -->
<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);">
<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;">
🔍 {{ agent.name }}
</h1>
<p style="font-size: clamp(14px, 3.5vw, 16px); color: #6b7280; margin: 0;">
{{ agent.description }}
</p>
<div class="card">
<h3 class="section-title">🔍 {{ agent.name }}</h3>
<p class="form-help">{{ agent.description }}</p>
</div>
<!-- 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;">
<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 class="card">
<div id="chatContainer" class="chat-container">
<h4 class="section-subtitle">💬 Chat with 5 Whys Analyst</h4>
<!-- 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 style="font-weight: 600; color: #4338ca; margin-bottom: 4px;">5 Whys Analyst</div>
<div style="color: #374151; line-height: 1.5;">
Hello! I'm here to help you with root cause analysis using the 5 Whys methodology.
<!-- Welcome Message -->
<div class="welcome-message">
<div class="message-header">5 Whys Analyst</div>
<div class="message-content">
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>
<!-- Chat messages will be dynamically added here -->
<div id="chatMessages"></div>
</div>
<!-- Chat messages will be dynamically added here -->
<div id="chatMessages"></div>
<!-- Chat Input Form -->
<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>
<!-- Chat Input -->
<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 style="display: flex; gap: 12px; align-items: flex-end;">
<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 Section -->
<div class="card" id="reportSection">
<h4 class="section-subtitle">📋 Generate Final Report</h4>
<!-- Report Generation Form -->
<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;">
<div id="reportNotReady" class="info-message">
💬 Ask 2-3 questions about your problem first, then I'll generate a comprehensive report
</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
</div>
<button
id="generateReportBtn"
onclick="generateReport()"
class="btn btn-primary"
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)
</button>
</div>
<!-- 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 style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
<div style="font-size: 24px;"></div>
<h3 style="font-size: 20px; font-weight: 600; color: #1f2937; 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;">
✅ Complete
</div>
<div id="reportResults" 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;">5 Whys Analysis Report</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 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 -->
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<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;">
📋 Copy Report
</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 class="action-buttons">
<button onclick="copyReport()" class="btn btn-primary">📋 Copy Report</button>
<button onclick="downloadReport()" class="btn btn-secondary">💾 Download Report</button>
</div>
</div>
</div>
<!-- Wallet Sidebar -->
<div style="position: sticky; top: 20px;">
<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 style="font-size: clamp(16px, 4vw, 18px); font-weight: 600; color: #1f2937; margin: 0 0 16px 0;">💳 Your Wallet</h3>
<div class="wallet-section">
<div class="card">
<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>
{{ user.wallet_balance|floatformat:2 }} AED
</div>
<div style="font-size: clamp(14px, 3.5vw, 16px); color: #6b7280; margin-bottom: 20px;">
Available Balance
</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>
<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;">
💳 Top Up Wallet
</a>
{% if user.is_authenticated %}
<a href="{% url 'core:wallet' %}" class="btn btn-primary" style="text-decoration: none; margin-top: 16px;">
💰 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 style="padding: 16px; background: rgba(99, 102, 241, 0.1); border-radius: 12px; border: 1px solid rgba(99, 102, 241, 0.2);">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600; color: #4338ca;">💡 How it works</h4>
<ul style="margin: 0; font-size: 12px; color: #374151; line-height: 1.4; list-style: none; padding-left: 0;">
<li style="margin: 4px 0; padding-left: 16px; position: relative;">
<span style="position: absolute; left: 0; color: #6366f1;"></span>
Chat freely to explore your problem
</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>
<div class="usage-info">
<h4>💡 How it works</h4>
<ul>
<li>Chat freely to explore your problem</li>
<li>Get guidance and ask questions</li>
<li>Generate final report when ready</li>
<li>Pay only for the final report</li>
</ul>
</div>
</div>
@ -148,138 +242,186 @@
</div>
<style>
.container {
background: linear-gradient(135deg, #f6f8ff 0%, #e8f0fe 50%, #f0f7ff 100%);
min-height: 100vh;
color: #1f2937;
}
.chat-container {
background: var(--background-subtle);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
}
.card:hover {
transform: translateY(-1px);
transition: transform 0.2s ease;
}
.welcome-message {
margin-bottom: 16px;
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 {
transform: translateY(-1px);
}
.message-header {
font-weight: 600;
color: var(--primary-color);
margin-bottom: 8px;
}
button:disabled {
background: #9ca3af !important;
cursor: not-allowed !important;
transform: none !important;
}
.message-content {
color: var(--text-primary);
line-height: 1.6;
}
.message {
margin-bottom: 16px;
animation: fadeIn 0.3s ease;
}
.chat-form {
border-top: 1px solid var(--border-color);
padding-top: 16px;
margin-top: 16px;
}
.user-message {
margin-left: 20%;
padding: 12px 16px;
background: #6366f1;
color: white;
border-radius: 16px 16px 4px 16px;
}
.input-group {
display: flex;
gap: 12px;
align-items: stretch;
}
.assistant-message {
margin-right: 20%;
padding: 16px 20px;
background: #f8fafc;
border-radius: 16px 16px 16px 4px;
border-left: 4px solid #6366f1;
line-height: 1.6;
}
.input-group .form-input {
width: 80%;
height: 40px;
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px;
font-family: inherit;
font-size: 14px;
background: var(--background-primary);
color: var(--text-primary);
box-sizing: border-box;
}
.assistant-message .message-content {
color: #374151;
line-height: 1.6;
}
.input-group .form-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.assistant-message .message-content h3 {
color: #1f2937;
font-size: 16px;
font-weight: 600;
margin: 16px 0 8px 0;
}
.input-group .btn {
height: 40px;
padding: 8px 12px;
width: 20%;
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 {
margin-top: 0;
}
.info-message {
padding: 16px;
background: var(--background-subtle);
border-radius: 8px;
text-align: center;
color: var(--text-secondary);
margin-bottom: 16px;
}
.assistant-message .message-content ul {
margin: 8px 0;
padding-left: 20px;
}
.success-message {
padding: 16px;
background: #ecfdf5;
border-radius: 8px;
text-align: center;
color: #059669;
margin-bottom: 16px;
}
.assistant-message .message-content li {
margin: 4px 0;
}
.message {
margin-bottom: 16px;
animation: fadeIn 0.3s ease;
}
.assistant-message .message-content p {
margin: 8px 0;
}
.user-message {
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 {
margin-top: 0;
}
.assistant-message {
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 {
margin-bottom: 0;
}
.assistant-message .message-content {
color: var(--text-primary);
line-height: 1.6;
}
.assistant-message .message-content strong {
color: #1f2937;
font-weight: 600;
}
.assistant-message .message-content h3 {
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
margin: 16px 0 8px 0;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.assistant-message .message-content h3:first-child {
margin-top: 0;
}
.typing-dots {
display: flex;
gap: 4px;
align-items: center;
}
.assistant-message .message-content ul {
margin: 8px 0;
padding-left: 20px;
}
.typing-dots span {
width: 6px;
height: 6px;
border-radius: 50%;
background: #6366f1;
animation: typingDots 1.4s infinite ease-in-out;
}
.assistant-message .message-content li {
margin: 4px 0;
}
.typing-dots span:nth-child(1) {
animation-delay: 0s;
}
.assistant-message .message-content p {
margin: 8px 0;
}
.typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.assistant-message .message-content p:first-child {
margin-top: 0;
}
.typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
.assistant-message .message-content p:last-child {
margin-bottom: 0;
}
@keyframes typingDots {
0%, 80%, 100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.assistant-message .message-content strong {
color: var(--text-primary);
font-weight: 600;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.main-grid {
grid-template-columns: 1fr !important;
}
}
.typing-dots {
display: flex;
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>
<script>
@ -292,8 +434,14 @@
// Generate new session ID
currentSessionId = generateSessionId();
// Add Enter key support for chat input
document.getElementById('chatInput').addEventListener('keypress', function(e) {
// Handle chat form submission
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) {
e.preventDefault();
sendChatMessage();
@ -507,8 +655,6 @@
const btn = document.getElementById('generateReportBtn');
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
displayReport(data.report);
// Update wallet balance
AgentUtils.updateWalletBalance(data.wallet_balance);
// Update wallet balance if provided
if (data.wallet_balance !== undefined) {
AgentUtils.updateWalletBalance(data.wallet_balance);
}
AgentUtils.showToast('✅ Report generated and payment processed!', 'success');
} else {

View File

@ -13,8 +13,104 @@
<link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Shared JavaScript Utilities -->
<script src="{% static 'js/agent-utils.js' %}"></script>
<!-- Job Posting Generator Specific Utilities -->
<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 %}
{% block content %}

View File

@ -13,8 +13,129 @@
<link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Shared JavaScript Utilities -->
<script src="{% static 'js/agent-utils.js' %}"></script>
<!-- Social Ads Generator Specific Utilities -->
<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 %}
{% block content %}

View File

@ -80,11 +80,23 @@ window.AgentUtils = {
* Update wallet balance display across all agents
*/
updateWalletBalance(newBalance) {
const balanceElement = document.querySelector('[data-wallet-balance]') ||
document.getElementById('walletBalance');
if (balanceElement) {
balanceElement.textContent = `${newBalance.toFixed(2)} AED`;
// Update all wallet balance elements
document.querySelectorAll('[data-wallet-balance]').forEach(element => {
if (element.tagName === 'A') {
// 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;
},

View File

@ -13,8 +13,104 @@
<link rel="stylesheet" href="{% static 'css/themes.css' %}">
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<!-- Shared JavaScript Utilities -->
<script src="{% static 'js/agent-utils.js' %}"></script>
<!-- Weather Reporter Specific Utilities -->
<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>
/* Custom styles for weather reporter radio grid */