mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 06:52:57 +00:00
🧹 Major codebase cleanup - remove obsolete files and reduce project bloat
Removed obsolete files across multiple categories: - AGENTS_APP_RECREATION_GUIDE.md (640 lines) - obsolete recreation guide - 4 HTML prototype files (agent_template_prototype.html, social_ads*.html, workflow_template.html) - Log files and database dumps (netcop.log, server.log, local_database_dump.json, docs_update_summary.txt) - 6 obsolete test files for old agent systems and webhooks - 4 unused static JS files (data-analyzer.js, workflows*.js, text-summarizer.js) - 2 unused CSS files (digital-branding.css, workflows.css) This cleanup reduces codebase size significantly while maintaining all 8 active agents and core functionality. File-based agent system remains fully operational with streamlined, relevant files only. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e4f3e7993a
commit
2f17475445
@ -1,639 +0,0 @@
|
||||
# Django Agents App Recreation Guide
|
||||
|
||||
This guide provides complete instructions for recreating the agents app in another Django project.
|
||||
|
||||
## Overview
|
||||
|
||||
Create a Django app called `agents` with the following functionality:
|
||||
- Agent marketplace with categories
|
||||
- Agent execution system with n8n webhook integration
|
||||
- User balance checking and fee deduction
|
||||
- Complete REST API with pagination
|
||||
- Admin interface for management
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Create the App
|
||||
|
||||
```bash
|
||||
python manage.py startapp agents
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install requests djangorestframework
|
||||
```
|
||||
|
||||
### 3. Add to INSTALLED_APPS
|
||||
|
||||
In your `settings.py`:
|
||||
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
# ... other apps
|
||||
'rest_framework',
|
||||
'agents',
|
||||
]
|
||||
```
|
||||
|
||||
### 4. Add to URLs
|
||||
|
||||
In your main `urls.py`:
|
||||
|
||||
```python
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
# ... other URLs
|
||||
path('api/agents/', include('agents.urls')),
|
||||
]
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
agents/
|
||||
├── __init__.py
|
||||
├── admin.py
|
||||
├── apps.py
|
||||
├── models.py
|
||||
├── serializers.py
|
||||
├── views.py
|
||||
├── urls.py
|
||||
├── migrations/
|
||||
│ └── __init__.py
|
||||
└── management/
|
||||
└── commands/
|
||||
└── create_sample_agents.py
|
||||
```
|
||||
|
||||
## Code Files
|
||||
|
||||
### agents/models.py
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
import uuid
|
||||
|
||||
class AgentCategory(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
icon = models.CharField(max_length=50, blank=True, help_text="Icon class or emoji")
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Agent(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(unique=True)
|
||||
short_description = models.CharField(max_length=300)
|
||||
description = models.TextField()
|
||||
category = models.ForeignKey(AgentCategory, on_delete=models.CASCADE, related_name='agents')
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
form_schema = models.JSONField(help_text="JSON schema for agent input form")
|
||||
webhook_url = models.URLField(help_text="n8n webhook URL for execution")
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class AgentExecution(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending'),
|
||||
('running', 'Running'),
|
||||
('completed', 'Completed'),
|
||||
('failed', 'Failed'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
agent = models.ForeignKey(Agent, on_delete=models.CASCADE, related_name='executions')
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE) # Adjust to your user model
|
||||
input_data = models.JSONField()
|
||||
output_data = models.JSONField(null=True, blank=True)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||
fee_charged = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
webhook_response = models.JSONField(null=True, blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
execution_time = models.DurationField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.agent.name} - {self.user.email} - {self.status}"
|
||||
```
|
||||
|
||||
### agents/admin.py
|
||||
|
||||
```python
|
||||
from django.contrib import admin
|
||||
from .models import AgentCategory, Agent, AgentExecution
|
||||
|
||||
@admin.register(AgentCategory)
|
||||
class AgentCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'is_active', 'created_at']
|
||||
list_filter = ['is_active', 'created_at']
|
||||
search_fields = ['name', 'description']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
|
||||
@admin.register(Agent)
|
||||
class AgentAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'category', 'price', 'is_active', 'created_at']
|
||||
list_filter = ['category', 'is_active', 'created_at']
|
||||
search_fields = ['name', 'description', 'short_description']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
@admin.register(AgentExecution)
|
||||
class AgentExecutionAdmin(admin.ModelAdmin):
|
||||
list_display = ['agent', 'user', 'status', 'fee_charged', 'created_at']
|
||||
list_filter = ['status', 'created_at', 'agent__category']
|
||||
search_fields = ['agent__name', 'user__email']
|
||||
readonly_fields = ['created_at', 'completed_at']
|
||||
```
|
||||
|
||||
### agents/serializers.py
|
||||
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from .models import Agent, AgentCategory, AgentExecution
|
||||
|
||||
class AgentCategorySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AgentCategory
|
||||
fields = ['id', 'name', 'slug', 'description', 'icon']
|
||||
|
||||
class AgentSerializer(serializers.ModelSerializer):
|
||||
category = AgentCategorySerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'short_description', 'description',
|
||||
'category', 'price', 'form_schema', 'created_at'
|
||||
]
|
||||
|
||||
class AgentExecutionSerializer(serializers.ModelSerializer):
|
||||
agent = AgentSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AgentExecution
|
||||
fields = [
|
||||
'id', 'agent', 'input_data', 'output_data', 'status',
|
||||
'fee_charged', 'error_message', 'execution_time',
|
||||
'created_at', 'completed_at'
|
||||
]
|
||||
```
|
||||
|
||||
### agents/views.py
|
||||
|
||||
```python
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from .models import Agent, AgentExecution
|
||||
from .serializers import AgentSerializer, AgentExecutionSerializer
|
||||
import requests
|
||||
import json
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def agent_list(request):
|
||||
"""List all active agents with optional category filtering"""
|
||||
agents = Agent.objects.filter(is_active=True)
|
||||
|
||||
category = request.GET.get('category')
|
||||
if category:
|
||||
agents = agents.filter(category__slug=category)
|
||||
|
||||
search = request.GET.get('search')
|
||||
if search:
|
||||
agents = agents.filter(name__icontains=search)
|
||||
|
||||
paginator = PageNumberPagination()
|
||||
paginator.page_size = 20
|
||||
result_page = paginator.paginate_queryset(agents, request)
|
||||
serializer = AgentSerializer(result_page, many=True)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def agent_detail(request, slug):
|
||||
"""Get detailed agent information"""
|
||||
agent = get_object_or_404(Agent, slug=slug, is_active=True)
|
||||
serializer = AgentSerializer(agent)
|
||||
return Response(serializer.data)
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def execute_agent(request):
|
||||
"""Execute an agent with provided input data"""
|
||||
agent_slug = request.data.get('agent_slug')
|
||||
input_data = request.data.get('input_data', {})
|
||||
|
||||
if not agent_slug:
|
||||
return Response({'error': 'agent_slug is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
agent = get_object_or_404(Agent, slug=agent_slug, is_active=True)
|
||||
|
||||
# Check if user has sufficient balance (adjust based on your wallet system)
|
||||
if hasattr(request.user, 'wallet_balance') and request.user.wallet_balance < agent.price:
|
||||
return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Create execution record
|
||||
execution = AgentExecution.objects.create(
|
||||
agent=agent,
|
||||
user=request.user,
|
||||
input_data=input_data,
|
||||
fee_charged=agent.price,
|
||||
status='pending'
|
||||
)
|
||||
|
||||
try:
|
||||
# Deduct fee from user wallet (adjust based on your wallet system)
|
||||
if hasattr(request.user, 'deduct_balance'):
|
||||
request.user.deduct_balance(agent.price)
|
||||
|
||||
# Call n8n webhook
|
||||
execution.status = 'running'
|
||||
execution.save()
|
||||
|
||||
webhook_payload = {
|
||||
'execution_id': str(execution.id),
|
||||
'agent_slug': agent.slug,
|
||||
'user_id': str(request.user.id),
|
||||
'input_data': input_data
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
agent.webhook_url,
|
||||
json=webhook_payload,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
execution.webhook_response = response.json() if response.headers.get('content-type', '').startswith('application/json') else {'raw': response.text}
|
||||
|
||||
if response.status_code == 200:
|
||||
execution.status = 'completed'
|
||||
execution.output_data = execution.webhook_response
|
||||
else:
|
||||
execution.status = 'failed'
|
||||
execution.error_message = f"Webhook returned {response.status_code}"
|
||||
|
||||
execution.completed_at = timezone.now()
|
||||
execution.save()
|
||||
|
||||
serializer = AgentExecutionSerializer(execution)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
except requests.RequestException as e:
|
||||
execution.status = 'failed'
|
||||
execution.error_message = str(e)
|
||||
execution.completed_at = timezone.now()
|
||||
execution.save()
|
||||
|
||||
return Response({
|
||||
'error': 'Failed to execute agent',
|
||||
'execution_id': str(execution.id)
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def execution_list(request):
|
||||
"""List user's agent executions"""
|
||||
executions = AgentExecution.objects.filter(user=request.user)
|
||||
|
||||
paginator = PageNumberPagination()
|
||||
paginator.page_size = 20
|
||||
result_page = paginator.paginate_queryset(executions, request)
|
||||
serializer = AgentExecutionSerializer(result_page, many=True)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def execution_detail(request, execution_id):
|
||||
"""Get detailed execution information"""
|
||||
execution = get_object_or_404(AgentExecution, id=execution_id, user=request.user)
|
||||
serializer = AgentExecutionSerializer(execution)
|
||||
return Response(serializer.data)
|
||||
```
|
||||
|
||||
### agents/urls.py
|
||||
|
||||
```python
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.agent_list, name='agent_list'),
|
||||
path('<slug:slug>/', views.agent_detail, name='agent_detail'),
|
||||
path('execute/', views.execute_agent, name='execute_agent'),
|
||||
path('executions/', views.execution_list, name='execution_list'),
|
||||
path('executions/<uuid:execution_id>/', views.execution_detail, name='execution_detail'),
|
||||
]
|
||||
```
|
||||
|
||||
### agents/management/commands/create_sample_agents.py
|
||||
|
||||
First create the directories:
|
||||
|
||||
```bash
|
||||
mkdir -p agents/management/commands
|
||||
touch agents/management/__init__.py
|
||||
touch agents/management/commands/__init__.py
|
||||
```
|
||||
|
||||
Then create the file:
|
||||
|
||||
```python
|
||||
from django.core.management.base import BaseCommand
|
||||
from agents.models import AgentCategory, Agent
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Create sample agents for testing'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Create categories
|
||||
ai_category, _ = AgentCategory.objects.get_or_create(
|
||||
slug='ai-tools',
|
||||
defaults={
|
||||
'name': 'AI Tools',
|
||||
'description': 'AI-powered automation tools',
|
||||
'icon': '🤖'
|
||||
}
|
||||
)
|
||||
|
||||
data_category, _ = AgentCategory.objects.get_or_create(
|
||||
slug='data-analysis',
|
||||
defaults={
|
||||
'name': 'Data Analysis',
|
||||
'description': 'Data processing and analysis tools',
|
||||
'icon': '📊'
|
||||
}
|
||||
)
|
||||
|
||||
web_category, _ = AgentCategory.objects.get_or_create(
|
||||
slug='web-scraping',
|
||||
defaults={
|
||||
'name': 'Web Scraping',
|
||||
'description': 'Web data extraction tools',
|
||||
'icon': '🕷️'
|
||||
}
|
||||
)
|
||||
|
||||
# Create sample agents
|
||||
Agent.objects.get_or_create(
|
||||
slug='pdf-analyzer',
|
||||
defaults={
|
||||
'name': 'PDF Content Analyzer',
|
||||
'short_description': 'Extract and analyze content from PDF documents',
|
||||
'description': 'This agent processes PDF files and extracts meaningful insights including summaries, keywords, and sentiment analysis. Perfect for document processing workflows.',
|
||||
'category': ai_category,
|
||||
'price': 5.00,
|
||||
'form_schema': {
|
||||
'fields': [
|
||||
{
|
||||
'name': 'pdf_url',
|
||||
'type': 'url',
|
||||
'label': 'PDF URL',
|
||||
'placeholder': 'https://example.com/document.pdf',
|
||||
'required': True
|
||||
},
|
||||
{
|
||||
'name': 'analysis_type',
|
||||
'type': 'select',
|
||||
'label': 'Analysis Type',
|
||||
'options': [
|
||||
{'value': 'summary', 'label': 'Summary'},
|
||||
{'value': 'keywords', 'label': 'Keywords'},
|
||||
{'value': 'sentiment', 'label': 'Sentiment Analysis'}
|
||||
],
|
||||
'required': True
|
||||
}
|
||||
]
|
||||
},
|
||||
'webhook_url': 'https://your-n8n-instance.com/webhook/pdf-analyzer'
|
||||
}
|
||||
)
|
||||
|
||||
Agent.objects.get_or_create(
|
||||
slug='website-scraper',
|
||||
defaults={
|
||||
'name': 'Website Data Scraper',
|
||||
'short_description': 'Extract structured data from any website',
|
||||
'description': 'Advanced web scraping agent that can extract specific data from websites using CSS selectors or XPath. Handles JavaScript-rendered content and returns clean, structured data.',
|
||||
'category': web_category,
|
||||
'price': 3.00,
|
||||
'form_schema': {
|
||||
'fields': [
|
||||
{
|
||||
'name': 'website_url',
|
||||
'type': 'url',
|
||||
'label': 'Website URL',
|
||||
'placeholder': 'https://example.com',
|
||||
'required': True
|
||||
},
|
||||
{
|
||||
'name': 'selectors',
|
||||
'type': 'textarea',
|
||||
'label': 'CSS Selectors (one per line)',
|
||||
'placeholder': 'h1.title\n.price\n.description',
|
||||
'required': True
|
||||
},
|
||||
{
|
||||
'name': 'wait_for_js',
|
||||
'type': 'checkbox',
|
||||
'label': 'Wait for JavaScript to load',
|
||||
'required': False
|
||||
}
|
||||
]
|
||||
},
|
||||
'webhook_url': 'https://your-n8n-instance.com/webhook/website-scraper'
|
||||
}
|
||||
)
|
||||
|
||||
Agent.objects.get_or_create(
|
||||
slug='data-analyzer',
|
||||
defaults={
|
||||
'name': 'CSV Data Analyzer',
|
||||
'short_description': 'Analyze and visualize CSV data with insights',
|
||||
'description': 'Upload CSV files and get comprehensive data analysis including statistics, trends, and visualizations. Perfect for business intelligence and data exploration.',
|
||||
'category': data_category,
|
||||
'price': 4.50,
|
||||
'form_schema': {
|
||||
'fields': [
|
||||
{
|
||||
'name': 'csv_url',
|
||||
'type': 'url',
|
||||
'label': 'CSV File URL',
|
||||
'placeholder': 'https://example.com/data.csv',
|
||||
'required': True
|
||||
},
|
||||
{
|
||||
'name': 'analysis_columns',
|
||||
'type': 'text',
|
||||
'label': 'Columns to Analyze (comma-separated)',
|
||||
'placeholder': 'sales,revenue,date',
|
||||
'required': False
|
||||
},
|
||||
{
|
||||
'name': 'chart_type',
|
||||
'type': 'select',
|
||||
'label': 'Chart Type',
|
||||
'options': [
|
||||
{'value': 'line', 'label': 'Line Chart'},
|
||||
{'value': 'bar', 'label': 'Bar Chart'},
|
||||
{'value': 'pie', 'label': 'Pie Chart'},
|
||||
{'value': 'scatter', 'label': 'Scatter Plot'}
|
||||
],
|
||||
'required': False
|
||||
}
|
||||
]
|
||||
},
|
||||
'webhook_url': 'https://your-n8n-instance.com/webhook/data-analyzer'
|
||||
}
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Sample agents created successfully'))
|
||||
self.stdout.write(f'Created categories: {AgentCategory.objects.count()}')
|
||||
self.stdout.write(f'Created agents: {Agent.objects.count()}')
|
||||
```
|
||||
|
||||
### agents/apps.py
|
||||
|
||||
```python
|
||||
from django.apps import AppConfig
|
||||
|
||||
class AgentsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'agents'
|
||||
verbose_name = 'Agents'
|
||||
```
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Run Migrations
|
||||
|
||||
```bash
|
||||
python manage.py makemigrations agents
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### 2. Create Sample Data
|
||||
|
||||
```bash
|
||||
python manage.py create_sample_agents
|
||||
```
|
||||
|
||||
### 3. Create Superuser (if needed)
|
||||
|
||||
```bash
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### 4. Test the API
|
||||
|
||||
Start the server and test these endpoints:
|
||||
|
||||
- `GET /api/agents/` - List all agents
|
||||
- `GET /api/agents/pdf-analyzer/` - Agent details
|
||||
- `POST /api/agents/execute/` - Execute an agent
|
||||
- `GET /api/agents/executions/` - List executions
|
||||
|
||||
## API Usage Examples
|
||||
|
||||
### List Agents
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Token YOUR_TOKEN" http://localhost:8000/api/agents/
|
||||
```
|
||||
|
||||
### Execute Agent
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Token YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"agent_slug": "pdf-analyzer",
|
||||
"input_data": {
|
||||
"pdf_url": "https://example.com/document.pdf",
|
||||
"analysis_type": "summary"
|
||||
}
|
||||
}' \
|
||||
http://localhost:8000/api/agents/execute/
|
||||
```
|
||||
|
||||
## Customization Notes
|
||||
|
||||
### User Model Integration
|
||||
|
||||
Update the `AgentExecution` model to reference your custom user model:
|
||||
|
||||
```python
|
||||
# If your user model is in a different app
|
||||
user = models.ForeignKey('accounts.CustomUser', on_delete=models.CASCADE)
|
||||
```
|
||||
|
||||
### Wallet Integration
|
||||
|
||||
The code assumes your user model has these methods:
|
||||
- `wallet_balance` property
|
||||
- `deduct_balance(amount)` method
|
||||
|
||||
Adjust the wallet checking logic in `execute_agent` view as needed.
|
||||
|
||||
### n8n Webhook Format
|
||||
|
||||
The webhook payload sent to n8n includes:
|
||||
- `execution_id`: UUID of the execution
|
||||
- `agent_slug`: Identifier for the agent
|
||||
- `user_id`: User who triggered the execution
|
||||
- `input_data`: Form data submitted by user
|
||||
|
||||
## Features Included
|
||||
|
||||
✅ **Agent Categories** - Organize agents by type
|
||||
✅ **Agent Management** - Full CRUD via Django admin
|
||||
✅ **Execution System** - Track agent runs with status
|
||||
✅ **Webhook Integration** - Connect to n8n workflows
|
||||
✅ **User Balance Checking** - Wallet integration ready
|
||||
✅ **REST API** - Complete API endpoints
|
||||
✅ **Pagination** - Built-in pagination for lists
|
||||
✅ **Error Handling** - Comprehensive error management
|
||||
✅ **Sample Data** - Management command for test data
|
||||
✅ **Form Schema** - Dynamic form generation support
|
||||
✅ **Admin Interface** - Django admin integration
|
||||
✅ **UUID Primary Keys** - Better security and uniqueness
|
||||
|
||||
## Production Considerations
|
||||
|
||||
1. **Environment Variables**: Store webhook URLs and API keys in environment variables
|
||||
2. **Rate Limiting**: Add rate limiting to prevent abuse
|
||||
3. **Caching**: Cache agent lists and categories for better performance
|
||||
4. **Background Tasks**: Use Celery for long-running agent executions
|
||||
5. **Logging**: Add comprehensive logging for debugging
|
||||
6. **Monitoring**: Monitor webhook success rates and execution times
|
||||
7. **Security**: Validate webhook responses and sanitize input data
|
||||
|
||||
This guide provides a complete, production-ready agents marketplace that can be easily integrated into any Django project.
|
||||
@ -1,969 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Agent Frontend Template Prototype</title>
|
||||
<style>
|
||||
/* Agent Frontend Template - Common Components CSS */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #000000;
|
||||
--surface: #ffffff;
|
||||
--surface-variant: #f8fafc;
|
||||
--background: #f3f4f6;
|
||||
--outline: #e4e7eb;
|
||||
--outline-variant: #e1e4e7;
|
||||
--on-surface: #1a1a1a;
|
||||
--on-surface-variant: #6b7280;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--on-surface);
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.agent-container {
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-lg);
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
.agent-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.agent-grid {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.agent-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--on-surface);
|
||||
margin: 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.agent-subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--on-surface-variant);
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
/* Widgets */
|
||||
.agent-widget {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
border: 1px solid var(--outline-variant);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--outline-variant);
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.widget-icon {
|
||||
font-size: 20px;
|
||||
padding: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-variant);
|
||||
}
|
||||
|
||||
.widget-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Widget Sizes */
|
||||
.widget-large {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.widget-small {
|
||||
flex: 0 0 280px;
|
||||
}
|
||||
|
||||
.widget-wide {
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Wallet Card */
|
||||
.wallet-card {
|
||||
background: linear-gradient(135deg, #000000 0%, #333333 100%);
|
||||
color: white;
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
border: none;
|
||||
box-shadow: var(--shadow-md);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.wallet-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
transform: translate(30px, -30px);
|
||||
}
|
||||
|
||||
.wallet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.wallet-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.wallet-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.balance-display {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wallet-topup-btn {
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.wallet-topup-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Processing Status */
|
||||
.processing-status {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--outline-variant);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.processing-status.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--spacing-md);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Quick Agent Access Panel */
|
||||
.quick-agent-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--on-surface);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-agent-toggle:hover {
|
||||
background: var(--surface-variant);
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.quick-agent-toggle.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.quick-agent-toggle.active .toggle-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.quick-agents-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: min(400px, 90vw);
|
||||
height: 100vh;
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--outline);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 1000;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.quick-agents-panel.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.quick-agents-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--outline-variant);
|
||||
background: var(--surface-variant);
|
||||
}
|
||||
|
||||
.quick-agents-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.close-panel {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: var(--on-surface-variant);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-panel:hover {
|
||||
background: var(--surface);
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.quick-agents-grid {
|
||||
padding: var(--spacing-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.quick-agent-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid var(--outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none;
|
||||
color: var(--on-surface);
|
||||
transition: all 0.2s ease;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.quick-agent-card:hover {
|
||||
background: var(--surface-variant);
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.agent-icon {
|
||||
font-size: 24px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--surface-variant);
|
||||
border-radius: var(--radius-md);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.agent-info h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.agent-info p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--on-surface-variant);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.agent-price {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.quick-agents-footer {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-top: 1px solid var(--outline-variant);
|
||||
background: var(--surface-variant);
|
||||
}
|
||||
|
||||
.view-all-agents {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.view-all-agents:hover {
|
||||
background: #333333;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Overlay for mobile */
|
||||
.quick-agents-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-agents-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Placeholder Sections */
|
||||
.placeholder-section {
|
||||
background: var(--surface);
|
||||
border: 2px dashed var(--outline);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
text-align: center;
|
||||
color: var(--on-surface-variant);
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.placeholder-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.placeholder-example {
|
||||
background: var(--surface-variant);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-sm);
|
||||
margin-top: var(--spacing-md);
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
/* Info List - For How It Works */
|
||||
.info-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
counter-reset: step-counter;
|
||||
}
|
||||
|
||||
.info-list li {
|
||||
padding: var(--spacing-sm) 0;
|
||||
color: var(--on-surface-variant);
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--outline-variant);
|
||||
position: relative;
|
||||
padding-left: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.info-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-list li::before {
|
||||
content: counter(step-counter);
|
||||
counter-increment: step-counter;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: var(--spacing-sm);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--outline);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-color: var(--success);
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-color: var(--error);
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.agent-container {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.agent-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.agent-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.agent-grid {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.widget-large,
|
||||
.widget-small {
|
||||
min-width: auto;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.quick-agents-panel {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="agent-container">
|
||||
<!-- Agent Header Component -->
|
||||
<div class="agent-header">
|
||||
<div>
|
||||
<h1 class="agent-title">Sample Agent Title</h1>
|
||||
<p class="agent-subtitle">This is the agent subtitle describing what this agent does</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<!-- Wallet Card Component -->
|
||||
<div class="wallet-card widget-small">
|
||||
<div class="wallet-header">
|
||||
<h3 class="wallet-title">Your Wallet</h3>
|
||||
<div class="wallet-icon">💳</div>
|
||||
</div>
|
||||
<div class="balance-display">
|
||||
<div class="balance-amount">
|
||||
<span id="walletBalance">25.50</span> AED
|
||||
</div>
|
||||
<div class="balance-label">Available Balance</div>
|
||||
</div>
|
||||
<button type="button" class="wallet-topup-btn" onclick="showToast('Top-up feature demo!', 'success')">
|
||||
💳 Top Up Wallet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Agent Access Panel Overlay -->
|
||||
<div class="quick-agents-overlay" id="quickAgentsOverlay" onclick="closeQuickAgents()"></div>
|
||||
|
||||
<!-- Quick Agent Access Panel -->
|
||||
<div class="quick-agents-panel" id="quickAgentsPanel">
|
||||
<div class="quick-agents-header">
|
||||
<h3>Quick Access</h3>
|
||||
<button class="close-panel" onclick="closeQuickAgents()" aria-label="Close panel">×</button>
|
||||
</div>
|
||||
<div class="quick-agents-grid">
|
||||
<!-- Sample Agent Cards -->
|
||||
<a href="#" class="quick-agent-card">
|
||||
<div class="agent-icon">🌤️</div>
|
||||
<div class="agent-info">
|
||||
<h4>Weather Reporter</h4>
|
||||
<p>Get real-time weather data</p>
|
||||
<span class="agent-price">2.00 AED</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" class="quick-agent-card">
|
||||
<div class="agent-icon">📊</div>
|
||||
<div class="agent-info">
|
||||
<h4>Data Analyzer</h4>
|
||||
<p>AI-powered data analysis</p>
|
||||
<span class="agent-price">5.00 AED</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" class="quick-agent-card">
|
||||
<div class="agent-icon">💼</div>
|
||||
<div class="agent-info">
|
||||
<h4>Job Posting Generator</h4>
|
||||
<p>Create professional job postings</p>
|
||||
<span class="agent-price">4.00 AED</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="quick-agents-footer">
|
||||
<a href="#" class="view-all-agents">View All Agents</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Grid - PLACEHOLDER FOR AGENT-SPECIFIC CONTENT -->
|
||||
<div class="agent-grid">
|
||||
<div class="placeholder-section widget-large" style="flex: 1; margin-right: var(--spacing-lg);">
|
||||
<div class="placeholder-title">🎯 Agent Grid Section</div>
|
||||
<div class="placeholder-description">
|
||||
This section is where each agent will place their unique form inputs, configuration options, and interaction elements.
|
||||
</div>
|
||||
<div class="placeholder-example">
|
||||
Examples: File upload forms, text inputs, dropdowns, radio buttons, etc.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- How It Works Widget - Common Pattern -->
|
||||
<div class="agent-widget widget-small" style="min-width: min(280px, 100%); max-width: min(280px, 100%); margin-left: auto;">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">
|
||||
<span class="widget-icon">ℹ️</span>
|
||||
How It Works
|
||||
</h3>
|
||||
</div>
|
||||
<div class="widget-content">
|
||||
<ol class="info-list">
|
||||
<li>Enter your requirements</li>
|
||||
<li>Configure options</li>
|
||||
<li>Process with AI</li>
|
||||
<li>Get results instantly</li>
|
||||
</ol>
|
||||
|
||||
<!-- Quick Agents Toggle Button -->
|
||||
<button class="quick-agent-toggle" onclick="toggleQuickAgents()"
|
||||
title="Quick access to other agents"
|
||||
aria-label="Open quick access panel for other AI agents"
|
||||
aria-expanded="false"
|
||||
aria-controls="quickAgentsPanel"
|
||||
style="margin-top: var(--spacing-md);">
|
||||
<span class="toggle-icon" aria-hidden="true">🚀</span>
|
||||
<span class="toggle-text">Explore Other Agents</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Status Component -->
|
||||
<div class="agent-grid">
|
||||
<div id="processingStatus" class="agent-widget widget-wide processing-status">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">
|
||||
<span class="widget-icon">⏳</span>
|
||||
Processing Status
|
||||
</h3>
|
||||
</div>
|
||||
<div class="widget-content" style="text-align: center;">
|
||||
<div class="status-icon">⏳</div>
|
||||
<div class="status-title">Processing your request...</div>
|
||||
<div class="status-text" id="statusText">Please wait while we analyze your data...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section - PLACEHOLDER FOR AGENT-SPECIFIC RESULTS -->
|
||||
<div class="agent-grid">
|
||||
<div class="placeholder-section widget-wide">
|
||||
<div class="placeholder-title">📊 Results Section</div>
|
||||
<div class="placeholder-description">
|
||||
This section is where each agent will display their specific results format, content structure, and action buttons.
|
||||
</div>
|
||||
<div class="placeholder-example">
|
||||
Examples: Formatted reports, analysis results, generated content, download buttons, etc.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Demo Controls -->
|
||||
<div class="agent-grid" style="margin-top: var(--spacing-xl); border-top: 1px solid var(--outline-variant); padding-top: var(--spacing-lg);">
|
||||
<div class="agent-widget widget-wide">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">
|
||||
<span class="widget-icon">🧪</span>
|
||||
Demo Controls
|
||||
</h3>
|
||||
</div>
|
||||
<div class="widget-content">
|
||||
<p style="margin-bottom: var(--spacing-md); color: var(--on-surface-variant);">
|
||||
Test the common components and interactions:
|
||||
</p>
|
||||
<div style="display: flex; gap: var(--spacing-md); flex-wrap: wrap;">
|
||||
<button onclick="showProcessing()" style="padding: var(--spacing-sm) var(--spacing-md); background: var(--primary); color: white; border: none; border-radius: var(--radius-sm); cursor: pointer;">
|
||||
Show Processing
|
||||
</button>
|
||||
<button onclick="hideProcessing()" style="padding: var(--spacing-sm) var(--spacing-md); background: var(--surface); color: var(--on-surface); border: 1px solid var(--outline); border-radius: var(--radius-sm); cursor: pointer;">
|
||||
Hide Processing
|
||||
</button>
|
||||
<button onclick="showToast('Success message!', 'success')" style="padding: var(--spacing-sm) var(--spacing-md); background: var(--success); color: white; border: none; border-radius: var(--radius-sm); cursor: pointer;">
|
||||
Success Toast
|
||||
</button>
|
||||
<button onclick="showToast('Error message!', 'error')" style="padding: var(--spacing-sm) var(--spacing-md); background: var(--error); color: white; border: none; border-radius: var(--radius-sm); cursor: pointer;">
|
||||
Error Toast
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Agent Frontend Template - Common JavaScript Utilities
|
||||
|
||||
// Quick Agent Access Panel Functions
|
||||
function toggleQuickAgents() {
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
const toggle = document.querySelector('.quick-agent-toggle');
|
||||
|
||||
const isActive = panel.classList.contains('active');
|
||||
|
||||
if (isActive) {
|
||||
// Close panel
|
||||
panel.classList.remove('active');
|
||||
overlay.classList.remove('active');
|
||||
toggle.classList.remove('active');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
overlay.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = 'auto';
|
||||
} else {
|
||||
// Open panel
|
||||
panel.classList.add('active');
|
||||
overlay.classList.add('active');
|
||||
toggle.classList.add('active');
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
panel.setAttribute('aria-hidden', 'false');
|
||||
overlay.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function closeQuickAgents() {
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
const toggle = document.querySelector('.quick-agent-toggle');
|
||||
|
||||
if (panel && overlay && toggle) {
|
||||
panel.classList.remove('active');
|
||||
overlay.classList.remove('active');
|
||||
toggle.classList.remove('active');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
overlay.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
// Toast Notification Function
|
||||
function showToast(message, type = 'info') {
|
||||
// Remove existing toasts
|
||||
document.querySelectorAll('.toast').forEach(toast => toast.remove());
|
||||
|
||||
// Create new toast
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Show toast with animation
|
||||
setTimeout(() => toast.classList.add('show'), 100);
|
||||
|
||||
// Auto remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Processing Status Functions
|
||||
function showProcessing() {
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
processingStatus.style.display = 'block';
|
||||
processingStatus.classList.add('active');
|
||||
showToast('Processing started...', 'success');
|
||||
}
|
||||
|
||||
function hideProcessing() {
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
processingStatus.style.display = 'none';
|
||||
processingStatus.classList.remove('active');
|
||||
showToast('Processing stopped...', 'success');
|
||||
}
|
||||
|
||||
// Wallet Balance Update Function
|
||||
function updateWalletBalance(newBalance) {
|
||||
if (newBalance !== undefined) {
|
||||
const walletBalance = document.getElementById('walletBalance');
|
||||
if (walletBalance) {
|
||||
walletBalance.textContent = newBalance.toFixed(2);
|
||||
}
|
||||
showToast(`Wallet updated: ${newBalance.toFixed(2)} AED`, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Copy to Clipboard Utility
|
||||
function copyToClipboard(text, successMessage = 'Copied to clipboard!') {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast(`📋 ${successMessage}`, 'success');
|
||||
}).catch(() => {
|
||||
showToast('Failed to copy to clipboard', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Download as File Utility
|
||||
function 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);
|
||||
showToast(`💾 ${successMessage}`, 'success');
|
||||
}
|
||||
|
||||
// Close panel on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeQuickAgents();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize accessibility features
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set initial ARIA states
|
||||
const quickAgentsButton = document.querySelector('.quick-agent-toggle');
|
||||
if (quickAgentsButton) {
|
||||
quickAgentsButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
if (panel) panel.setAttribute('aria-hidden', 'true');
|
||||
if (overlay) overlay.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Show welcome message
|
||||
setTimeout(() => {
|
||||
showToast('Agent template prototype loaded successfully!', 'success');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Placeholder for agent-specific JavaScript
|
||||
// ========================================
|
||||
//
|
||||
// Agent-specific functions would go here:
|
||||
// - Form validation
|
||||
// - AJAX requests
|
||||
// - Result processing
|
||||
// - Custom interactions
|
||||
//
|
||||
// Example structure:
|
||||
// function validateAgentForm() { ... }
|
||||
// function submitAgentRequest() { ... }
|
||||
// function displayAgentResults() { ... }
|
||||
//
|
||||
// ========================================
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,17 +0,0 @@
|
||||
=== Documentation Auto-Update Summary ===
|
||||
Update Date: 2025-08-01 09:23:11
|
||||
|
||||
Recent Commits:
|
||||
- 11d28a1 📚 Update documentation after GitHub push
|
||||
- f6970b6 🎨 Complete Phase 1 UI optimization with button hover fixes
|
||||
- 277e7ec 📄 Auto-update documentation timestamp after security fixes
|
||||
|
||||
Documentation Changes:
|
||||
- CLAUDE.md
|
||||
|
||||
Backend Changes:
|
||||
- docs_update_summary.txt
|
||||
|
||||
No documentation files required updates.
|
||||
|
||||
=== End Summary ===
|
||||
File diff suppressed because it is too large
Load Diff
1503
social_ads.html
1503
social_ads.html
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,652 +0,0 @@
|
||||
/* Digital Branding Services Page Styles */
|
||||
/* Following homepage.css patterns and using unified color system from base.css */
|
||||
|
||||
/* Hero Section - matches homepage hero */
|
||||
.digital-branding-hero {
|
||||
position: relative;
|
||||
background: var(--gradient-hero);
|
||||
overflow: hidden;
|
||||
min-height: 85vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: clamp(40px, 10vw, 80px) clamp(16px, 4vw, 24px) clamp(60px, 15vw, 100px);
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.trust-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 50px;
|
||||
margin-bottom: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-blue);
|
||||
border: 1px solid rgba(59, 130, 246, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.trust-badge-emoji {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-weight: 800;
|
||||
margin-bottom: 2.5rem;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
font-size: clamp(36px, 8vw, 110px);
|
||||
margin-bottom: clamp(24px, 6vw, 40px);
|
||||
}
|
||||
|
||||
.hero-title-gradient {
|
||||
background: var(--gradient-primary);
|
||||
background-size: 300% 300%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-title-normal {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 3.5rem;
|
||||
max-width: 720px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
line-height: 1.7;
|
||||
font-weight: 400;
|
||||
font-size: clamp(1.1rem, 3vw, 1.4rem);
|
||||
margin-bottom: clamp(32px, 8vw, 56px);
|
||||
padding: 0 clamp(8px, 2vw, 16px);
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 5rem;
|
||||
gap: clamp(12px, 3vw, 20px);
|
||||
margin-bottom: clamp(40px, 10vw, 80px);
|
||||
padding: 0 clamp(8px, 2vw, 16px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
border-radius: 1rem;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.25), 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
letter-spacing: 0.01em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
padding: clamp(14px, 4vw, 18px) clamp(24px, 6vw, 36px);
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
min-height: 48px;
|
||||
min-width: clamp(140px, 40vw, 180px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--background-card);
|
||||
color: var(--primary-blue);
|
||||
border-radius: 1rem;
|
||||
font-weight: 600;
|
||||
border: 2px solid var(--border-light);
|
||||
text-decoration: none;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(59, 130, 246, 0.08);
|
||||
letter-spacing: 0.01em;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
padding: clamp(14px, 4vw, 18px) clamp(24px, 6vw, 36px);
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
min-height: 48px;
|
||||
min-width: clamp(140px, 40vw, 180px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.15);
|
||||
border-color: var(--primary-blue);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
.trust-indicators {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
|
||||
gap: clamp(16px, 4vw, 48px);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 0 clamp(8px, 2vw, 16px);
|
||||
}
|
||||
|
||||
.trust-card {
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03);
|
||||
border-radius: 1rem;
|
||||
padding: clamp(16px, 4vw, 24px) clamp(12px, 3vw, 16px);
|
||||
}
|
||||
|
||||
.trust-number {
|
||||
background: var(--text-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: clamp(1rem, 3vw, 1.2rem);
|
||||
}
|
||||
|
||||
.trust-text {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
font-size: clamp(12px, 3vw, 15px);
|
||||
}
|
||||
|
||||
/* Why Choose Us Section - matches company profile */
|
||||
.why-choose-us {
|
||||
position: relative;
|
||||
background: var(--company-gradient);
|
||||
overflow: hidden;
|
||||
padding: clamp(60px, 15vw, 120px) clamp(16px, 4vw, 24px);
|
||||
}
|
||||
|
||||
.section-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: clamp(40px, 10vw, 80px);
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(30, 64, 175, 0.1);
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 50px;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(30, 64, 175, 0.2);
|
||||
}
|
||||
|
||||
.section-badge-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-badge-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 800;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-light);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.why-choose-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
||||
gap: clamp(24px, 6vw, 40px);
|
||||
}
|
||||
|
||||
.choice-card {
|
||||
background: white;
|
||||
border-radius: clamp(16px, 4vw, 24px);
|
||||
box-shadow: 0 8px 32px rgba(30, 64, 175, 0.08);
|
||||
text-align: center;
|
||||
padding: clamp(24px, 6vw, 40px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.choice-icon {
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: clamp(2.5rem, 6vw, 3.5rem);
|
||||
margin-bottom: clamp(16px, 4vw, 20px);
|
||||
}
|
||||
|
||||
.choice-title {
|
||||
font-weight: bold;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 1rem;
|
||||
font-size: clamp(1.2rem, 4vw, 1.5rem);
|
||||
margin-bottom: clamp(12px, 3vw, 16px);
|
||||
}
|
||||
|
||||
.choice-description {
|
||||
color: var(--text-light);
|
||||
line-height: 1.6;
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
}
|
||||
|
||||
/* Our Process Section - matches services */
|
||||
.our-process {
|
||||
background: var(--services-gradient);
|
||||
padding: clamp(60px, 15vw, 120px) clamp(16px, 4vw, 24px);
|
||||
}
|
||||
|
||||
.process-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
|
||||
gap: clamp(20px, 5vw, 32px);
|
||||
margin-bottom: clamp(40px, 10vw, 60px);
|
||||
}
|
||||
|
||||
.process-step-card {
|
||||
background: white;
|
||||
border-radius: clamp(16px, 4vw, 20px);
|
||||
box-shadow: 0 8px 32px rgba(30, 64, 175, 0.08);
|
||||
padding: clamp(24px, 6vw, 32px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: clamp(16px, 4vw, 20px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
width: clamp(40px, 10vw, 50px);
|
||||
height: clamp(40px, 10vw, 50px);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: clamp(16px, 4vw, 20px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: bold;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: clamp(1.1rem, 3.5vw, 1.3rem);
|
||||
}
|
||||
|
||||
.step-description {
|
||||
color: var(--text-light);
|
||||
line-height: 1.6;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
/* RACE Framework */
|
||||
.race-framework {
|
||||
background: white;
|
||||
border-radius: clamp(16px, 4vw, 24px);
|
||||
box-shadow: 0 12px 40px rgba(30, 64, 175, 0.08);
|
||||
padding: clamp(24px, 6vw, 40px);
|
||||
}
|
||||
|
||||
.race-title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: clamp(20px, 5vw, 32px);
|
||||
font-size: clamp(1.3rem, 4vw, 1.8rem);
|
||||
}
|
||||
|
||||
.race-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
|
||||
gap: clamp(16px, 4vw, 24px);
|
||||
}
|
||||
|
||||
.race-card {
|
||||
text-align: center;
|
||||
background: var(--background-light);
|
||||
border-radius: clamp(12px, 3vw, 16px);
|
||||
padding: clamp(16px, 4vw, 24px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.race-phase {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.race-icon {
|
||||
font-size: clamp(1.5rem, 4vw, 2rem);
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.race-phase h4 {
|
||||
font-weight: bold;
|
||||
color: var(--primary-blue);
|
||||
margin: 0;
|
||||
font-size: clamp(1.1rem, 3.5vw, 1.3rem);
|
||||
}
|
||||
|
||||
.race-focus {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.race-actions {
|
||||
color: var(--text-light);
|
||||
font-size: clamp(12px, 3vw, 14px);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Branding Services Section - matches services */
|
||||
.branding-services {
|
||||
background: var(--clients-gradient);
|
||||
padding: clamp(60px, 15vw, 120px) clamp(16px, 4vw, 24px);
|
||||
}
|
||||
|
||||
.services-title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-blue);
|
||||
font-size: clamp(1.8rem, 5vw, 2.5rem);
|
||||
}
|
||||
|
||||
.services-subtitle {
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 3rem;
|
||||
font-size: clamp(16px, 4vw, 20px);
|
||||
margin-bottom: clamp(24px, 6vw, 48px);
|
||||
}
|
||||
|
||||
.branding-services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
||||
gap: clamp(20px, 5vw, 32px);
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: clamp(16px, 4vw, 20px);
|
||||
box-shadow: 0 8px 32px rgba(30, 64, 175, 0.08);
|
||||
text-align: center;
|
||||
padding: clamp(24px, 6vw, 32px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: clamp(2.5rem, 6vw, 3rem);
|
||||
margin-bottom: clamp(16px, 4vw, 20px);
|
||||
}
|
||||
|
||||
.service-title {
|
||||
font-weight: bold;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 1rem;
|
||||
font-size: clamp(1.2rem, 4vw, 1.4rem);
|
||||
margin-bottom: clamp(12px, 3vw, 16px);
|
||||
}
|
||||
|
||||
.service-description {
|
||||
color: var(--text-light);
|
||||
line-height: 1.6;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
/* CTA Section - matches contact */
|
||||
.branding-cta {
|
||||
background: var(--contact-gradient);
|
||||
padding: clamp(60px, 15vw, 120px) clamp(16px, 4vw, 24px);
|
||||
}
|
||||
|
||||
.cta-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: var(--primary-blue);
|
||||
font-size: clamp(1.8rem, 5vw, 2.5rem);
|
||||
margin-bottom: clamp(24px, 6vw, 48px);
|
||||
}
|
||||
|
||||
.cta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(350px, 100%), 1fr));
|
||||
gap: clamp(30px, 8vw, 60px);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.cta-content {
|
||||
background: white;
|
||||
border-radius: clamp(16px, 4vw, 20px);
|
||||
box-shadow: 0 8px 32px rgba(30, 64, 175, 0.08);
|
||||
padding: clamp(24px, 6vw, 40px);
|
||||
}
|
||||
|
||||
.cta-content-title {
|
||||
font-weight: bold;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 1rem;
|
||||
font-size: clamp(1.3rem, 4vw, 1.6rem);
|
||||
}
|
||||
|
||||
.cta-description {
|
||||
color: var(--text-light);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
}
|
||||
|
||||
.cta-features {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.cta-feature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
color: var(--success-green);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cta-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(12px, 3vw, 16px);
|
||||
}
|
||||
|
||||
.cta-btn {
|
||||
padding: clamp(14px, 4vw, 16px) clamp(20px, 5vw, 24px);
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
transition: all 0.2s ease;
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cta-btn.primary {
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.cta-btn.primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.35);
|
||||
}
|
||||
|
||||
.cta-btn.secondary {
|
||||
background: var(--background-card);
|
||||
color: var(--primary-blue);
|
||||
border: 2px solid var(--border-light);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.cta-btn.secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--primary-blue);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.cta-info {
|
||||
background: white;
|
||||
border-radius: clamp(16px, 4vw, 20px);
|
||||
box-shadow: 0 8px 32px rgba(30, 64, 175, 0.08);
|
||||
padding: clamp(24px, 6vw, 40px);
|
||||
}
|
||||
|
||||
.cta-info-title {
|
||||
font-weight: bold;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: clamp(1.2rem, 4vw, 1.5rem);
|
||||
margin-bottom: clamp(16px, 4vw, 24px);
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: clamp(12px, 3vw, 16px);
|
||||
margin-bottom: clamp(20px, 5vw, 32px);
|
||||
}
|
||||
|
||||
.contact-icon {
|
||||
background: var(--primary-gradient);
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: clamp(40px, 10vw, 50px);
|
||||
height: clamp(40px, 10vw, 50px);
|
||||
font-size: clamp(16px, 4vw, 20px);
|
||||
}
|
||||
|
||||
.contact-details h4 {
|
||||
font-weight: 600;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: clamp(16px, 4vw, 19px);
|
||||
}
|
||||
|
||||
.contact-details p {
|
||||
color: var(--text-light);
|
||||
line-height: 1.6;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.contact-email {
|
||||
color: var(--primary-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.contact-email:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.commitment-statement {
|
||||
background: var(--services-gradient);
|
||||
border-left: 4px solid var(--primary-blue);
|
||||
border-radius: clamp(12px, 3vw, 16px);
|
||||
padding: clamp(16px, 4vw, 24px);
|
||||
margin-top: clamp(20px, 5vw, 32px);
|
||||
}
|
||||
|
||||
.commitment-text {
|
||||
color: var(--primary-blue);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Loading state for buttons */
|
||||
.cta-btn.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Mobile responsive adjustments */
|
||||
@media (max-width: 767px) {
|
||||
.cta-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.process-step-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
margin: 0 auto 1rem auto;
|
||||
}
|
||||
|
||||
.race-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -1,645 +0,0 @@
|
||||
/**
|
||||
* Data Analyzer - Agent-Specific JavaScript
|
||||
* Handles unique functionality for Data Analyzer agent
|
||||
* Uses WorkflowsCore architecture like other agents
|
||||
*/
|
||||
|
||||
class DataAnalyzerProcessor extends WorkflowsCore {
|
||||
constructor() {
|
||||
super();
|
||||
this.agentSlug = 'data-analyzer';
|
||||
this.webhookUrl = 'http://localhost:5678/webhook/simple-pdf-processor';
|
||||
this.price = 8.0; // Will be overridden by template data
|
||||
this.sessionId = this.constructor.generateSessionId();
|
||||
|
||||
// Initialize on page load
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Set data attributes from page
|
||||
const priceElement = document.body.getAttribute('data-agent-price');
|
||||
if (priceElement) {
|
||||
this.price = parseFloat(priceElement);
|
||||
}
|
||||
|
||||
// Initialize form submission
|
||||
const form = document.getElementById('agentForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', this.handleFormSubmission.bind(this));
|
||||
}
|
||||
|
||||
// Initialize file upload functionality
|
||||
this.initializeFileUpload();
|
||||
|
||||
// Initialize form validation
|
||||
this.initializeFormValidation();
|
||||
|
||||
// Set initial radio selection
|
||||
const firstRadio = document.querySelector('.radio-card');
|
||||
if (firstRadio && !document.querySelector('.radio-card.selected')) {
|
||||
firstRadio.classList.add('selected');
|
||||
const input = firstRadio.querySelector('input[type="radio"]');
|
||||
if (input) input.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize file upload functionality
|
||||
*/
|
||||
initializeFileUpload() {
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', this.handleFileChange.bind(this));
|
||||
}
|
||||
|
||||
// Initialize drag and drop
|
||||
const uploadArea = document.querySelector('.file-upload-area');
|
||||
if (uploadArea && fileInput) {
|
||||
this.constructor.setupDragAndDrop(uploadArea, fileInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission with hybrid N8N/Django approach
|
||||
*/
|
||||
async handleFormSubmission(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.isFormValid()) {
|
||||
this.constructor.showToast('Please upload a file and select analysis type', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check authentication and balance
|
||||
if (!this.constructor.checkAuthentication()) return;
|
||||
if (!this.constructor.checkBalance(this.price)) return;
|
||||
|
||||
// Show processing status and disable submit button
|
||||
this.constructor.showProcessing('Analyzing your data file...');
|
||||
|
||||
const submitBtn = document.getElementById('generateBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Analyzing...';
|
||||
}
|
||||
|
||||
try {
|
||||
// Try direct N8N integration for better performance (with Django fallback)
|
||||
const useDirectN8N = false; // Feature flag - disabled for file uploads (complex)
|
||||
|
||||
if (useDirectN8N) {
|
||||
await this.processViaDirectN8N(e.target);
|
||||
} else {
|
||||
// For file uploads, use immediate Django processing (N8N direct upload is complex)
|
||||
await this.processViaDjangoImmediate(e.target);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast('❌ Connection error. Please try again.', 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Django processing for file uploads (immediate response for data analyzer)
|
||||
*/
|
||||
async processViaDjangoImmediate(form) {
|
||||
const formData = new FormData(form);
|
||||
|
||||
const response = await fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.analysis_results) {
|
||||
// Data analyzer returns results immediately, no polling needed
|
||||
this.constructor.hideProcessing();
|
||||
|
||||
if (result.wallet_balance !== undefined) {
|
||||
this.constructor.updateWalletBalance(result.wallet_balance);
|
||||
}
|
||||
|
||||
// Display results immediately
|
||||
const analysisData = result.analysis_results;
|
||||
const formattedHtml = this.formatAnalysisResults(analysisData);
|
||||
WorkflowsCore.showResults(formattedHtml, 'Analysis Results');
|
||||
this.constructor.showToast('✅ Data analysis completed successfully!', 'success');
|
||||
|
||||
this.resetSubmitButton();
|
||||
} else {
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast(`❌ ${result.error || 'Processing failed'}`, 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation specific to Data Analyzer
|
||||
*/
|
||||
initializeFormValidation() {
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
const analysisTypeInputs = document.querySelectorAll('input[name="analysisType"]');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', () => this.validateField('dataFile'));
|
||||
}
|
||||
|
||||
analysisTypeInputs.forEach(input => {
|
||||
input.addEventListener('change', () => this.validateField('analysisType'));
|
||||
});
|
||||
}
|
||||
|
||||
validateField(fieldName) {
|
||||
switch (fieldName) {
|
||||
case 'dataFile':
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
if (!fileInput.files || fileInput.files.length === 0) {
|
||||
this.constructor.showFieldError('dataFile', 'Please select a data file');
|
||||
return false;
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
this.constructor.showFieldError('dataFile', 'File too large. Maximum size is 10MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowedExtensions = ['.pdf'];
|
||||
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
||||
if (!allowedExtensions.includes(fileExtension)) {
|
||||
this.constructor.showFieldError('dataFile', 'Unsupported file type. Please use PDF files only');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'analysisType':
|
||||
const analysisType = document.querySelector('input[name="analysisType"]:checked');
|
||||
if (!analysisType) {
|
||||
this.constructor.showFieldError('analysisType', 'Please select an analysis type');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.constructor.clearFieldError(fieldName);
|
||||
return true;
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
const fileValid = this.validateField('dataFile');
|
||||
const analysisValid = this.validateField('analysisType');
|
||||
|
||||
return fileValid && analysisValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file change events with enhanced UX
|
||||
*/
|
||||
handleFileChange(event) {
|
||||
const file = event.target.files[0];
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
|
||||
if (file) {
|
||||
// Validate file first
|
||||
const validation = this.validateFileUpload(file);
|
||||
|
||||
if (!validation.valid) {
|
||||
this.showValidationMessage(validation.message, 'error');
|
||||
uploadArea.classList.add('upload-error');
|
||||
uploadArea.classList.remove('file-selected');
|
||||
this.hideFilePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show success validation
|
||||
this.showValidationMessage(validation.message, 'success');
|
||||
|
||||
// Update upload area
|
||||
uploadArea.classList.remove('upload-error');
|
||||
uploadArea.classList.add('file-selected');
|
||||
|
||||
// Show file preview with enhanced info
|
||||
this.showFilePreview(file);
|
||||
|
||||
// Clear any previous errors
|
||||
this.constructor.clearFieldError('dataFile');
|
||||
} else {
|
||||
// Reset all states
|
||||
this.resetFileUploadState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file upload with detailed feedback
|
||||
*/
|
||||
validateFileUpload(file) {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
const allowedTypes = ['application/pdf'];
|
||||
const allowedExtensions = ['.pdf'];
|
||||
|
||||
// Check file type
|
||||
if (!allowedTypes.includes(file.type) && !allowedExtensions.includes('.' + file.name.split('.').pop().toLowerCase())) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Invalid file type. Please upload a PDF file only.'
|
||||
};
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (file.size > maxSize) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `File too large (${this.formatFileSize(file.size)}). Maximum size is 10MB.`
|
||||
};
|
||||
}
|
||||
|
||||
// Check for empty file
|
||||
if (file.size === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'File appears to be empty. Please select a valid PDF file.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: `✅ File validated successfully (${this.formatFileSize(file.size)})`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show file preview with enhanced information
|
||||
*/
|
||||
showFilePreview(file) {
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const previewFileName = document.getElementById('previewFileName');
|
||||
const previewFileSize = document.getElementById('previewFileSize');
|
||||
const previewTimestamp = document.getElementById('previewTimestamp');
|
||||
|
||||
if (filePreview && previewFileName && previewFileSize && previewTimestamp) {
|
||||
previewFileName.textContent = file.name;
|
||||
previewFileSize.textContent = this.formatFileSize(file.size);
|
||||
previewTimestamp.textContent = `Added ${new Date().toLocaleTimeString()}`;
|
||||
|
||||
filePreview.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide file preview
|
||||
*/
|
||||
hideFilePreview() {
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
if (filePreview) {
|
||||
filePreview.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show validation message with type
|
||||
*/
|
||||
showValidationMessage(message, type = 'info') {
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
if (validationMessage) {
|
||||
validationMessage.textContent = message;
|
||||
validationMessage.className = `validation-message show ${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide validation message
|
||||
*/
|
||||
hideValidationMessage() {
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
if (validationMessage) {
|
||||
validationMessage.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset file upload state
|
||||
*/
|
||||
resetFileUploadState() {
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove('file-selected', 'upload-error', 'uploading');
|
||||
}
|
||||
|
||||
this.hideFilePreview();
|
||||
this.hideValidationMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show upload progress
|
||||
*/
|
||||
showUploadProgress() {
|
||||
const uploadProgress = document.getElementById('uploadProgress');
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
const progressText = document.getElementById('progressText');
|
||||
|
||||
if (uploadProgress) {
|
||||
uploadProgress.classList.add('show');
|
||||
}
|
||||
|
||||
// Simulate progress for visual feedback
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 15;
|
||||
if (progress > 90) progress = 90;
|
||||
|
||||
if (progressFill) progressFill.style.width = `${progress}%`;
|
||||
if (progressText) progressText.textContent = `Uploading... ${Math.round(progress)}%`;
|
||||
|
||||
if (progress >= 90) {
|
||||
clearInterval(interval);
|
||||
if (progressText) progressText.textContent = 'Processing file...';
|
||||
}
|
||||
}, 200);
|
||||
|
||||
this.uploadProgressInterval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide upload progress
|
||||
*/
|
||||
hideUploadProgress() {
|
||||
const uploadProgress = document.getElementById('uploadProgress');
|
||||
if (uploadProgress) {
|
||||
uploadProgress.classList.remove('show');
|
||||
}
|
||||
|
||||
if (this.uploadProgressInterval) {
|
||||
clearInterval(this.uploadProgressInterval);
|
||||
this.uploadProgressInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format analysis results for HTML display
|
||||
*/
|
||||
formatAnalysisResults(analysisData) {
|
||||
let resultsHtml = '<h3>✅ Analysis Complete</h3>';
|
||||
|
||||
// Check if we have structured sections data
|
||||
if (analysisData && typeof analysisData === 'object' && analysisData.sections && Array.isArray(analysisData.sections) && analysisData.sections.length > 0) {
|
||||
resultsHtml += '<div class="analysis-sections">';
|
||||
|
||||
analysisData.sections.forEach(section => {
|
||||
if (section.heading && section.content) {
|
||||
resultsHtml += `
|
||||
<div style="background: var(--surface-variant); border-radius: var(--radius-md); padding: var(--spacing-lg); margin-bottom: var(--spacing-md); border-left: 4px solid var(--primary);">
|
||||
<h4 style="color: var(--primary); font-weight: 600; margin: 0 0 var(--spacing-md) 0; font-size: 16px;">📋 ${this.escapeHtml(section.heading)}</h4>
|
||||
<div style="color: var(--on-surface); line-height: 1.6; font-size: 14px;">${this.escapeHtml(section.content).replace(/\n/g, '<br>')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
resultsHtml += '</div>';
|
||||
} else {
|
||||
// Handle simple text response or fallback
|
||||
const content = typeof analysisData === 'string' ? analysisData : JSON.stringify(analysisData, null, 2);
|
||||
resultsHtml += `
|
||||
<div style="background: var(--surface-variant); border-radius: var(--radius-md); padding: var(--spacing-lg); margin-bottom: var(--spacing-md); border-left: 4px solid var(--primary);">
|
||||
<h4 style="color: var(--primary); font-weight: 600; margin: 0 0 var(--spacing-md) 0; font-size: 16px;">📊 Analysis Results</h4>
|
||||
<div style="color: var(--on-surface); line-height: 1.6; font-size: 14px; white-space: pre-wrap;">${this.escapeHtml(content)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
if (analysisData && analysisData.timestamp) {
|
||||
resultsHtml += `<p style="margin-top: var(--spacing-lg); text-align: center; color: var(--on-surface-variant);"><small>Analysis completed: ${new Date(analysisData.timestamp).toLocaleString()}</small></p>`;
|
||||
} else {
|
||||
resultsHtml += `<p style="margin-top: var(--spacing-lg); text-align: center; color: var(--on-surface-variant);"><small>Analysis completed: ${new Date().toLocaleString()}</small></p>`;
|
||||
}
|
||||
|
||||
return resultsHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset submit button to original state
|
||||
*/
|
||||
resetSubmitButton() {
|
||||
const submitBtn = document.getElementById('generateBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = `🚀 Analyze Data (${this.price} AED)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data Analyzer specific functions (global for template onclick handlers)
|
||||
function selectRadio(value) {
|
||||
// Remove selected class from all cards
|
||||
document.querySelectorAll('.radio-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Add selected class to clicked card
|
||||
const selectedCard = document.querySelector(`input[value="${value}"]`).closest('.radio-card');
|
||||
if (selectedCard) {
|
||||
selectedCard.classList.add('selected');
|
||||
}
|
||||
|
||||
// Select the radio button
|
||||
const radioInput = document.getElementById(value);
|
||||
if (radioInput) {
|
||||
radioInput.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// File management functions (global for template onclick handlers)
|
||||
function triggerFileSelect() {
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFile() {
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile() {
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
|
||||
// Reset upload area
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove('file-selected', 'upload-error');
|
||||
}
|
||||
|
||||
// Hide preview and validation
|
||||
if (filePreview) {
|
||||
filePreview.classList.remove('show');
|
||||
}
|
||||
|
||||
if (validationMessage) {
|
||||
validationMessage.classList.remove('show');
|
||||
}
|
||||
|
||||
// Clear any form errors
|
||||
if (window.dataAnalyzerProcessor) {
|
||||
window.dataAnalyzerProcessor.constructor.clearFieldError('dataFile');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and drop functionality
|
||||
function setupDragAndDrop() {
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
|
||||
if (!uploadArea || !fileInput) return;
|
||||
|
||||
// Prevent default drag behaviors
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
document.body.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
// Highlight drop area when item is dragged over it
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
// Handle dropped files
|
||||
uploadArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function highlight(e) {
|
||||
uploadArea.classList.add('dragover');
|
||||
}
|
||||
|
||||
function unhighlight(e) {
|
||||
uploadArea.classList.remove('dragover');
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
if (files.length > 0) {
|
||||
fileInput.files = files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result action functions (global for button onclick handlers)
|
||||
function copyResults() {
|
||||
const content = document.getElementById('resultsContent');
|
||||
if (content) {
|
||||
const text = content.textContent || '';
|
||||
WorkflowsCore.copyToClipboard(text, 'Analysis results copied to clipboard!');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadResults() {
|
||||
const content = document.getElementById('resultsContent');
|
||||
if (content) {
|
||||
const text = content.textContent || '';
|
||||
WorkflowsCore.downloadAsFile(text, 'data-analysis-results.txt', 'Analysis results downloaded!');
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
const form = document.getElementById('agentForm');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
|
||||
if (resultsContainer) resultsContainer.style.display = 'none';
|
||||
if (processingStatus) processingStatus.style.display = 'none';
|
||||
|
||||
// Clear file display
|
||||
const fileNameDisplay = document.getElementById('fileName');
|
||||
const fileSizeDisplay = document.getElementById('fileSize');
|
||||
if (fileNameDisplay) {
|
||||
fileNameDisplay.textContent = '';
|
||||
fileNameDisplay.style.display = 'none';
|
||||
}
|
||||
if (fileSizeDisplay) {
|
||||
fileSizeDisplay.textContent = '';
|
||||
fileSizeDisplay.style.display = 'none';
|
||||
}
|
||||
|
||||
// Clear validation errors
|
||||
WorkflowsCore.clearFieldError('dataFile');
|
||||
WorkflowsCore.clearFieldError('analysisType');
|
||||
|
||||
// Reset radio selection
|
||||
const firstRadio = document.querySelector('.radio-card');
|
||||
if (firstRadio) {
|
||||
document.querySelectorAll('.radio-card').forEach(card => card.classList.remove('selected'));
|
||||
firstRadio.classList.add('selected');
|
||||
const input = firstRadio.querySelector('input[type="radio"]');
|
||||
if (input) input.checked = true;
|
||||
}
|
||||
|
||||
// Scroll back to form
|
||||
const formSection = document.getElementById('agentForm');
|
||||
if (formSection) {
|
||||
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Data Analyzer Processor when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize processor (data attributes set by template)
|
||||
window.dataAnalyzerProcessor = new DataAnalyzerProcessor();
|
||||
|
||||
// Setup drag and drop functionality
|
||||
setupDragAndDrop();
|
||||
});
|
||||
@ -1,586 +0,0 @@
|
||||
/**
|
||||
* Workflows Core - Shared utilities for all agents
|
||||
* Contains only truly universal functions that ALL agents use identically
|
||||
*/
|
||||
|
||||
class WorkflowsCore {
|
||||
/**
|
||||
* Update wallet balance display across the page
|
||||
*/
|
||||
static updateWalletBalance(newBalance) {
|
||||
if (newBalance !== undefined) {
|
||||
// Update header balance
|
||||
const headerBalance = document.querySelector('a[data-wallet-balance]');
|
||||
if (headerBalance) {
|
||||
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
|
||||
}
|
||||
|
||||
// Update page balance
|
||||
const pageBalance = document.getElementById('walletBalance');
|
||||
if (pageBalance) {
|
||||
pageBalance.textContent = newBalance.toFixed(2);
|
||||
}
|
||||
|
||||
// Update all data attributes
|
||||
document.querySelectorAll('[data-wallet-balance]').forEach(element => {
|
||||
element.textContent = `${newBalance.toFixed(2)} AED`;
|
||||
});
|
||||
|
||||
// Update balance in navigation
|
||||
const headerBalanceNav = document.querySelector('a[href="/wallet/"]');
|
||||
if (headerBalanceNav) {
|
||||
headerBalanceNav.textContent = `💰 ${newBalance.toFixed(2)} AED`;
|
||||
}
|
||||
|
||||
// Store current balance globally
|
||||
window.currentWalletBalance = newBalance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification with consistent styling
|
||||
*/
|
||||
static showToast(message, type = 'info') {
|
||||
// Remove existing toasts
|
||||
document.querySelectorAll('.toast').forEach(toast => toast.remove());
|
||||
|
||||
// Create new toast
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Show toast
|
||||
setTimeout(() => toast.classList.add('show'), 100);
|
||||
|
||||
// Auto remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSRF token from page
|
||||
*/
|
||||
static getCsrfToken() {
|
||||
const token = document.querySelector('[name=csrfmiddlewaretoken]');
|
||||
return token ? token.value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique session ID for N8N calls
|
||||
*/
|
||||
static generateSessionId() {
|
||||
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user authentication
|
||||
*/
|
||||
static checkAuthentication() {
|
||||
const isAuthenticated = document.body.getAttribute('data-user-authenticated') === 'true';
|
||||
|
||||
if (!isAuthenticated) {
|
||||
this.showToast('Please log in to use this agent', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth/login/';
|
||||
}, 2000);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check wallet balance against required amount
|
||||
*/
|
||||
static checkBalance(requiredAmount) {
|
||||
// Get current balance from wallet card
|
||||
const balanceElement = document.querySelector('[data-wallet-balance]');
|
||||
if (balanceElement) {
|
||||
const currentBalance = parseFloat(balanceElement.textContent.replace(/[^\d.]/g, ''));
|
||||
if (currentBalance < requiredAmount) {
|
||||
this.showToast(`Insufficient balance. You need ${requiredAmount} AED but have ${currentBalance.toFixed(2)} AED`, 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/wallet/';
|
||||
}, 2000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduct wallet balance via Django API
|
||||
*/
|
||||
static async deductBalance(amount, description, agentSlug) {
|
||||
try {
|
||||
const response = await fetch('/wallet/api/deduct/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount: amount,
|
||||
description: description,
|
||||
agent: agentSlug
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Balance deduction failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.updateWalletBalance(result.new_balance);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Wallet deduction error:', error);
|
||||
this.showToast(`Payment error: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show processing status (common pattern)
|
||||
*/
|
||||
static showProcessing(customTitle = 'Processing your request...') {
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
|
||||
if (processingStatus) {
|
||||
processingStatus.style.display = 'block';
|
||||
processingStatus.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (resultsContainer) {
|
||||
resultsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Show processing toast
|
||||
this.showToast('🔄 ' + customTitle, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide processing status (common pattern)
|
||||
*/
|
||||
static hideProcessing() {
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
if (processingStatus) {
|
||||
processingStatus.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard with feedback
|
||||
*/
|
||||
static async copyToClipboard(text, successMessage = 'Copied to clipboard!') {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
this.showToast('📋 ' + successMessage, 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
this.showToast('❌ Failed to copy to clipboard', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download text as file with feedback
|
||||
*/
|
||||
static downloadAsFile(content, filename, successMessage = 'File downloaded!') {
|
||||
try {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
// Show success message only if provided
|
||||
if (successMessage && successMessage.trim()) {
|
||||
this.showToast('💾 ' + successMessage, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download error:', error);
|
||||
this.showToast('❌ Failed to download file', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display
|
||||
*/
|
||||
static 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show field validation error
|
||||
*/
|
||||
static showFieldError(fieldName, message) {
|
||||
const field = document.getElementById(fieldName);
|
||||
const errorElement = document.getElementById(`${fieldName}-error`);
|
||||
|
||||
if (field) {
|
||||
field.classList.add('error');
|
||||
}
|
||||
|
||||
if (errorElement) {
|
||||
errorElement.textContent = message;
|
||||
errorElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear field validation error
|
||||
*/
|
||||
static clearFieldError(fieldName) {
|
||||
const field = document.getElementById(fieldName);
|
||||
const errorElement = document.getElementById(`${fieldName}-error`);
|
||||
|
||||
if (field) {
|
||||
field.classList.remove('error');
|
||||
}
|
||||
|
||||
if (errorElement) {
|
||||
errorElement.textContent = '';
|
||||
errorElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick Agent Panel Management (common across all agents)
|
||||
*/
|
||||
static toggleQuickAgents() {
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
const toggle = document.querySelector('.quick-agent-toggle');
|
||||
|
||||
if (!panel || !overlay) return;
|
||||
|
||||
const isActive = panel.classList.contains('active');
|
||||
|
||||
if (isActive) {
|
||||
// Close panel
|
||||
panel.classList.remove('active');
|
||||
overlay.classList.remove('active');
|
||||
if (toggle) toggle.classList.remove('active');
|
||||
// Update ARIA attributes
|
||||
if (toggle) toggle.setAttribute('aria-expanded', 'false');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
overlay.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
// Open panel
|
||||
panel.classList.add('active');
|
||||
overlay.classList.add('active');
|
||||
if (toggle) toggle.classList.add('active');
|
||||
// Update ARIA attributes
|
||||
if (toggle) toggle.setAttribute('aria-expanded', 'true');
|
||||
panel.setAttribute('aria-hidden', 'false');
|
||||
overlay.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
static closeQuickAgents() {
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
const toggle = document.querySelector('.quick-agent-toggle');
|
||||
|
||||
if (panel) panel.classList.remove('active');
|
||||
if (overlay) overlay.classList.remove('active');
|
||||
if (toggle) toggle.classList.remove('active');
|
||||
|
||||
// Update ARIA attributes
|
||||
if (toggle) toggle.setAttribute('aria-expanded', 'false');
|
||||
if (panel) panel.setAttribute('aria-hidden', 'true');
|
||||
if (overlay) overlay.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize form validation on all forms (common pattern)
|
||||
*/
|
||||
static initializeFormValidation() {
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
const inputs = form.querySelectorAll('input, textarea, select');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('input', () => {
|
||||
if (input.value.trim()) {
|
||||
this.clearFieldError(input.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/hide results container (common pattern)
|
||||
*/
|
||||
static showResults(content, title = 'Results') {
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const resultsTitle = document.querySelector('#resultsContainer .widget-title');
|
||||
const resultsContent = document.querySelector('#resultsContainer .results-content');
|
||||
|
||||
if (resultsContainer) {
|
||||
resultsContainer.style.display = 'block';
|
||||
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (resultsTitle) {
|
||||
resultsTitle.textContent = title;
|
||||
}
|
||||
|
||||
if (resultsContent) {
|
||||
resultsContent.innerHTML = content;
|
||||
}
|
||||
|
||||
// Hide processing
|
||||
this.hideProcessing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy agent results to clipboard (common agent function)
|
||||
*/
|
||||
static copyResults() {
|
||||
const content = document.getElementById('resultsContent') || document.querySelector('.results-content');
|
||||
if (content) {
|
||||
const text = content.textContent || content.innerText || '';
|
||||
this.copyToClipboard(text, 'Results copied to clipboard!');
|
||||
} else {
|
||||
this.showToast('❌ No results to copy', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download agent results as file (common agent function)
|
||||
*/
|
||||
static downloadResults(filename = 'analysis-results.txt') {
|
||||
const content = document.getElementById('resultsContent') || document.querySelector('.results-content');
|
||||
if (content) {
|
||||
const text = content.textContent || content.innerText || '';
|
||||
this.downloadAsFile(text, filename, ''); // No toast message
|
||||
} else {
|
||||
this.showToast('❌ No results to download', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset agent form and hide results (common agent function)
|
||||
*/
|
||||
static resetForm() {
|
||||
const form = document.getElementById('agentForm');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
|
||||
if (resultsContainer) resultsContainer.style.display = 'none';
|
||||
if (processingStatus) processingStatus.style.display = 'none';
|
||||
|
||||
// Reset file upload areas
|
||||
const uploadAreas = document.querySelectorAll('.file-upload-area');
|
||||
uploadAreas.forEach(area => {
|
||||
area.classList.remove('file-selected');
|
||||
const uploadText = area.querySelector('.upload-text');
|
||||
if (uploadText) {
|
||||
uploadText.innerHTML = `
|
||||
<div class="upload-icon">📁</div>
|
||||
<div><strong>Click to upload</strong> or drag and drop</div>
|
||||
<div>PDF files only</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset radio selections to first option
|
||||
const radioCards = document.querySelectorAll('.radio-card');
|
||||
radioCards.forEach(card => card.classList.remove('selected'));
|
||||
const firstCard = document.querySelector('.radio-card');
|
||||
if (firstCard) {
|
||||
firstCard.classList.add('selected');
|
||||
const input = firstCard.querySelector('input[type="radio"]');
|
||||
if (input) input.checked = true;
|
||||
}
|
||||
|
||||
// Form reset complete - no toast needed (visual feedback is sufficient)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup drag and drop for file inputs (enhanced from prototype)
|
||||
*/
|
||||
static setupDragAndDrop(container, fileInput) {
|
||||
if (!container || !fileInput) return;
|
||||
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
container.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
container.addEventListener(eventName, () => {
|
||||
container.classList.add('dragover');
|
||||
}, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
container.addEventListener(eventName, () => {
|
||||
container.classList.remove('dragover');
|
||||
}, false);
|
||||
});
|
||||
|
||||
container.addEventListener('drop', (e) => {
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
fileInput.files = files;
|
||||
fileInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file input change (common pattern for file uploads)
|
||||
*/
|
||||
static handleFileChange(fileInput) {
|
||||
const file = fileInput.files[0];
|
||||
const fieldName = fileInput.name;
|
||||
const container = fileInput.closest('.file-upload-container');
|
||||
|
||||
if (!file || !container) return;
|
||||
|
||||
const fileInfo = container.querySelector(`#${fieldName}_file_info`);
|
||||
const fileName = fileInfo?.querySelector('.file-name');
|
||||
const fileSize = fileInfo?.querySelector('.file-size');
|
||||
const uploadArea = container.querySelector(`#${fieldName}_upload_area`);
|
||||
|
||||
if (fileName) fileName.textContent = file.name;
|
||||
if (fileSize) fileSize.textContent = this.formatFileSize(file.size);
|
||||
if (fileInfo) fileInfo.style.display = 'block';
|
||||
if (uploadArea) uploadArea.style.display = 'none';
|
||||
|
||||
// Clear any previous errors
|
||||
this.clearFieldError(fieldName);
|
||||
|
||||
// Show success feedback
|
||||
this.showToast(`📁 File selected: ${file.name}`, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove selected file (common pattern)
|
||||
*/
|
||||
static removeFile(fieldName) {
|
||||
const fileInput = document.getElementById(fieldName);
|
||||
const container = fileInput?.closest('.file-upload-container');
|
||||
|
||||
if (!fileInput || !container) return;
|
||||
|
||||
const fileInfo = container.querySelector(`#${fieldName}_file_info`);
|
||||
const uploadArea = container.querySelector(`#${fieldName}_upload_area`);
|
||||
|
||||
// Clear file input
|
||||
fileInput.value = '';
|
||||
|
||||
// Hide file info, show upload area
|
||||
if (fileInfo) fileInfo.style.display = 'none';
|
||||
if (uploadArea) uploadArea.style.display = 'block';
|
||||
|
||||
this.showToast('📁 File removed', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// Global utility functions that all agents can use
|
||||
function toggleQuickAgents() {
|
||||
WorkflowsCore.toggleQuickAgents();
|
||||
}
|
||||
|
||||
function closeQuickAgents() {
|
||||
WorkflowsCore.closeQuickAgents();
|
||||
}
|
||||
|
||||
// Global convenience functions for common actions
|
||||
function removeFile(fieldName) {
|
||||
WorkflowsCore.removeFile(fieldName);
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
WorkflowsCore.showToast(message, type);
|
||||
}
|
||||
|
||||
function copyToClipboard(text, successMessage) {
|
||||
WorkflowsCore.copyToClipboard(text, successMessage);
|
||||
}
|
||||
|
||||
function downloadAsFile(content, filename, successMessage) {
|
||||
WorkflowsCore.downloadAsFile(content, filename, successMessage);
|
||||
}
|
||||
|
||||
// Global convenience functions for common agent actions
|
||||
function copyResults() {
|
||||
WorkflowsCore.copyResults();
|
||||
}
|
||||
|
||||
function downloadResults(filename) {
|
||||
WorkflowsCore.downloadResults(filename);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
WorkflowsCore.resetForm();
|
||||
}
|
||||
|
||||
// Initialize on DOM load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize form validation for all forms
|
||||
WorkflowsCore.initializeFormValidation();
|
||||
|
||||
// Setup file upload drag and drop for any file inputs
|
||||
const fileInputs = document.querySelectorAll('input[type="file"]');
|
||||
fileInputs.forEach(input => {
|
||||
const container = input.closest('.file-upload-container');
|
||||
if (container) {
|
||||
WorkflowsCore.setupDragAndDrop(container, input);
|
||||
}
|
||||
|
||||
// Setup file change handler
|
||||
input.addEventListener('change', () => {
|
||||
WorkflowsCore.handleFileChange(input);
|
||||
});
|
||||
});
|
||||
|
||||
// Set initial ARIA states for quick agents panel
|
||||
const quickAgentsButton = document.querySelector('.quick-agent-toggle');
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
|
||||
if (quickAgentsButton) quickAgentsButton.setAttribute('aria-expanded', 'false');
|
||||
if (panel) panel.setAttribute('aria-hidden', 'true');
|
||||
if (overlay) overlay.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
// Close panel with Escape key (common functionality)
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
WorkflowsCore.closeQuickAgents();
|
||||
}
|
||||
});
|
||||
|
||||
// Export for module usage if needed
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = WorkflowsCore;
|
||||
}
|
||||
@ -1,684 +0,0 @@
|
||||
/**
|
||||
* Universal Workflows JavaScript Framework
|
||||
* Handles all agent interactions with direct N8N integration
|
||||
*/
|
||||
|
||||
class WorkflowProcessor {
|
||||
constructor(agentSlug, webhookUrl, price) {
|
||||
this.agentSlug = agentSlug;
|
||||
this.webhookUrl = webhookUrl;
|
||||
this.price = price;
|
||||
this.sessionId = this.generateSessionId();
|
||||
this.processing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission - main entry point
|
||||
*/
|
||||
async handleFormSubmission(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.processing) {
|
||||
this.showToast('Please wait, processing your previous request...', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Convert FormData to object
|
||||
const data = {};
|
||||
for (let [key, value] of formData.entries()) {
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
await this.processWorkflow(data, formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main workflow processing function
|
||||
*/
|
||||
async processWorkflow(data, formData = null) {
|
||||
try {
|
||||
this.processing = true;
|
||||
|
||||
// 1. Validate form
|
||||
if (!this.validateForm(data)) {
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check authentication and balance
|
||||
if (!await this.checkBalance()) {
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Show processing status
|
||||
this.showProcessing();
|
||||
|
||||
// 4. Call N8N directly
|
||||
const result = await this.callN8N(data, formData);
|
||||
|
||||
if (result && result.output) {
|
||||
// 5. Deduct balance via Django API
|
||||
await this.deductBalance();
|
||||
|
||||
// 6. Display results
|
||||
this.displayResults(result);
|
||||
this.showToast('Processing completed successfully!', 'success');
|
||||
} else {
|
||||
throw new Error('No output received from N8N');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Workflow processing error:', error);
|
||||
this.showError(`Processing failed: ${error.message}`);
|
||||
this.showToast('Processing failed. Please try again.', 'error');
|
||||
} finally {
|
||||
this.processing = false;
|
||||
this.hideProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call N8N webhook directly
|
||||
*/
|
||||
async callN8N(data, formData = null) {
|
||||
const messageText = this.formatMessage(data);
|
||||
|
||||
const payload = {
|
||||
sessionId: this.sessionId,
|
||||
message: { text: messageText },
|
||||
agentSlug: this.agentSlug,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Handle file uploads if present
|
||||
if (formData && this.hasFileUploads(data)) {
|
||||
// For file uploads, we need to handle differently
|
||||
return await this.callN8NWithFiles(messageText, formData);
|
||||
}
|
||||
|
||||
const response = await fetch(this.webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`N8N webhook failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle N8N calls with file uploads
|
||||
*/
|
||||
async callN8NWithFiles(messageText, formData) {
|
||||
// Create multipart form data for file uploads
|
||||
const uploadData = new FormData();
|
||||
uploadData.append('sessionId', this.sessionId);
|
||||
uploadData.append('message', JSON.stringify({ text: messageText }));
|
||||
uploadData.append('agentSlug', this.agentSlug);
|
||||
|
||||
// Add files
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (value instanceof File) {
|
||||
uploadData.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(this.webhookUrl, {
|
||||
method: 'POST',
|
||||
body: uploadData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`N8N webhook with files failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduct wallet balance via Django API
|
||||
*/
|
||||
async deductBalance() {
|
||||
const response = await fetch('/wallet/api/deduct/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount: this.price,
|
||||
description: `${this.agentSlug} processing`,
|
||||
agent: this.agentSlug
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Balance deduction failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.updateWalletBalance(result.new_balance);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate form data
|
||||
*/
|
||||
validateForm(data) {
|
||||
const requiredFields = document.querySelectorAll('[required]');
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
const value = data[field.name];
|
||||
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||
this.showFieldError(field, 'This field is required');
|
||||
isValid = false;
|
||||
} else {
|
||||
this.clearFieldError(field);
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user authentication and balance
|
||||
*/
|
||||
async checkBalance() {
|
||||
const isAuthenticated = document.body.getAttribute('data-user-authenticated') === 'true';
|
||||
|
||||
if (!isAuthenticated) {
|
||||
this.showToast('Please log in to use this agent', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth/login/';
|
||||
}, 2000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current balance from wallet card
|
||||
const balanceElement = document.querySelector('[data-wallet-balance]');
|
||||
if (balanceElement) {
|
||||
const currentBalance = parseFloat(balanceElement.textContent.replace(/[^\d.]/g, ''));
|
||||
if (currentBalance < this.price) {
|
||||
this.showToast(`Insufficient balance. You need ${this.price} AED but have ${currentBalance} AED`, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format message for N8N based on agent configuration
|
||||
*/
|
||||
formatMessage(data) {
|
||||
// Create a descriptive message based on the agent and data
|
||||
let message = `Process ${this.agentSlug} request:\n\n`;
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (value && key !== 'csrfmiddlewaretoken') {
|
||||
const fieldLabel = this.getFieldLabel(key) || key.replace(/[_-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
message += `${fieldLabel}: ${value}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return message.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field label from DOM
|
||||
*/
|
||||
getFieldLabel(fieldName) {
|
||||
const field = document.querySelector(`[name="${fieldName}"]`);
|
||||
if (field) {
|
||||
const label = document.querySelector(`label[for="${field.id}"]`);
|
||||
if (label) {
|
||||
return label.textContent.replace('*', '').trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if form has file uploads
|
||||
*/
|
||||
hasFileUploads(data) {
|
||||
return Object.values(data).some(value => value instanceof File);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display processing status
|
||||
*/
|
||||
showProcessing() {
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
if (processingStatus) {
|
||||
processingStatus.style.display = 'block';
|
||||
processingStatus.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
if (resultsContainer) {
|
||||
resultsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Processing...';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide processing status
|
||||
*/
|
||||
hideProcessing() {
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
if (processingStatus) {
|
||||
processingStatus.style.display = 'none';
|
||||
}
|
||||
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = `🚀 Process with ${this.agentSlug.replace(/-/g, ' ')} (${this.price} AED)`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results
|
||||
*/
|
||||
displayResults(result) {
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const resultsContent = document.querySelector('.results-content');
|
||||
|
||||
if (!resultsContainer || !resultsContent) return;
|
||||
|
||||
// Clear previous results
|
||||
resultsContent.innerHTML = '';
|
||||
|
||||
// Create result content
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'workflow-result';
|
||||
|
||||
if (result.output) {
|
||||
// Create formatted output
|
||||
const outputDiv = document.createElement('div');
|
||||
outputDiv.className = 'result-output';
|
||||
|
||||
// Handle different output formats
|
||||
if (typeof result.output === 'string') {
|
||||
outputDiv.innerHTML = this.formatTextOutput(result.output);
|
||||
} else if (typeof result.output === 'object') {
|
||||
outputDiv.innerHTML = this.formatObjectOutput(result.output);
|
||||
} else {
|
||||
outputDiv.textContent = String(result.output);
|
||||
}
|
||||
|
||||
resultDiv.appendChild(outputDiv);
|
||||
}
|
||||
|
||||
// Add action buttons
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'result-actions';
|
||||
actionsDiv.innerHTML = `
|
||||
<button class="btn btn-secondary" onclick="workflowProcessor.copyResults()">
|
||||
📋 Copy Results
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="workflowProcessor.downloadResults()">
|
||||
💾 Download
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="workflowProcessor.newRequest()">
|
||||
🔄 New Request
|
||||
</button>
|
||||
`;
|
||||
|
||||
resultDiv.appendChild(actionsDiv);
|
||||
resultsContent.appendChild(resultDiv);
|
||||
|
||||
// Show results container
|
||||
resultsContainer.style.display = 'block';
|
||||
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
// Store results for actions
|
||||
this.lastResult = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format text output with proper styling
|
||||
*/
|
||||
formatTextOutput(text) {
|
||||
// Convert newlines to HTML breaks and preserve formatting
|
||||
return text
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/^(.*)/, '<p>$1')
|
||||
.replace(/(.*?)$/, '$1</p>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // Bold
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>'); // Italic
|
||||
}
|
||||
|
||||
/**
|
||||
* Format object output as structured data
|
||||
*/
|
||||
formatObjectOutput(obj) {
|
||||
if (obj.formatted_content) {
|
||||
return this.formatTextOutput(obj.formatted_content);
|
||||
}
|
||||
|
||||
let html = '<div class="structured-output">';
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value && key !== 'raw_data') {
|
||||
const label = key.replace(/[_-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
html += `<div class="output-item">`;
|
||||
html += `<strong>${label}:</strong> `;
|
||||
if (typeof value === 'string') {
|
||||
html += this.formatTextOutput(value);
|
||||
} else {
|
||||
html += String(value);
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy results to clipboard
|
||||
*/
|
||||
async copyResults() {
|
||||
if (!this.lastResult) return;
|
||||
|
||||
let textToCopy = '';
|
||||
if (typeof this.lastResult.output === 'string') {
|
||||
textToCopy = this.lastResult.output;
|
||||
} else if (typeof this.lastResult.output === 'object') {
|
||||
textToCopy = JSON.stringify(this.lastResult.output, null, 2);
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
this.showToast('Results copied to clipboard!', 'success');
|
||||
} catch (err) {
|
||||
this.showToast('Failed to copy to clipboard', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download results as text file
|
||||
*/
|
||||
downloadResults() {
|
||||
if (!this.lastResult) return;
|
||||
|
||||
let content = '';
|
||||
if (typeof this.lastResult.output === 'string') {
|
||||
content = this.lastResult.output;
|
||||
} else {
|
||||
content = JSON.stringify(this.lastResult.output, null, 2);
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${this.agentSlug}-result-${new Date().toISOString().slice(0, 10)}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
this.showToast('Results downloaded!', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset form for new request
|
||||
*/
|
||||
newRequest() {
|
||||
const form = document.getElementById('workflowForm');
|
||||
if (form) {
|
||||
form.reset();
|
||||
|
||||
// Clear file uploads
|
||||
document.querySelectorAll('.file-info').forEach(info => {
|
||||
info.style.display = 'none';
|
||||
});
|
||||
|
||||
// Reset radio cards
|
||||
document.querySelectorAll('.radio-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
}
|
||||
|
||||
// Hide results
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
if (resultsContainer) {
|
||||
resultsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Scroll to form
|
||||
form.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
this.showToast('Ready for new request', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
showError(message) {
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const resultsContent = document.querySelector('.results-content');
|
||||
|
||||
if (resultsContainer && resultsContent) {
|
||||
resultsContent.innerHTML = `
|
||||
<div class="error-message">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<div class="error-text">${message}</div>
|
||||
<button class="btn btn-primary" onclick="workflowProcessor.newRequest()">
|
||||
🔄 Try Again
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
resultsContainer.style.display = 'block';
|
||||
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show field error
|
||||
*/
|
||||
showFieldError(field, message) {
|
||||
this.clearFieldError(field);
|
||||
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'field-error';
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.id = `${field.name}_error`;
|
||||
|
||||
field.parentNode.appendChild(errorDiv);
|
||||
field.classList.add('error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear field error
|
||||
*/
|
||||
clearFieldError(field) {
|
||||
const existingError = document.getElementById(`${field.name}_error`);
|
||||
if (existingError) {
|
||||
existingError.remove();
|
||||
}
|
||||
field.classList.remove('error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update wallet balance display
|
||||
*/
|
||||
updateWalletBalance(newBalance) {
|
||||
if (newBalance !== undefined) {
|
||||
// Update all balance displays
|
||||
document.querySelectorAll('[data-wallet-balance]').forEach(element => {
|
||||
element.textContent = `${newBalance.toFixed(2)} AED`;
|
||||
});
|
||||
|
||||
// Update balance in navigation
|
||||
const headerBalance = document.querySelector('a[href="/wallet/"]');
|
||||
if (headerBalance) {
|
||||
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
|
||||
}
|
||||
|
||||
// Store current balance globally
|
||||
window.currentWalletBalance = newBalance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
showToast(message, type = 'info') {
|
||||
// Remove existing toasts
|
||||
document.querySelectorAll('.toast').forEach(toast => toast.remove());
|
||||
|
||||
// Create new toast
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Show toast
|
||||
setTimeout(() => toast.classList.add('show'), 100);
|
||||
|
||||
// Auto remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file input changes
|
||||
*/
|
||||
handleFileChange(event) {
|
||||
const input = event.target;
|
||||
const file = input.files[0];
|
||||
const container = input.closest('.file-upload-container');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const fileInfo = container.querySelector('.file-info');
|
||||
const uploadArea = container.querySelector('.file-upload-area');
|
||||
|
||||
if (file && fileInfo) {
|
||||
// Show file info
|
||||
const fileName = fileInfo.querySelector('.file-name');
|
||||
const fileSize = fileInfo.querySelector('.file-size');
|
||||
|
||||
if (fileName) fileName.textContent = file.name;
|
||||
if (fileSize) fileSize.textContent = this.formatFileSize(file.size);
|
||||
|
||||
fileInfo.style.display = 'block';
|
||||
uploadArea.classList.add('has-file');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup drag and drop for file uploads
|
||||
*/
|
||||
setupDragAndDrop(container, input) {
|
||||
const uploadArea = container.querySelector('.file-upload-area');
|
||||
if (!uploadArea) return;
|
||||
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('drag-over');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('drag-over');
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
input.files = files;
|
||||
this.handleFileChange({ target: input });
|
||||
}
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('click', () => {
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique session ID
|
||||
*/
|
||||
generateSessionId() {
|
||||
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSRF token from page
|
||||
*/
|
||||
getCsrfToken() {
|
||||
const token = document.querySelector('[name=csrfmiddlewaretoken]');
|
||||
return token ? token.value : '';
|
||||
}
|
||||
}
|
||||
|
||||
// Global utility functions
|
||||
function removeFile(fieldName) {
|
||||
const input = document.getElementById(fieldName);
|
||||
const container = input.closest('.file-upload-container');
|
||||
|
||||
if (input) input.value = '';
|
||||
|
||||
if (container) {
|
||||
const fileInfo = container.querySelector('.file-info');
|
||||
const uploadArea = container.querySelector('.file-upload-area');
|
||||
|
||||
if (fileInfo) fileInfo.style.display = 'none';
|
||||
if (uploadArea) uploadArea.classList.remove('has-file');
|
||||
}
|
||||
}
|
||||
|
||||
function selectRadio(name, value) {
|
||||
// Remove selection from all radio cards with this name
|
||||
document.querySelectorAll(`input[name="${name}"]`).forEach(radio => {
|
||||
radio.closest('.radio-card').classList.remove('selected');
|
||||
radio.checked = false;
|
||||
});
|
||||
|
||||
// Select the clicked radio
|
||||
const radio = document.querySelector(`input[name="${name}"][value="${value}"]`);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
radio.closest('.radio-card').classList.add('selected');
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Final test of the updated social ads processor
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def test_final_webhook():
|
||||
"""Test the final corrected webhook format"""
|
||||
|
||||
webhook_url = "http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d"
|
||||
|
||||
# Simulate the data that would come from the Django form
|
||||
form_data = {
|
||||
'user_id': 1,
|
||||
'description': 'Revolutionary AI-powered fitness tracker that monitors your health 24/7',
|
||||
'social_platform': 'instagram',
|
||||
'include_emoji': True,
|
||||
'language': 'English',
|
||||
'cost': 7.00
|
||||
}
|
||||
|
||||
# Format as the processor now does
|
||||
emoji_text = "Yes" if form_data['include_emoji'] else "No"
|
||||
platform_display = form_data['social_platform'].title()
|
||||
|
||||
message_text = f"""Create a social media advertisement with the following details:
|
||||
|
||||
Description: {form_data['description']}
|
||||
Include Emoji: {emoji_text}
|
||||
Social Media Platform: {platform_display}
|
||||
Language: {form_data['language']}
|
||||
|
||||
Please create an engaging, platform-optimized social media ad based on this information."""
|
||||
|
||||
payload = {
|
||||
'message': {
|
||||
'text': message_text
|
||||
},
|
||||
'sessionId': f'social_ad_{form_data["user_id"]}_{int(datetime.now().timestamp() * 1000)}'
|
||||
}
|
||||
|
||||
print("🎯 Final Webhook Test - Social Ads Generator")
|
||||
print("=" * 60)
|
||||
print(f"Webhook URL: {webhook_url}")
|
||||
print(f"\nForm Data: {json.dumps(form_data, indent=2)}")
|
||||
print(f"\nFormatted Payload:")
|
||||
print(json.dumps(payload, indent=2))
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
response = requests.post(webhook_url, json=payload, timeout=30)
|
||||
|
||||
print(f"✅ Status Code: {response.status_code}")
|
||||
print(f"✅ Response Headers: {dict(response.headers)}")
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
json_response = response.json()
|
||||
ad_content = json_response.get('output', '')
|
||||
|
||||
print(f"\n🎉 SUCCESS! Generated Ad Content:")
|
||||
print("-" * 40)
|
||||
print(ad_content)
|
||||
print("-" * 40)
|
||||
|
||||
print(f"\n📊 Response Analysis:")
|
||||
print(f"- Content Length: {len(ad_content)} characters")
|
||||
print(f"- Contains Emojis: {'Yes' if any(ord(char) > 127 for char in ad_content) else 'No'}")
|
||||
print(f"- Platform Optimized: Instagram format detected")
|
||||
|
||||
return True
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"❌ Invalid JSON response: {response.text}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ HTTP Error {response.status_code}: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Request failed: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_final_webhook()
|
||||
print("\n" + "=" * 60)
|
||||
if success:
|
||||
print("🎉 WEBHOOK TEST PASSED! The social ads generator is working correctly.")
|
||||
print("✅ The processor format has been updated to match N8N expectations.")
|
||||
print("✅ Ready for production use!")
|
||||
else:
|
||||
print("❌ WEBHOOK TEST FAILED! Check the error messages above.")
|
||||
print("=" * 60)
|
||||
@ -1,153 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Final test of the updated Five Whys processor
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def test_final_five_whys():
|
||||
"""Test the final corrected Five Whys webhook format"""
|
||||
|
||||
webhook_url = "https://quantumtaskai.app.n8n.cloud/webhook/5-whys-web"
|
||||
|
||||
# Simulate the data that would come from the Django form
|
||||
form_data = {
|
||||
'user_id': 'test-user-456',
|
||||
'problem_statement': 'Our e-commerce website has a high cart abandonment rate',
|
||||
'problem_category': 'customer',
|
||||
'context_information': 'Cart abandonment rate is 75%, industry average is 50%. Customers add items but leave before checkout.',
|
||||
'include_solutions': True,
|
||||
'cost': 8.00
|
||||
}
|
||||
|
||||
# Format as the processor now does
|
||||
message_text = f"""Perform a Five Whys root cause analysis with the following details:
|
||||
|
||||
Problem Statement: {form_data['problem_statement']}
|
||||
Problem Category: {form_data['problem_category']}
|
||||
Context Information: {form_data['context_information']}
|
||||
Include Solutions: {'Yes' if form_data['include_solutions'] else 'No'}
|
||||
|
||||
Please conduct a systematic Five Whys analysis to identify the root cause and provide actionable solutions."""
|
||||
|
||||
payload = {
|
||||
'message': {
|
||||
'text': message_text
|
||||
},
|
||||
'sessionId': f'five_whys_{int(datetime.now().timestamp() * 1000)}',
|
||||
'userId': form_data['user_id'],
|
||||
'agentId': '5',
|
||||
'problemStatement': form_data['problem_statement'],
|
||||
'problemCategory': form_data['problem_category']
|
||||
}
|
||||
|
||||
print("🔍 Final Five Whys Webhook Test")
|
||||
print("=" * 60)
|
||||
print(f"Webhook URL: {webhook_url}")
|
||||
print(f"\nForm Data: {json.dumps(form_data, indent=2)}")
|
||||
print(f"\nFormatted Payload:")
|
||||
print(json.dumps(payload, indent=2))
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
response = requests.post(webhook_url, json=payload, timeout=60)
|
||||
|
||||
print(f"✅ Status Code: {response.status_code}")
|
||||
print(f"✅ Response Headers: {dict(response.headers)}")
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
json_response = response.json()
|
||||
analysis_content = json_response.get('output', '')
|
||||
|
||||
print(f"\n🎉 SUCCESS! Generated Five Whys Analysis:")
|
||||
print("-" * 40)
|
||||
print(analysis_content)
|
||||
print("-" * 40)
|
||||
|
||||
print(f"\n📊 Response Analysis:")
|
||||
print(f"- Content Length: {len(analysis_content)} characters")
|
||||
print(f"- Contains 'Why': {'Yes' if 'Why' in analysis_content else 'No'}")
|
||||
print(f"- Contains 'Root Cause': {'Yes' if 'root cause' in analysis_content.lower() else 'No'}")
|
||||
print(f"- Contains 'Solution': {'Yes' if 'solution' in analysis_content.lower() else 'No'}")
|
||||
print(f"- Contains Problem Statement: {'Yes' if form_data['problem_statement'] in analysis_content else 'No'}")
|
||||
|
||||
return True
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"❌ Invalid JSON response: {response.text}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ HTTP Error {response.status_code}: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Request failed: {e}")
|
||||
return False
|
||||
|
||||
def test_different_problem():
|
||||
"""Test with a different type of problem"""
|
||||
webhook_url = "https://quantumtaskai.app.n8n.cloud/webhook/5-whys-web"
|
||||
|
||||
# Test with a technical problem
|
||||
message_text = """Perform a Five Whys root cause analysis with the following details:
|
||||
|
||||
Problem Statement: Server downtime incidents are increasing
|
||||
Problem Category: technical
|
||||
Context Information: 3 incidents this month, each lasting 2+ hours. Users unable to access application.
|
||||
Include Solutions: Yes
|
||||
|
||||
Please conduct a systematic Five Whys analysis to identify the root cause and provide actionable solutions."""
|
||||
|
||||
payload = {
|
||||
'message': {
|
||||
'text': message_text
|
||||
},
|
||||
'sessionId': f'five_whys_{int(datetime.now().timestamp() * 1000)}',
|
||||
'userId': 'test-user-789',
|
||||
'agentId': '5',
|
||||
'problemStatement': 'Server downtime incidents are increasing',
|
||||
'problemCategory': 'technical'
|
||||
}
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🔍 Testing Different Problem Type - Technical")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
response = requests.post(webhook_url, json=payload, timeout=60)
|
||||
|
||||
if response.status_code == 200:
|
||||
json_response = response.json()
|
||||
analysis_content = json_response.get('output', '')
|
||||
print(f"✅ Technical problem analysis generated ({len(analysis_content)} chars)")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Failed with status {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Request failed: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Testing Updated Five Whys Webhook")
|
||||
print("=" * 60)
|
||||
|
||||
# Test main scenario
|
||||
success1 = test_final_five_whys()
|
||||
|
||||
# Test different problem type
|
||||
success2 = test_different_problem()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if success1 and success2:
|
||||
print("🎉 FIVE WHYS WEBHOOK TESTS PASSED!")
|
||||
print("✅ The processor format has been updated to match N8N expectations.")
|
||||
print("✅ Works with different problem types and categories.")
|
||||
print("✅ Ready for production use!")
|
||||
else:
|
||||
print("❌ FIVE WHYS WEBHOOK TESTS FAILED!")
|
||||
print("Check the error messages above.")
|
||||
print("=" * 60)
|
||||
@ -1,144 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test the Five Whys webhook to understand expected format
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def test_current_format():
|
||||
"""Test with our current format"""
|
||||
webhook_url = "https://quantumtaskai.app.n8n.cloud/webhook/5-whys-web"
|
||||
|
||||
# Current format we're sending
|
||||
current_payload = {
|
||||
'user_id': 'test-user-123',
|
||||
'problem_statement': 'Our customer support response time is too slow',
|
||||
'problem_category': 'operational',
|
||||
'context_information': 'Average response time is 4 hours, customers complaining',
|
||||
'include_solutions': True,
|
||||
'agent_type': 'five_whys_analyzer',
|
||||
'cost': 8.00,
|
||||
'timestamp': datetime.now().isoformat() + 'Z'
|
||||
}
|
||||
|
||||
print("🔍 Testing Five Whys Webhook - Current Format")
|
||||
print("=" * 60)
|
||||
print(f"Webhook URL: {webhook_url}")
|
||||
print(f"Current Payload: {json.dumps(current_payload, indent=2)}")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
response = requests.post(webhook_url, json=current_payload, timeout=30)
|
||||
print(f"Status Code: {response.status_code}")
|
||||
print(f"Response: {response.text[:500]}...")
|
||||
|
||||
if response.status_code == 200:
|
||||
print("✅ Current format working!")
|
||||
return True
|
||||
else:
|
||||
print("❌ Current format not working")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Request failed: {e}")
|
||||
return False
|
||||
|
||||
def test_message_format():
|
||||
"""Test with message format like other agents"""
|
||||
webhook_url = "https://quantumtaskai.app.n8n.cloud/webhook/5-whys-web"
|
||||
|
||||
# Format similar to social ads and job posting
|
||||
problem_statement = "Our customer support response time is too slow"
|
||||
problem_category = "operational"
|
||||
context_information = "Average response time is 4 hours, customers complaining"
|
||||
include_solutions = True
|
||||
|
||||
message_text = f"""Perform a Five Whys root cause analysis with the following details:
|
||||
|
||||
Problem Statement: {problem_statement}
|
||||
Problem Category: {problem_category}
|
||||
Context Information: {context_information}
|
||||
Include Solutions: {'Yes' if include_solutions else 'No'}
|
||||
|
||||
Please conduct a systematic Five Whys analysis to identify the root cause and provide actionable solutions."""
|
||||
|
||||
message_payload = {
|
||||
'message': {
|
||||
'text': message_text
|
||||
},
|
||||
'sessionId': f'five_whys_{int(datetime.now().timestamp() * 1000)}',
|
||||
'userId': 'test-user-123',
|
||||
'agentId': '5', # Five Whys agent ID
|
||||
'problemStatement': problem_statement,
|
||||
'problemCategory': problem_category
|
||||
}
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🔍 Testing Five Whys Webhook - Message Format")
|
||||
print("=" * 60)
|
||||
print(f"Message Payload: {json.dumps(message_payload, indent=2)}")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
response = requests.post(webhook_url, json=message_payload, timeout=30)
|
||||
print(f"Status Code: {response.status_code}")
|
||||
print(f"Response: {response.text[:500]}...")
|
||||
|
||||
if response.status_code == 200:
|
||||
print("✅ Message format working!")
|
||||
return True
|
||||
else:
|
||||
print("❌ Message format not working")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Request failed: {e}")
|
||||
return False
|
||||
|
||||
def test_simple_connectivity():
|
||||
"""Test basic connectivity"""
|
||||
webhook_url = "https://quantumtaskai.app.n8n.cloud/webhook/5-whys-web"
|
||||
|
||||
print("🔗 Testing Basic Connectivity")
|
||||
print("-" * 30)
|
||||
|
||||
try:
|
||||
response = requests.post(webhook_url, json={'test': 'ping'}, timeout=10)
|
||||
print(f"✅ Connectivity: OK (Status: {response.status_code})")
|
||||
print(f"Response: {response.text[:200]}...")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Connectivity: FAILED ({e})")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Testing Five Whys Webhook Formats")
|
||||
print("=" * 60)
|
||||
|
||||
# Test basic connectivity
|
||||
if test_simple_connectivity():
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
# Test current format
|
||||
current_works = test_current_format()
|
||||
|
||||
# Test message format
|
||||
message_works = test_message_format()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 RESULTS SUMMARY:")
|
||||
print(f"Current Format: {'✅ WORKS' if current_works else '❌ FAILED'}")
|
||||
print(f"Message Format: {'✅ WORKS' if message_works else '❌ FAILED'}")
|
||||
|
||||
if current_works:
|
||||
print("\n💡 Current format is working - no changes needed!")
|
||||
elif message_works:
|
||||
print("\n💡 Need to update to message format!")
|
||||
else:
|
||||
print("\n⚠️ Both formats failed - need to investigate webhook structure")
|
||||
|
||||
else:
|
||||
print("\n❌ Cannot test formats - webhook not reachable")
|
||||
|
||||
print("=" * 60)
|
||||
@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test the job posting webhook with updated format
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def test_job_posting_webhook():
|
||||
"""Test the job posting webhook connectivity"""
|
||||
|
||||
webhook_url = "http://localhost:5678/webhook/43f84411-eaaa-488c-9b1f-856e90d0aaf6"
|
||||
|
||||
# Simulate the data that would come from the Django form
|
||||
form_data = {
|
||||
'user_id': 'test-user-123',
|
||||
'job_title': 'Senior Software Developer',
|
||||
'company_name': 'Quantum Tasks AI Technologies',
|
||||
'industry': 'technology',
|
||||
'job_type': 'full-time',
|
||||
'experience_level': 'senior',
|
||||
'location': 'Remote',
|
||||
'salary_range': '$80,000 - $120,000',
|
||||
'key_responsibilities': 'Develop and maintain web applications, lead technical projects, mentor junior developers',
|
||||
'required_skills': 'Python, Django, React, PostgreSQL, AWS, Git',
|
||||
'company_culture': 'Innovative, collaborative, work-life balance focused',
|
||||
'cost': 10.00
|
||||
}
|
||||
|
||||
# Format as the processor now does
|
||||
message_text = f"""Create a professional job posting with the following details:
|
||||
|
||||
Job Title: {form_data['job_title']}
|
||||
Company: {form_data['company_name']}
|
||||
Description: {form_data['key_responsibilities']}
|
||||
Seniority Level: {form_data['experience_level']}
|
||||
Contract Type: {form_data['job_type']}
|
||||
Location: {form_data['location']}
|
||||
Language: English
|
||||
Required Skills: {form_data['required_skills']}
|
||||
Salary Range: {form_data['salary_range']}
|
||||
Company Culture: {form_data['company_culture']}
|
||||
|
||||
Please create a complete, engaging job posting based on this information."""
|
||||
|
||||
payload = {
|
||||
'message': {
|
||||
'text': message_text
|
||||
},
|
||||
'sessionId': f'job_posting_{int(datetime.now().timestamp() * 1000)}',
|
||||
'userId': form_data['user_id'],
|
||||
'agentId': '9',
|
||||
'jobTitle': form_data['job_title'],
|
||||
'companyName': form_data['company_name']
|
||||
}
|
||||
|
||||
print("💼 Job Posting Webhook Test")
|
||||
print("=" * 60)
|
||||
print(f"Webhook URL: {webhook_url}")
|
||||
print(f"\nForm Data: {json.dumps(form_data, indent=2)}")
|
||||
print(f"\nFormatted Payload:")
|
||||
print(json.dumps(payload, indent=2))
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
response = requests.post(webhook_url, json=payload, timeout=60)
|
||||
|
||||
print(f"✅ Status Code: {response.status_code}")
|
||||
print(f"✅ Response Headers: {dict(response.headers)}")
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
json_response = response.json()
|
||||
job_content = json_response.get('output', '')
|
||||
|
||||
print(f"\n🎉 SUCCESS! Generated Job Posting:")
|
||||
print("-" * 40)
|
||||
print(job_content)
|
||||
print("-" * 40)
|
||||
|
||||
print(f"\n📊 Response Analysis:")
|
||||
print(f"- Content Length: {len(job_content)} characters")
|
||||
print(f"- Contains Company Name: {'Yes' if form_data['company_name'] in job_content else 'No'}")
|
||||
print(f"- Contains Job Title: {'Yes' if form_data['job_title'] in job_content else 'No'}")
|
||||
print(f"- Professional Format: Job posting format detected")
|
||||
|
||||
return True
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"❌ Invalid JSON response: {response.text}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ HTTP Error {response.status_code}: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Request failed: {e}")
|
||||
return False
|
||||
|
||||
def test_simple_connectivity():
|
||||
"""Simple test to check if webhook endpoint exists"""
|
||||
webhook_url = "http://localhost:5678/webhook/43f84411-eaaa-488c-9b1f-856e90d0aaf6"
|
||||
|
||||
print("🔗 Simple Connectivity Test")
|
||||
print("-" * 30)
|
||||
|
||||
try:
|
||||
# Simple POST with minimal data to test connectivity
|
||||
response = requests.post(webhook_url, json={'test': 'ping'}, timeout=10)
|
||||
print(f"✅ Connectivity: OK (Status: {response.status_code})")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Connectivity: FAILED ({e})")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Testing Job Posting Webhook")
|
||||
print("=" * 60)
|
||||
|
||||
# Test connectivity first
|
||||
if test_simple_connectivity():
|
||||
print("\n" + "=" * 60)
|
||||
success = test_job_posting_webhook()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if success:
|
||||
print("🎉 JOB POSTING WEBHOOK TEST PASSED!")
|
||||
print("✅ The processor format has been updated to match N8N expectations.")
|
||||
print("✅ Ready for production use!")
|
||||
else:
|
||||
print("❌ JOB POSTING WEBHOOK TEST FAILED!")
|
||||
print("Check the error messages above.")
|
||||
else:
|
||||
print("\n❌ Cannot proceed with full test - webhook endpoint not reachable")
|
||||
|
||||
print("=" * 60)
|
||||
@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# Add the project root to Python path
|
||||
sys.path.insert(0, '/home/amit/projects/quantumtaskai_django')
|
||||
|
||||
# Set Django settings
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'quantumtaskai_hub.settings')
|
||||
django.setup()
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from agent_base.models import BaseAgent
|
||||
from weather_reporter.models import WeatherReporterRequest, WeatherReporterResponse
|
||||
from weather_reporter.processor import WeatherReporterProcessor
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
def test_weather_agent():
|
||||
print("🧪 Testing Weather Reporter Agent System")
|
||||
print("=" * 50)
|
||||
|
||||
# Get or create a test user
|
||||
user, created = User.objects.get_or_create(
|
||||
email='test@example.com',
|
||||
defaults={
|
||||
'username': 'testuser',
|
||||
'wallet_balance': 100.00 # Give them some balance
|
||||
}
|
||||
)
|
||||
if created:
|
||||
print(f"✅ Created test user: {user.email} with balance: {user.wallet_balance} AED")
|
||||
else:
|
||||
print(f"✅ Using existing test user: {user.email} with balance: {user.wallet_balance} AED")
|
||||
|
||||
# Get the weather agent
|
||||
try:
|
||||
agent = BaseAgent.objects.get(slug='weather-reporter')
|
||||
print(f"✅ Found Weather Reporter agent: {agent.name} (Price: {agent.price} AED)")
|
||||
except BaseAgent.DoesNotExist:
|
||||
print("❌ Weather Reporter agent not found in database")
|
||||
return False
|
||||
|
||||
# Test the processor directly (without API key for now)
|
||||
processor = WeatherReporterProcessor()
|
||||
print(f"✅ Created Weather Reporter processor: {processor.agent_slug}")
|
||||
|
||||
# Create a test request
|
||||
request_obj = WeatherReporterRequest.objects.create(
|
||||
user=user,
|
||||
agent=agent,
|
||||
cost=agent.price,
|
||||
location='London',
|
||||
report_type='current'
|
||||
)
|
||||
print(f"✅ Created test request: {request_obj.id}")
|
||||
|
||||
# Test webhook format detection
|
||||
from agent_base.processors import WebhookFormatDetector
|
||||
print("\n🔍 Testing Webhook Format Detector:")
|
||||
print("This tests the webhook format detection utility...")
|
||||
# Note: We won't test with real URLs to avoid network calls
|
||||
print("✅ WebhookFormatDetector class loaded successfully")
|
||||
|
||||
print("\n🎉 Weather Reporter Agent System Test Complete!")
|
||||
print("=" * 50)
|
||||
print("✅ Agent Base Framework: Working")
|
||||
print("✅ Weather Reporter Agent: Created")
|
||||
print("✅ Models & Database: Working")
|
||||
print("✅ Processor Classes: Working")
|
||||
print("✅ Management Commands: Working")
|
||||
print("✅ Template System: Working")
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_weather_agent()
|
||||
@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Simple webhook connectivity test for social ads generator
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def test_social_ads_webhook():
|
||||
"""Test the social ads webhook connectivity"""
|
||||
|
||||
webhook_url = "http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d"
|
||||
|
||||
# Test payload using the correct format expected by N8N
|
||||
message_text = """Create a social media advertisement with the following details:
|
||||
|
||||
Description: Amazing smartphone with cutting-edge features
|
||||
Include Emoji: Yes
|
||||
Social Media Platform: Facebook
|
||||
Language: English
|
||||
|
||||
Please create an engaging, platform-optimized social media ad based on this information."""
|
||||
|
||||
test_payload = {
|
||||
'message': {
|
||||
'text': message_text
|
||||
},
|
||||
'sessionId': f'social_ad_test_{int(datetime.now().timestamp() * 1000)}'
|
||||
}
|
||||
|
||||
print(f"Testing webhook: {webhook_url}")
|
||||
print(f"Payload: {json.dumps(test_payload, indent=2)}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
# Send POST request to webhook
|
||||
response = requests.post(
|
||||
webhook_url,
|
||||
json=test_payload,
|
||||
timeout=30,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
print(f"✅ Response Status: {response.status_code}")
|
||||
print(f"✅ Response Headers: {dict(response.headers)}")
|
||||
print(f"✅ Response Content: {response.text}")
|
||||
|
||||
if response.status_code == 200:
|
||||
print("\n🎉 SUCCESS: Webhook is reachable and responding!")
|
||||
|
||||
# Try to parse JSON response
|
||||
try:
|
||||
json_response = response.json()
|
||||
print(f"📄 JSON Response: {json.dumps(json_response, indent=2)}")
|
||||
except:
|
||||
print("📄 Response is not JSON format")
|
||||
|
||||
else:
|
||||
print(f"\n⚠️ WARNING: Webhook returned status code {response.status_code}")
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"\n❌ CONNECTION ERROR: Cannot reach webhook")
|
||||
print(f"Details: {e}")
|
||||
print("\nPossible causes:")
|
||||
print("1. N8N server is not running on localhost:5678")
|
||||
print("2. Webhook ID is incorrect")
|
||||
print("3. Firewall blocking the connection")
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
print(f"\n⏰ TIMEOUT ERROR: Webhook took too long to respond")
|
||||
print(f"Details: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ UNEXPECTED ERROR: {e}")
|
||||
|
||||
def test_webhook_simple():
|
||||
"""Simple ping test to check if webhook endpoint exists"""
|
||||
|
||||
webhook_url = "http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d"
|
||||
|
||||
print(f"Simple connectivity test for: {webhook_url}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
# Simple GET request to see if endpoint exists
|
||||
response = requests.get(webhook_url, timeout=10)
|
||||
print(f"GET Response Status: {response.status_code}")
|
||||
print(f"GET Response: {response.text[:200]}...")
|
||||
|
||||
except Exception as e:
|
||||
print(f"GET request failed: {e}")
|
||||
|
||||
try:
|
||||
# Simple POST with minimal data
|
||||
response = requests.post(webhook_url, json={'test': 'ping'}, timeout=10)
|
||||
print(f"POST Response Status: {response.status_code}")
|
||||
print(f"POST Response: {response.text[:200]}...")
|
||||
|
||||
except Exception as e:
|
||||
print(f"POST request failed: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Testing Social Ads Webhook Connectivity")
|
||||
print("=" * 60)
|
||||
|
||||
# Run simple test first
|
||||
test_webhook_simple()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
# Run full test
|
||||
test_social_ads_webhook()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test completed!")
|
||||
@ -1,969 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Social Ads Generator - NetCop AI Hub</title>
|
||||
<style>
|
||||
/* Agent Frontend Template - Common Components CSS */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #000000;
|
||||
--surface: #ffffff;
|
||||
--surface-variant: #f8fafc;
|
||||
--background: #f3f4f6;
|
||||
--outline: #e4e7eb;
|
||||
--outline-variant: #e1e4e7;
|
||||
--on-surface: #1a1a1a;
|
||||
--on-surface-variant: #6b7280;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--on-surface);
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.agent-container {
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-lg);
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
.agent-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.agent-grid {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.agent-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--on-surface);
|
||||
margin: 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.agent-subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--on-surface-variant);
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
/* Widgets */
|
||||
.agent-widget {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
border: 1px solid var(--outline-variant);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--outline-variant);
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.widget-icon {
|
||||
font-size: 20px;
|
||||
padding: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-variant);
|
||||
}
|
||||
|
||||
.widget-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Widget Sizes */
|
||||
.widget-large {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.widget-small {
|
||||
flex: 0 0 280px;
|
||||
}
|
||||
|
||||
.widget-wide {
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Wallet Card */
|
||||
.wallet-card {
|
||||
background: linear-gradient(135deg, #000000 0%, #333333 100%);
|
||||
color: white;
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
border: none;
|
||||
box-shadow: var(--shadow-md);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.wallet-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
transform: translate(30px, -30px);
|
||||
}
|
||||
|
||||
.wallet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.wallet-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.wallet-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.balance-display {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wallet-topup-btn {
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.wallet-topup-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Processing Status */
|
||||
.processing-status {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--outline-variant);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.processing-status.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--spacing-md);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Quick Agent Access Panel */
|
||||
.quick-agent-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--on-surface);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-agent-toggle:hover {
|
||||
background: var(--surface-variant);
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.quick-agent-toggle.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.quick-agent-toggle.active .toggle-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.quick-agents-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: min(400px, 90vw);
|
||||
height: 100vh;
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--outline);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 1000;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.quick-agents-panel.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.quick-agents-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--outline-variant);
|
||||
background: var(--surface-variant);
|
||||
}
|
||||
|
||||
.quick-agents-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.close-panel {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: var(--on-surface-variant);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-panel:hover {
|
||||
background: var(--surface);
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.quick-agents-grid {
|
||||
padding: var(--spacing-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.quick-agent-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid var(--outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none;
|
||||
color: var(--on-surface);
|
||||
transition: all 0.2s ease;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.quick-agent-card:hover {
|
||||
background: var(--surface-variant);
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.agent-icon {
|
||||
font-size: 24px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--surface-variant);
|
||||
border-radius: var(--radius-md);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.agent-info h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.agent-info p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--on-surface-variant);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.agent-price {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.quick-agents-footer {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-top: 1px solid var(--outline-variant);
|
||||
background: var(--surface-variant);
|
||||
}
|
||||
|
||||
.view-all-agents {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.view-all-agents:hover {
|
||||
background: #333333;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.quick-agents-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-agents-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Placeholder Sections */
|
||||
.placeholder-section {
|
||||
background: var(--surface);
|
||||
border: 2px dashed var(--outline);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
text-align: center;
|
||||
color: var(--on-surface-variant);
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.placeholder-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.placeholder-example {
|
||||
background: var(--surface-variant);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-sm);
|
||||
margin-top: var(--spacing-md);
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
/* Info List - For How It Works */
|
||||
.info-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
counter-reset: step-counter;
|
||||
}
|
||||
|
||||
.info-list li {
|
||||
padding: var(--spacing-sm) 0;
|
||||
color: var(--on-surface-variant);
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--outline-variant);
|
||||
position: relative;
|
||||
padding-left: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.info-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-list li::before {
|
||||
content: counter(step-counter);
|
||||
counter-increment: step-counter;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: var(--spacing-sm);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--outline);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-color: var(--success);
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-color: var(--error);
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.agent-container {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.agent-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.agent-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.agent-grid {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.widget-large,
|
||||
.widget-small {
|
||||
min-width: auto;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.quick-agents-panel {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="agent-container">
|
||||
<!-- Agent Header Component -->
|
||||
<div class="agent-header">
|
||||
<div>
|
||||
<h1 class="agent-title">Social Ads Generator</h1>
|
||||
<p class="agent-subtitle">Create compelling social media advertisements with AI-powered content generation</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<!-- Wallet Card Component -->
|
||||
<div class="wallet-card widget-small">
|
||||
<div class="wallet-header">
|
||||
<h3 class="wallet-title">Your Wallet</h3>
|
||||
<div class="wallet-icon">💳</div>
|
||||
</div>
|
||||
<div class="balance-display">
|
||||
<div class="balance-amount">
|
||||
<span id="walletBalance">25.50</span> AED
|
||||
</div>
|
||||
<div class="balance-label">Available Balance</div>
|
||||
</div>
|
||||
<button type="button" class="wallet-topup-btn" onclick="showToast('Top-up feature demo!', 'success')">
|
||||
💳 Top Up Wallet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Agent Access Panel Overlay -->
|
||||
<div class="quick-agents-overlay" id="quickAgentsOverlay" onclick="closeQuickAgents()"></div>
|
||||
|
||||
<!-- Quick Agent Access Panel -->
|
||||
<div class="quick-agents-panel" id="quickAgentsPanel">
|
||||
<div class="quick-agents-header">
|
||||
<h3>Quick Access</h3>
|
||||
<button class="close-panel" onclick="closeQuickAgents()" aria-label="Close panel">×</button>
|
||||
</div>
|
||||
<div class="quick-agents-grid">
|
||||
<!-- Sample Agent Cards -->
|
||||
<a href="#" class="quick-agent-card">
|
||||
<div class="agent-icon">🌤️</div>
|
||||
<div class="agent-info">
|
||||
<h4>Weather Reporter</h4>
|
||||
<p>Get real-time weather data</p>
|
||||
<span class="agent-price">2.00 AED</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" class="quick-agent-card">
|
||||
<div class="agent-icon">📊</div>
|
||||
<div class="agent-info">
|
||||
<h4>Data Analyzer</h4>
|
||||
<p>AI-powered data analysis</p>
|
||||
<span class="agent-price">5.00 AED</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" class="quick-agent-card">
|
||||
<div class="agent-icon">💼</div>
|
||||
<div class="agent-info">
|
||||
<h4>Job Posting Generator</h4>
|
||||
<p>Create professional job postings</p>
|
||||
<span class="agent-price">4.00 AED</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="quick-agents-footer">
|
||||
<a href="#" class="view-all-agents">View All Agents</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Grid - SOCIAL ADS GENERATOR SPECIFIC CONTENT -->
|
||||
<div class="agent-grid">
|
||||
<div class="placeholder-section widget-large" style="flex: 1; margin-right: var(--spacing-lg);">
|
||||
<div class="placeholder-title">📱 Social Ads Generator Form</div>
|
||||
<div class="placeholder-description">
|
||||
This section would contain the Social Ads Generator form with inputs for business details, platform selection, target audience, and ad preferences.
|
||||
</div>
|
||||
<div class="placeholder-example">
|
||||
Examples: Business name input, platform radio buttons (Facebook, Instagram, Twitter), ad format selection, target audience textarea, tone checkboxes, etc.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- How It Works Widget - Common Pattern -->
|
||||
<div class="agent-widget widget-small" style="min-width: min(280px, 100%); max-width: min(280px, 100%); margin-left: auto;">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">
|
||||
<span class="widget-icon">ℹ️</span>
|
||||
How It Works
|
||||
</h3>
|
||||
</div>
|
||||
<div class="widget-content">
|
||||
<ol class="info-list">
|
||||
<li>Enter business and product details</li>
|
||||
<li>Choose platform and ad format</li>
|
||||
<li>Define target audience and tone</li>
|
||||
<li>Get optimized ad content and copy</li>
|
||||
</ol>
|
||||
|
||||
<!-- Quick Agents Toggle Button -->
|
||||
<button class="quick-agent-toggle" onclick="toggleQuickAgents()"
|
||||
title="Quick access to other agents"
|
||||
aria-label="Open quick access panel for other AI agents"
|
||||
aria-expanded="false"
|
||||
aria-controls="quickAgentsPanel"
|
||||
style="margin-top: var(--spacing-md);">
|
||||
<span class="toggle-icon" aria-hidden="true">🚀</span>
|
||||
<span class="toggle-text">Explore Other Agents</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Status Component -->
|
||||
<div class="agent-grid">
|
||||
<div id="processingStatus" class="agent-widget widget-wide processing-status">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">
|
||||
<span class="widget-icon">⏳</span>
|
||||
Processing Status
|
||||
</h3>
|
||||
</div>
|
||||
<div class="widget-content" style="text-align: center;">
|
||||
<div class="status-icon">⏳</div>
|
||||
<div class="status-title">Creating your social media ad...</div>
|
||||
<div class="status-text" id="statusText">Please wait while we generate compelling ad content...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section - SOCIAL ADS GENERATOR SPECIFIC RESULTS -->
|
||||
<div class="agent-grid">
|
||||
<div class="placeholder-section widget-wide">
|
||||
<div class="placeholder-title">📱 Generated Social Ad Results</div>
|
||||
<div class="placeholder-description">
|
||||
This section would display the generated social media ad content with platform-specific formatting, headlines, copy, and optimization suggestions.
|
||||
</div>
|
||||
<div class="placeholder-example">
|
||||
Examples: Ad headline, body copy, call-to-action button text, hashtag suggestions, platform optimization tips, copy/download buttons, etc.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Demo Controls -->
|
||||
<div class="agent-grid" style="margin-top: var(--spacing-xl); border-top: 1px solid var(--outline-variant); padding-top: var(--spacing-lg);">
|
||||
<div class="agent-widget widget-wide">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">
|
||||
<span class="widget-icon">🧪</span>
|
||||
Demo Controls
|
||||
</h3>
|
||||
</div>
|
||||
<div class="widget-content">
|
||||
<p style="margin-bottom: var(--spacing-md); color: var(--on-surface-variant);">
|
||||
Test the common components and interactions:
|
||||
</p>
|
||||
<div style="display: flex; gap: var(--spacing-md); flex-wrap: wrap;">
|
||||
<button onclick="showProcessing()" style="padding: var(--spacing-sm) var(--spacing-md); background: var(--primary); color: white; border: none; border-radius: var(--radius-sm); cursor: pointer;">
|
||||
Show Processing
|
||||
</button>
|
||||
<button onclick="hideProcessing()" style="padding: var(--spacing-sm) var(--spacing-md); background: var(--surface); color: var(--on-surface); border: 1px solid var(--outline); border-radius: var(--radius-sm); cursor: pointer;">
|
||||
Hide Processing
|
||||
</button>
|
||||
<button onclick="showToast('Success message!', 'success')" style="padding: var(--spacing-sm) var(--spacing-md); background: var(--success); color: white; border: none; border-radius: var(--radius-sm); cursor: pointer;">
|
||||
Success Toast
|
||||
</button>
|
||||
<button onclick="showToast('Error message!', 'error')" style="padding: var(--spacing-sm) var(--spacing-md); background: var(--error); color: white; border: none; border-radius: var(--radius-sm); cursor: pointer;">
|
||||
Error Toast
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Agent Frontend Template - Common JavaScript Utilities
|
||||
|
||||
// Quick Agent Access Panel Functions
|
||||
function toggleQuickAgents() {
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
const toggle = document.querySelector('.quick-agent-toggle');
|
||||
|
||||
const isActive = panel.classList.contains('active');
|
||||
|
||||
if (isActive) {
|
||||
// Close panel
|
||||
panel.classList.remove('active');
|
||||
overlay.classList.remove('active');
|
||||
toggle.classList.remove('active');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
overlay.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = 'auto';
|
||||
} else {
|
||||
// Open panel
|
||||
panel.classList.add('active');
|
||||
overlay.classList.add('active');
|
||||
toggle.classList.add('active');
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
panel.setAttribute('aria-hidden', 'false');
|
||||
overlay.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function closeQuickAgents() {
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
const toggle = document.querySelector('.quick-agent-toggle');
|
||||
|
||||
if (panel && overlay && toggle) {
|
||||
panel.classList.remove('active');
|
||||
overlay.classList.remove('active');
|
||||
toggle.classList.remove('active');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
overlay.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
// Toast Notification Function
|
||||
function showToast(message, type = 'info') {
|
||||
// Remove existing toasts
|
||||
document.querySelectorAll('.toast').forEach(toast => toast.remove());
|
||||
|
||||
// Create new toast
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Show toast with animation
|
||||
setTimeout(() => toast.classList.add('show'), 100);
|
||||
|
||||
// Auto remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Processing Status Functions
|
||||
function showProcessing() {
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
processingStatus.style.display = 'block';
|
||||
processingStatus.classList.add('active');
|
||||
showToast('Processing started...', 'success');
|
||||
}
|
||||
|
||||
function hideProcessing() {
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
processingStatus.style.display = 'none';
|
||||
processingStatus.classList.remove('active');
|
||||
showToast('Processing stopped...', 'success');
|
||||
}
|
||||
|
||||
// Wallet Balance Update Function
|
||||
function updateWalletBalance(newBalance) {
|
||||
if (newBalance !== undefined) {
|
||||
const walletBalance = document.getElementById('walletBalance');
|
||||
if (walletBalance) {
|
||||
walletBalance.textContent = newBalance.toFixed(2);
|
||||
}
|
||||
showToast(`Wallet updated: ${newBalance.toFixed(2)} AED`, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Copy to Clipboard Utility
|
||||
function copyToClipboard(text, successMessage = 'Copied to clipboard!') {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast(`📋 ${successMessage}`, 'success');
|
||||
}).catch(() => {
|
||||
showToast('Failed to copy to clipboard', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Download as File Utility
|
||||
function 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);
|
||||
showToast(`💾 ${successMessage}`, 'success');
|
||||
}
|
||||
|
||||
// Close panel on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeQuickAgents();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize accessibility features
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set initial ARIA states
|
||||
const quickAgentsButton = document.querySelector('.quick-agent-toggle');
|
||||
if (quickAgentsButton) {
|
||||
quickAgentsButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
const panel = document.getElementById('quickAgentsPanel');
|
||||
const overlay = document.getElementById('quickAgentsOverlay');
|
||||
if (panel) panel.setAttribute('aria-hidden', 'true');
|
||||
if (overlay) overlay.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Show welcome message
|
||||
setTimeout(() => {
|
||||
showToast('Social Ads Generator template loaded!', 'success');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Placeholder for agent-specific JavaScript
|
||||
// ========================================
|
||||
//
|
||||
// Social Ads Generator specific functions would go here:
|
||||
// - Form validation for business details
|
||||
// - Platform selection handling
|
||||
// - Target audience processing
|
||||
// - Ad generation and display
|
||||
// - Copy/download functionality for ads
|
||||
//
|
||||
// Example structure:
|
||||
// function validateSocialAdsForm() { ... }
|
||||
// function submitAdRequest() { ... }
|
||||
// function displayGeneratedAd() { ... }
|
||||
//
|
||||
// ========================================
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user