mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 10:12:58 +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