🗑️ Remove workflows app completely and streamline to agents-only

BREAKING CHANGE: Complete removal of legacy workflows system

- Remove entire workflows/ directory and all related files
- Update Django settings to remove workflows from INSTALLED_APPS
- Fix all URL references from workflows:marketplace to agents:marketplace
- Update core views to use agents.models.Agent instead of config files
- Fix agent detail template component includes (workflows/components → components)
- Update documentation to reflect database-driven agents system only
- Remove N8N workflow management script (no longer needed)

Template fixes:
- Agent header, quick agents panel, processing status, results container
- All error pages (403, 404, 500) now point to agents marketplace
- Base template navigation and footer updated
- Wallet pages redirect to agents marketplace

Core changes:
- Homepage uses Agent.objects instead of get_all_agents()
- Health check counts active agents from database
- All template references updated to use components/ path

Result: Clean, streamlined agents-only system with no legacy code

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2025-07-31 23:27:09 +05:30
parent c368bb55de
commit 657712fed8
38 changed files with 107 additions and 3631 deletions

108
CLAUDE.md
View File

@ -8,7 +8,7 @@ Quantum Tasks AI is a Django-based AI agent marketplace platform. Users can purc
**Key Architecture:** **Key Architecture:**
- **Django Framework**: Main web application using Django 5.2.4 - **Django Framework**: Main web application using Django 5.2.4
- **Agent System**: Unified workflows app handling all AI agents via N8N webhooks - **Agent System**: Database-driven agents app with marketplace and N8N webhook execution
- **Authentication**: Custom user model with email verification - **Authentication**: Custom user model with email verification
- **Payments**: Stripe integration with wallet system - **Payments**: Stripe integration with wallet system
- **Database**: SQLite for development, PostgreSQL for production (Railway) - **Database**: SQLite for development, PostgreSQL for production (Railway)
@ -59,7 +59,7 @@ pytest
# Run specific app tests # Run specific app tests
python manage.py test authentication python manage.py test authentication
python manage.py test workflows python manage.py test agents
python manage.py test wallet python manage.py test wallet
# Custom test scripts # Custom test scripts
@ -96,22 +96,22 @@ gunicorn netcop_hub.wsgi:application
### Apps Structure ### Apps Structure
- **authentication/**: Custom user model, email verification, password reset - **authentication/**: Custom user model, email verification, password reset
- **core/**: Homepage, error handlers, utility functions - **core/**: Homepage, error handlers, utility functions
- **workflows/**: Unified agent system (marketplace, execution, models) - **agents/**: Database-driven agent system (marketplace, execution, models, REST API)
- **wallet/**: Stripe payments, wallet management, transactions - **wallet/**: Stripe payments, wallet management, transactions
### Agent System (workflows app) ### Agent System (agents app)
**Key Files:** **Key Files:**
- `workflows/config/agents.py`: Agent definitions and configurations - `agents/models.py`: Agent, AgentCategory, AgentExecution models
- `workflows/models.py`: WorkflowRequest, WorkflowResponse, WorkflowAnalytics - `agents/views.py`: REST API and web interface views
- `workflows/views.py`: Marketplace and agent execution views - `agents/templates/agents/`: Dynamic agent templates with form generation
- `workflows/templates/workflows/`: Agent-specific templates - `agents/management/commands/`: Agent creation and management commands
**Agent Flow:** **Agent Flow:**
1. User selects agent from marketplace (`/agents/`) 1. User browses marketplace (`/agents/`)
2. Fills agent-specific form (`/agents/{slug}/`) 2. Selects agent and fills dynamic form (`/agents/{slug}/`)
3. Form submission creates WorkflowRequest and calls N8N webhook 3. Form submission creates AgentExecution and calls N8N webhook
4. N8N processes request and returns response via webhook 4. N8N processes request and returns response via webhook
5. WorkflowResponse stores results for user retrieval 5. Results displayed with file upload support and real-time wallet updates
### Database Models ### Database Models
**User Management:** **User Management:**
@ -119,10 +119,10 @@ gunicorn netcop_hub.wsgi:application
- `authentication.PasswordResetToken`: Password reset tokens - `authentication.PasswordResetToken`: Password reset tokens
- `authentication.EmailVerificationToken`: Email verification tokens - `authentication.EmailVerificationToken`: Email verification tokens
**Workflows:** **Agents:**
- `workflows.WorkflowRequest`: Universal agent request model - `agents.Agent`: Agent definitions with JSON form schemas and pricing
- `workflows.WorkflowResponse`: Universal agent response model - `agents.AgentCategory`: Agent categories with icons and descriptions
- `workflows.WorkflowAnalytics`: Usage tracking and analytics - `agents.AgentExecution`: Execution history and results tracking
**Payments:** **Payments:**
- `wallet.Wallet`: User wallet with balance tracking - `wallet.Wallet`: User wallet with balance tracking
@ -137,49 +137,81 @@ gunicorn netcop_hub.wsgi:application
- `DATABASE_URL`: PostgreSQL connection string (Railway) - `DATABASE_URL`: PostgreSQL connection string (Railway)
**N8N Webhook URLs:** **N8N Webhook URLs:**
- `N8N_WEBHOOK_DATA_ANALYZER`: Data analysis agent webhook Agent-specific webhook URLs are stored in the database with each agent. Current working agents:
- `N8N_WEBHOOK_FIVE_WHYS`: Five whys analysis webhook - Social Ads Generator: Creates compelling social media advertisements
- `N8N_WEBHOOK_JOB_POSTING`: Job posting generator webhook - Job Posting Generator: Creates professional job postings
- `N8N_WEBHOOK_FAQ_GENERATOR`: FAQ generator webhook - PDF Summarizer: Analyzes and summarizes PDF documents with file upload
- `N8N_WEBHOOK_SOCIAL_ADS`: Social ads generator webhook
### URL Structure ### URL Structure
``` ```
/ # Homepage (core app) / # Homepage (core app)
/auth/ # Authentication (login, register, etc.) /auth/ # Authentication (login, register, etc.)
/agents/ # Agent marketplace (workflows app) /agents/ # Agent marketplace (agents app)
/agents/{slug}/ # Individual agent pages /agents/{slug}/ # Individual agent pages
/wallet/ # Wallet management /wallet/ # Wallet management
/admin/ # Django admin /admin/ # Django admin
``` ```
### Key Components ### Key Components
**Agent Configuration (workflows/config/agents.py):** **Agent Configuration (Database-driven):**
- Centralizes all agent metadata (pricing, descriptions, webhooks) - All agent metadata stored in database (pricing, descriptions, webhooks)
- No database dependency for agent definitions - JSON form schemas for dynamic form generation
- Easy to add new agents by updating AGENT_CONFIGS - Easy to add new agents via management commands or admin interface
**Templates:** **Templates:**
- `templates/base.html`: Main layout with navigation - `templates/base.html`: Main layout with navigation
- `templates/components/`: Reusable UI components - `templates/components/`: Reusable UI components
- `workflows/templates/workflows/`: Agent-specific forms and pages - `agents/templates/agents/`: Dynamic agent forms and marketplace pages
## Adding New Agents ## Adding New Agents
1. **Add agent config** in `workflows/config/agents.py`: 1. **Create management command** (recommended approach):
```python ```python
'new-agent-slug': { # agents/management/commands/create_new_agent.py
from django.core.management.base import BaseCommand
from agents.models import AgentCategory, Agent
class Command(BaseCommand):
def handle(self, *args, **options):
category, _ = AgentCategory.objects.get_or_create(
slug='category-slug',
defaults={'name': 'Category Name', 'icon': '🤖'}
)
Agent.objects.get_or_create(
slug='agent-slug',
defaults={
'name': 'Agent Name', 'name': 'Agent Name',
'description': 'Agent description', 'short_description': 'Brief description',
'description': 'Full description',
'category': category,
'price': 10.0, 'price': 10.0,
'icon': '🤖', 'form_schema': {
'webhook_url': 'N8N_WEBHOOK_URL', 'fields': [
{
'name': 'input_field',
'type': 'text',
'label': 'Input Field',
'required': True
} }
]
},
'webhook_url': 'http://your-n8n-webhook-url'
}
)
``` ```
2. **Create agent template** in `workflows/templates/workflows/{slug}.html` 2. **Run the command**: `python manage.py create_new_agent`
3. **Add webhook URL** to environment variables 3. **Update N8N workflow** to handle the new agent
4. **Update N8N workflow** to handle new agent type 4. **Agent will automatically appear** in marketplace with dynamic form generation
**Supported Form Field Types:**
- `text`: Text input
- `textarea`: Multi-line text
- `select`: Dropdown with options
- `file`: File upload with drag-and-drop
- `url`: URL input with validation
- `checkbox`: Boolean checkbox
## Production Deployment ## Production Deployment
@ -220,9 +252,9 @@ gunicorn netcop_hub.wsgi:application
**Testing agent webhooks locally:** **Testing agent webhooks locally:**
1. Use ngrok or similar to expose local server 1. Use ngrok or similar to expose local server
2. Update webhook URLs in agent config 2. Update webhook URLs in agent database records
3. Test agent execution flow 3. Test agent execution flow
4. Check WorkflowRequest/WorkflowResponse creation 4. Check AgentExecution records and results display
--- ---
Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: 2025-07-31 22:55:38 Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: 2025-07-31 23:12:15

View File

@ -312,10 +312,10 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
<div class="agent-container"> <div class="agent-container">
<!-- Agent Header Component --> <!-- Agent Header Component -->
{% include "workflows/components/agent_header.html" with agent_title=agent.name agent_subtitle=agent.short_description %} {% include "components/agent_header.html" with agent_title=agent.name agent_subtitle=agent.short_description %}
<!-- Quick Agent Access Panel Component --> <!-- Quick Agent Access Panel Component -->
{% include "workflows/components/quick_agents_panel.html" %} {% include "components/quick_agents_panel.html" %}
<!-- Main Agent Grid --> <!-- Main Agent Grid -->
<div class="agent-grid"> <div class="agent-grid">
@ -460,14 +460,14 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
</div> </div>
<!-- How It Works Widget --> <!-- How It Works Widget -->
{% include "workflows/components/how_it_works_widget.html" with steps="agents" %} {% include "components/how_it_works_widget.html" with steps="agents" %}
</div> </div>
<!-- Processing Status Component --> <!-- Processing Status Component -->
{% include "workflows/components/processing_status.html" with status_title="Processing..." status_text="Please wait while we execute your agent..." %} {% include "components/processing_status.html" with status_title="Processing..." status_text="Please wait while we execute your agent..." %}
<!-- Results Component --> <!-- Results Component -->
{% include "workflows/components/results_container.html" with results_title="Agent Results" %} {% include "components/results_container.html" with results_title="Agent Results" %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -6,7 +6,7 @@ from django.core.mail import send_mail
from django.conf import settings from django.conf import settings
from django_ratelimit.decorators import ratelimit from django_ratelimit.decorators import ratelimit
from django_ratelimit import UNSAFE from django_ratelimit import UNSAFE
from workflows.config.agents import get_all_agents from agents.models import Agent
from .models import ContactSubmission from .models import ContactSubmission
from django.db import connection from django.db import connection
import logging import logging
@ -23,9 +23,8 @@ def homepage_view(request):
messages.warning(request, 'Too many requests. Please wait a moment before refreshing.') messages.warning(request, 'Too many requests. Please wait a moment before refreshing.')
try: try:
# Get featured agents for homepage from config # Get featured agents for homepage from database
all_agents = get_all_agents() featured_agents = Agent.objects.filter(is_active=True).select_related('category')[:6]
featured_agents = list(all_agents.items())[:6]
context = { context = {
'user_balance': request.user.wallet_balance if request.user.is_authenticated else 0, 'user_balance': request.user.wallet_balance if request.user.is_authenticated else 0,
@ -51,9 +50,8 @@ def pricing_view(request):
return redirect('wallet:wallet_topup') return redirect('wallet:wallet_topup')
try: try:
# Get sample agents to show pricing context from config # Get sample agents to show pricing context from database
all_agents = get_all_agents() sample_agents = Agent.objects.filter(is_active=True).select_related('category')[:4]
sample_agents = list(all_agents.items())[:4]
context = { context = {
'sample_agents': sample_agents, 'sample_agents': sample_agents,
@ -244,9 +242,9 @@ def health_check_view(request):
'response_time_ms': round((time.time() - start_time) * 1000, 2) 'response_time_ms': round((time.time() - start_time) * 1000, 2)
} }
# If database is working, get agent count from config # If database is working, get agent count from database
try: try:
agent_count = len(get_all_agents()) agent_count = Agent.objects.filter(is_active=True).count()
health_data['checks']['agents'] = { health_data['checks']['agents'] = {
'status': 'healthy', 'status': 'healthy',
'active_count': agent_count 'active_count': agent_count
@ -254,7 +252,7 @@ def health_check_view(request):
except Exception as e: except Exception as e:
health_data['checks']['agents'] = { health_data['checks']['agents'] = {
'status': 'warning', 'status': 'warning',
'error': 'Could not load agent config', 'error': 'Could not load agents',
'message': str(e)[:100] 'message': str(e)[:100]
} }

View File

@ -1,14 +1,17 @@
=== Documentation Auto-Update Summary === === Documentation Auto-Update Summary ===
Update Date: 2025-07-31 22:55:38 Update Date: 2025-07-31 23:12:15
Recent Commits: Recent Commits:
- c368bb5 ✨ Enhance marketplace with modern design and authentication
- 4424780 🧹 Clean up marketplace to keep only 3 working agents - 4424780 🧹 Clean up marketplace to keep only 3 working agents
- 400f2b7 📄 Add PDF Summarizer agent with advanced file upload system - 400f2b7 📄 Add PDF Summarizer agent with advanced file upload system
- cf8583d 💼 Add Job Posting Generator agent with comprehensive form schema
Agents Changes: Agents Changes:
- agents/templates/agents/marketplace.html - agents/templates/agents/marketplace.html
- agents/views.py
- static/js/agents-core.js - static/js/agents-core.js
- templates/components/quick_agents_panel.html
- workflows/templates/workflows/components/quick_agents_panel.html
Documentation Changes: Documentation Changes:
- CLAUDE.md - CLAUDE.md

View File

@ -1,268 +0,0 @@
#!/usr/bin/env python3
"""
N8N Workflow Management Script for Quantum Tasks AI
This script helps manage N8N workflows for webhook-based agents:
- Import workflows to N8N instance
- Export workflows from N8N instance
- Sync workflows between local files and N8N
- Backup and restore workflows
Usage:
python manage_n8n_workflows.py import [agent_name]
python manage_n8n_workflows.py export [agent_name]
python manage_n8n_workflows.py sync
python manage_n8n_workflows.py backup
"""
import os
import sys
import json
import requests
from datetime import datetime
from pathlib import Path
import argparse
# Configuration
N8N_BASE_URL = os.getenv('N8N_BASE_URL', 'http://localhost:5678')
N8N_API_KEY = os.getenv('N8N_API_KEY', '')
# Webhook-based agents that need N8N workflows
WEBHOOK_AGENTS = [
'data_analyzer',
'social_ads_generator',
'job_posting_generator',
'five_whys_analyzer'
]
class N8NWorkflowManager:
def __init__(self):
self.base_url = N8N_BASE_URL
self.api_key = N8N_API_KEY
self.headers = {
'Content-Type': 'application/json',
'X-N8N-API-KEY': self.api_key
} if self.api_key else {'Content-Type': 'application/json'}
def get_workflow_path(self, agent_name):
"""Get the workflow directory path for an agent"""
return Path(f"{agent_name}/n8n_workflows")
def load_workflow_json(self, agent_name, filename='workflow.json'):
"""Load workflow JSON from agent directory"""
workflow_path = self.get_workflow_path(agent_name) / filename
if not workflow_path.exists():
print(f"❌ Workflow file not found: {workflow_path}")
return None
try:
with open(workflow_path, 'r') as f:
return json.load(f)
except json.JSONDecodeError as e:
print(f"❌ Invalid JSON in {workflow_path}: {e}")
return None
def save_workflow_json(self, agent_name, workflow_data, filename='workflow.json'):
"""Save workflow JSON to agent directory"""
workflow_path = self.get_workflow_path(agent_name)
workflow_path.mkdir(exist_ok=True)
filepath = workflow_path / filename
with open(filepath, 'w') as f:
json.dump(workflow_data, f, indent=2)
print(f"✅ Workflow saved: {filepath}")
def import_workflow_to_n8n(self, agent_name):
"""Import workflow from local file to N8N instance"""
print(f"📥 Importing workflow for {agent_name}...")
workflow_data = self.load_workflow_json(agent_name)
if not workflow_data:
return False
# Create workflow in N8N
try:
response = requests.post(
f"{self.base_url}/api/v1/workflows",
headers=self.headers,
json=workflow_data
)
if response.status_code == 201:
workflow_id = response.json().get('id')
print(f"✅ Workflow imported successfully: ID {workflow_id}")
# Activate the workflow
activate_response = requests.post(
f"{self.base_url}/api/v1/workflows/{workflow_id}/activate",
headers=self.headers
)
if activate_response.status_code == 200:
print(f"✅ Workflow activated successfully")
else:
print(f"⚠️ Workflow imported but activation failed: {activate_response.text}")
return True
else:
print(f"❌ Import failed: {response.status_code} - {response.text}")
return False
except requests.RequestException as e:
print(f"❌ Connection error: {e}")
return False
def export_workflow_from_n8n(self, agent_name, workflow_name=None):
"""Export workflow from N8N instance to local file"""
print(f"📤 Exporting workflow for {agent_name}...")
if not workflow_name:
workflow_name = f"{agent_name.replace('_', ' ').title()} Agent"
try:
# Get all workflows
response = requests.get(
f"{self.base_url}/api/v1/workflows",
headers=self.headers
)
if response.status_code != 200:
print(f"❌ Failed to fetch workflows: {response.text}")
return False
workflows = response.json()
# Find workflow by name
target_workflow = None
for workflow in workflows:
if workflow.get('name', '').lower() == workflow_name.lower():
target_workflow = workflow
break
if not target_workflow:
print(f"❌ Workflow '{workflow_name}' not found in N8N")
print("Available workflows:")
for wf in workflows:
print(f" - {wf.get('name', 'Unnamed')}")
return False
# Save workflow with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.save_workflow_json(agent_name, target_workflow, f"workflow_exported_{timestamp}.json")
# Also save as main workflow file
self.save_workflow_json(agent_name, target_workflow, "workflow.json")
return True
except requests.RequestException as e:
print(f"❌ Connection error: {e}")
return False
def backup_all_workflows(self):
"""Backup all workflows to timestamped files"""
print("🔄 Backing up all workflows...")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
for agent_name in WEBHOOK_AGENTS:
workflow_path = self.get_workflow_path(agent_name)
if (workflow_path / "workflow.json").exists():
workflow_data = self.load_workflow_json(agent_name)
if workflow_data:
self.save_workflow_json(agent_name, workflow_data, f"workflow_backup_{timestamp}.json")
print(f"✅ Backed up {agent_name} workflow")
def sync_workflows(self):
"""Sync workflows between local files and N8N instance"""
print("🔄 Syncing all workflows...")
for agent_name in WEBHOOK_AGENTS:
print(f"\n--- {agent_name} ---")
# Check if local workflow exists
if (self.get_workflow_path(agent_name) / "workflow.json").exists():
print(f"📁 Local workflow found for {agent_name}")
# Try to import to N8N
success = self.import_workflow_to_n8n(agent_name)
if not success:
print(f"⚠️ Failed to sync {agent_name} to N8N")
else:
print(f"❌ No local workflow found for {agent_name}")
print(f"💡 Place your workflow JSON file at: {self.get_workflow_path(agent_name)}/workflow.json")
def list_workflows(self):
"""List all workflows in N8N and local directories"""
print("📋 Listing all workflows...\n")
# List N8N workflows
try:
response = requests.get(f"{self.base_url}/api/v1/workflows", headers=self.headers)
if response.status_code == 200:
workflows = response.json()
print(f"🌐 N8N Instance ({len(workflows)} workflows):")
for wf in workflows:
status = "🟢 Active" if wf.get('active') else "🔴 Inactive"
print(f" - {wf.get('name', 'Unnamed')} ({status})")
else:
print("❌ Could not connect to N8N instance")
except requests.RequestException:
print("❌ Could not connect to N8N instance")
print()
# List local workflows
print("📁 Local Workflows:")
for agent_name in WEBHOOK_AGENTS:
workflow_path = self.get_workflow_path(agent_name)
if workflow_path.exists():
files = list(workflow_path.glob("*.json"))
if files:
print(f" {agent_name}: {len(files)} files")
for file in files:
print(f" - {file.name}")
else:
print(f" {agent_name}: No workflow files")
else:
print(f" {agent_name}: Directory not found")
def main():
parser = argparse.ArgumentParser(description='N8N Workflow Management for Quantum Tasks AI')
parser.add_argument('action', choices=['import', 'export', 'sync', 'backup', 'list'],
help='Action to perform')
parser.add_argument('agent', nargs='?', choices=WEBHOOK_AGENTS,
help='Specific agent to operate on (for import/export)')
parser.add_argument('--workflow-name', help='Workflow name in N8N (for export)')
args = parser.parse_args()
manager = N8NWorkflowManager()
if args.action == 'import':
if not args.agent:
print("❌ Please specify an agent name for import")
print(f"Available agents: {', '.join(WEBHOOK_AGENTS)}")
sys.exit(1)
success = manager.import_workflow_to_n8n(args.agent)
sys.exit(0 if success else 1)
elif args.action == 'export':
if not args.agent:
print("❌ Please specify an agent name for export")
print(f"Available agents: {', '.join(WEBHOOK_AGENTS)}")
sys.exit(1)
success = manager.export_workflow_from_n8n(args.agent, args.workflow_name)
sys.exit(0 if success else 1)
elif args.action == 'sync':
manager.sync_workflows()
elif args.action == 'backup':
manager.backup_all_workflows()
elif args.action == 'list':
manager.list_workflows()
if __name__ == '__main__':
main()

View File

@ -75,8 +75,7 @@ INSTALLED_APPS = [
'authentication', 'authentication',
'wallet', 'wallet',
'core', 'core',
'workflows', # Unified workflows app (includes marketplace and agent execution) 'agents', # REST API-based agents system
'agents', # New REST API-based agents system
] ]
# Development apps (only in DEBUG mode) # Development apps (only in DEBUG mode)

View File

@ -24,10 +24,7 @@ urlpatterns = [
path('auth/', include('authentication.urls')), path('auth/', include('authentication.urls')),
path('wallet/', include('wallet.urls')), path('wallet/', include('wallet.urls')),
# Unified workflows system for all agents (includes marketplace) # REST API-based agents system (web interface + API)
path('workflows/', include('workflows.urls')),
# New REST API-based agents system (web interface + API)
path('agents/', include('agents.urls')), path('agents/', include('agents.urls')),
path('', include('core.urls')), path('', include('core.urls')),

View File

@ -53,7 +53,7 @@
<span class="btn-icon">🏠</span> <span class="btn-icon">🏠</span>
Go Home Go Home
</a> </a>
<a href="{% url 'workflows:marketplace' %}" class="btn btn-secondary"> <a href="{% url 'agents:marketplace' %}" class="btn btn-secondary">
<span class="btn-icon">🤖</span> <span class="btn-icon">🤖</span>
Browse AI Agents Browse AI Agents
</a> </a>
@ -98,7 +98,7 @@
<a href="mailto:support@quantumtaskai.com" class="support-link"> <a href="mailto:support@quantumtaskai.com" class="support-link">
📧 Contact Support 📧 Contact Support
</a> </a>
<a href="{% url 'workflows:marketplace' %}" class="support-link"> <a href="{% url 'agents:marketplace' %}" class="support-link">
🤖 View Available Agents 🤖 View Available Agents
</a> </a>
</div> </div>

View File

@ -43,7 +43,7 @@
<span class="btn-icon">🏠</span> <span class="btn-icon">🏠</span>
Go Home Go Home
</a> </a>
<a href="{% url 'workflows:marketplace' %}" class="btn btn-secondary"> <a href="{% url 'agents:marketplace' %}" class="btn btn-secondary">
<span class="btn-icon">🤖</span> <span class="btn-icon">🤖</span>
Browse AI Agents Browse AI Agents
</a> </a>
@ -87,7 +87,7 @@
<div class="help-content"> <div class="help-content">
<p class="help-text">Can't find what you're looking for?</p> <p class="help-text">Can't find what you're looking for?</p>
<div class="help-actions"> <div class="help-actions">
<a href="{% url 'workflows:marketplace' %}" class="help-link"> <a href="{% url 'agents:marketplace' %}" class="help-link">
🔍 Browse All Agents 🔍 Browse All Agents
</a> </a>
<a href="{% url 'core:pricing' %}" class="help-link"> <a href="{% url 'core:pricing' %}" class="help-link">

View File

@ -50,7 +50,7 @@
<span class="btn-icon">🏠</span> <span class="btn-icon">🏠</span>
Go Home Go Home
</a> </a>
<a href="{% url 'workflows:marketplace' %}" class="btn btn-secondary"> <a href="{% url 'agents:marketplace' %}" class="btn btn-secondary">
<span class="btn-icon">🤖</span> <span class="btn-icon">🤖</span>
Browse AI Agents Browse AI Agents
</a> </a>

View File

@ -30,7 +30,7 @@
</a> </a>
<nav class="header-nav" id="header-nav"> <nav class="header-nav" id="header-nav">
<a href="{% url 'core:homepage' %}" class="nav-link {% if request.resolver_match.url_name == 'homepage' %}active{% endif %}">Home</a> <a href="{% url 'core:homepage' %}" class="nav-link {% if request.resolver_match.url_name == 'homepage' %}active{% endif %}">Home</a>
<a href="{% url 'workflows:marketplace' %}" class="nav-link {% if request.resolver_match.url_name == 'marketplace' %}active{% endif %}">AI Marketplace</a> <a href="{% url 'agents:marketplace' %}" class="nav-link {% if request.resolver_match.url_name == 'marketplace' %}active{% endif %}">AI Marketplace</a>
<a href="{% url 'core:pricing' %}" class="nav-link {% if request.resolver_match.url_name == 'pricing' %}active{% endif %}">Pricing</a> <a href="{% url 'core:pricing' %}" class="nav-link {% if request.resolver_match.url_name == 'pricing' %}active{% endif %}">Pricing</a>
</nav> </nav>
<button class="mobile-nav-toggle" onclick="toggleMobileNav()" aria-label="Toggle navigation"> <button class="mobile-nav-toggle" onclick="toggleMobileNav()" aria-label="Toggle navigation">
@ -105,7 +105,7 @@
<a href="#company-profile" class="footer-nav-link">About Us</a> <a href="#company-profile" class="footer-nav-link">About Us</a>
<a href="#founder" class="footer-nav-link">Leadership</a> <a href="#founder" class="footer-nav-link">Leadership</a>
<a href="#contact" class="footer-nav-link">Contact Us</a> <a href="#contact" class="footer-nav-link">Contact Us</a>
<a href="{% url 'workflows:marketplace' %}" class="footer-nav-link">AI Marketplace</a> <a href="{% url 'agents:marketplace' %}" class="footer-nav-link">AI Marketplace</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -33,6 +33,6 @@
</div> </div>
<div class="quick-agents-footer"> <div class="quick-agents-footer">
<a href="{% url 'workflows:marketplace' %}" class="view-all-agents">View All Agents →</a> <a href="{% url 'agents:marketplace' %}" class="view-all-agents">View All Agents →</a>
</div> </div>
</div> </div>

View File

@ -41,7 +41,7 @@
<!-- CTA Buttons --> <!-- CTA Buttons -->
<div class="hero-buttons"> <div class="hero-buttons">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a href="{% url 'workflows:marketplace' %}" class="btn-primary"> <a href="{% url 'agents:marketplace' %}" class="btn-primary">
Explore AI Hub Explore AI Hub
</a> </a>
<a href="{% url 'wallet:wallet' %}" class="btn-secondary"> <a href="{% url 'wallet:wallet' %}" class="btn-secondary">
@ -51,7 +51,7 @@
<a href="{% url 'authentication:register' %}" class="btn-primary"> <a href="{% url 'authentication:register' %}" class="btn-primary">
Get Protected Now Get Protected Now
</a> </a>
<a href="{% url 'workflows:marketplace' %}" class="btn-secondary"> <a href="{% url 'agents:marketplace' %}" class="btn-secondary">
Explore AI Hub Explore AI Hub
</a> </a>
{% endif %} {% endif %}

View File

@ -93,7 +93,7 @@
aria-label="Create account to get started"> aria-label="Create account to get started">
🚀 Create Account 🚀 Create Account
</a> </a>
<a href="{% url 'workflows:marketplace' %}" <a href="{% url 'agents:marketplace' %}"
class="cta-btn secondary" class="cta-btn secondary"
aria-label="Browse available AI agents"> aria-label="Browse available AI agents">
🤖 View Marketplace 🤖 View Marketplace

View File

@ -700,7 +700,7 @@
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon">📭</div> <div class="empty-icon">📭</div>
<p class="empty-text">No transactions yet. Start using AI agents to see your transaction history!</p> <p class="empty-text">No transactions yet. Start using AI agents to see your transaction history!</p>
<a href="{% url 'workflows:marketplace' %}" class="btn btn-primary">🤖 Browse Agents</a> <a href="{% url 'agents:marketplace' %}" class="btn btn-primary">🤖 Browse Agents</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -719,7 +719,7 @@
<a href="{% url 'wallet:wallet_topup' %}" class="action-btn"> <a href="{% url 'wallet:wallet_topup' %}" class="action-btn">
💳 Top Up Wallet 💳 Top Up Wallet
</a> </a>
<a href="{% url 'workflows:marketplace' %}" class="action-btn"> <a href="{% url 'agents:marketplace' %}" class="action-btn">
🤖 Browse Agents 🤖 Browse Agents
</a> </a>
<a href="{% url 'core:homepage' %}" class="action-btn"> <a href="{% url 'core:homepage' %}" class="action-btn">

View File

@ -568,7 +568,7 @@
<a href="{% url 'wallet:wallet' %}" class="action-btn"> <a href="{% url 'wallet:wallet' %}" class="action-btn">
📊 View Transactions 📊 View Transactions
</a> </a>
<a href="{% url 'workflows:marketplace' %}" class="action-btn"> <a href="{% url 'agents:marketplace' %}" class="action-btn">
🤖 Browse Agents 🤖 Browse Agents
</a> </a>
<a href="{% url 'core:homepage' %}" class="action-btn"> <a href="{% url 'core:homepage' %}" class="action-btn">

View File

View File

@ -1,35 +0,0 @@
from django.contrib import admin
from .models import WorkflowRequest, WorkflowResponse, WorkflowAnalytics
@admin.register(WorkflowRequest)
class WorkflowRequestAdmin(admin.ModelAdmin):
list_display = ['id', 'user', 'agent_slug', 'status', 'created_at']
list_filter = ['status', 'agent_slug', 'created_at']
search_fields = ['user__username', 'agent_slug', 'id']
readonly_fields = ['id', 'created_at', 'updated_at']
def get_queryset(self, request):
return super().get_queryset(request).select_related('user')
@admin.register(WorkflowResponse)
class WorkflowResponseAdmin(admin.ModelAdmin):
list_display = ['request', 'success', 'processing_time', 'created_at']
list_filter = ['success', 'created_at']
search_fields = ['request__id', 'request__user__username']
readonly_fields = ['created_at']
def get_queryset(self, request):
return super().get_queryset(request).select_related('request__user')
@admin.register(WorkflowAnalytics)
class WorkflowAnalyticsAdmin(admin.ModelAdmin):
list_display = ['agent_slug', 'user', 'success', 'processing_time', 'date']
list_filter = ['success', 'agent_slug', 'date']
search_fields = ['user__username', 'agent_slug']
date_hierarchy = 'date'
def get_queryset(self, request):
return super().get_queryset(request).select_related('user')

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class WorkflowsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "workflows"

View File

@ -1 +0,0 @@
# Configuration package for workflows app

View File

@ -1,63 +0,0 @@
"""
Simplified agent configuration system for unified workflows app.
Each agent is defined by essential metadata only - forms are handled in individual templates.
"""
AGENT_CONFIGS = {
'social-ads-generator': {
'name': 'Social Ads Generator',
'description': 'Create engaging social media advertisements with AI-powered content generation',
'price': 6.0,
'icon': '📱',
'webhook_url': 'http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d',
},
'job-posting-generator': {
'name': 'Job Posting Generator',
'description': 'Create professional job postings that attract top talent',
'price': 10.0,
'icon': '💼',
'webhook_url': 'http://localhost:5678/webhook/43f84411-eaaa-488c-9b1f-856e90d0aaf6',
},
'data-analyzer': {
'name': 'Data Analyzer',
'description': 'AI-powered analysis of your data files with comprehensive insights',
'price': 8.0,
'icon': '📊',
'webhook_url': 'http://localhost:5678/webhook/simple-pdf-processor',
},
}
def get_agent_config(agent_slug):
"""Get agent configuration by slug"""
return AGENT_CONFIGS.get(agent_slug)
def get_all_agents():
"""Get all available agent configurations"""
return AGENT_CONFIGS
def get_available_agents():
"""Get all agents formatted for navigation components"""
return {
slug: {
'name': config['name'],
'icon': config['icon'],
'description': config['description']
}
for slug, config in AGENT_CONFIGS.items()
}
def format_message_for_n8n(agent_slug, form_data):
"""Format form data into message for N8N webhook"""
config = get_agent_config(agent_slug)
if not config:
return None
# Simple format - just send the form data as is
return f"Process {config['name']} request: {str(form_data)}"

View File

@ -1,145 +0,0 @@
# Generated by Django 5.2.4 on 2025-07-28 10:08
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="WorkflowRequest",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("agent_slug", models.CharField(db_index=True, max_length=100)),
("input_data", models.JSONField(default=dict)),
(
"status",
models.CharField(
choices=[
("processing", "Processing"),
("completed", "Completed"),
("failed", "Failed"),
],
default="processing",
max_length=20,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workflow_requests",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="WorkflowResponse",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("output_data", models.JSONField(default=dict)),
(
"processing_time",
models.DecimalField(
blank=True, decimal_places=2, max_digits=5, null=True
),
),
("success", models.BooleanField(default=True)),
("error_message", models.TextField(blank=True)),
("n8n_session_id", models.CharField(blank=True, max_length=100)),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"request",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="response",
to="workflows.workflowrequest",
),
),
],
),
migrations.CreateModel(
name="WorkflowAnalytics",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("agent_slug", models.CharField(db_index=True, max_length=100)),
(
"processing_time",
models.DecimalField(decimal_places=2, max_digits=5),
),
("success", models.BooleanField()),
("date", models.DateField(auto_now_add=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"indexes": [
models.Index(
fields=["agent_slug", "date"],
name="workflows_w_agent_s_feee49_idx",
),
models.Index(
fields=["user", "date"], name="workflows_w_user_id_94f7cd_idx"
),
],
},
),
migrations.AddIndex(
model_name="workflowrequest",
index=models.Index(
fields=["user", "-created_at"], name="workflows_w_user_id_29fa9d_idx"
),
),
migrations.AddIndex(
model_name="workflowrequest",
index=models.Index(
fields=["agent_slug", "-created_at"],
name="workflows_w_agent_s_c56767_idx",
),
),
]

View File

@ -1,82 +0,0 @@
from django.db import models
from django.contrib.auth import get_user_model
import uuid
import json
User = get_user_model()
class WorkflowRequest(models.Model):
"""Universal model for all agent workflow requests"""
STATUS_CHOICES = [
('processing', 'Processing'),
('completed', 'Completed'),
('failed', 'Failed'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='workflow_requests')
agent_slug = models.CharField(max_length=100, db_index=True) # e.g., 'social-ads-generator'
input_data = models.JSONField(default=dict) # All form data as JSON
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='processing')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', '-created_at']),
models.Index(fields=['agent_slug', '-created_at']),
]
def __str__(self):
return f"{self.agent_slug} - {self.user.username} ({self.created_at})"
class WorkflowResponse(models.Model):
"""Universal model for all agent workflow responses"""
request = models.OneToOneField(WorkflowRequest, on_delete=models.CASCADE, related_name='response')
output_data = models.JSONField(default=dict) # N8N response as JSON
processing_time = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) # in seconds
success = models.BooleanField(default=True)
error_message = models.TextField(blank=True)
n8n_session_id = models.CharField(max_length=100, blank=True) # Track N8N session
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
status = "Success" if self.success else "Failed"
return f"{self.request.agent_slug} Response - {status}"
@property
def formatted_output(self):
"""Format output data for display"""
if not self.output_data:
return "No output data"
# Handle different output formats based on agent type
if isinstance(self.output_data, dict):
if 'output' in self.output_data:
return self.output_data['output']
elif 'result' in self.output_data:
return self.output_data['result']
return json.dumps(self.output_data, indent=2)
class WorkflowAnalytics(models.Model):
"""Track workflow usage analytics"""
agent_slug = models.CharField(max_length=100, db_index=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
processing_time = models.DecimalField(max_digits=5, decimal_places=2) # in seconds
success = models.BooleanField()
date = models.DateField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['agent_slug', 'date']),
models.Index(fields=['user', 'date']),
]
def __str__(self):
return f"{self.agent_slug} - {self.date}"

View File

@ -1,802 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{#
AGENT TEMPLATE STARTER - Enhanced Version with Modern UX Patterns
Copy this file and customize for new agents
REQUIRED CUSTOMIZATIONS:
1. File names: "agent-template-starter" -> "your-agent-slug"
2. Agent info: "Agent Template Starter" -> Your agent name
3. Form fields: Replace example fields with your agent's specific inputs
4. JavaScript: Update agent-template-starter.js with your agent logic
5. Agent config: Add your agent to workflows/config/agents.py
TEMPLATE FEATURES INCLUDED:
✅ Enhanced file upload with preview, progress, and validation
✅ Real-time form validation with detailed error messages
✅ Drag-and-drop file support with visual feedback
✅ Class-based JavaScript architecture following data-analyzer pattern
✅ Comprehensive CSS framework with all modern patterns
✅ Mobile-responsive design with proper accessibility
✅ Integration with workflows-core.js and existing components
KEEP THE FOLLOWING UNCHANGED:
- All {% include %} statements for shared components
- Template structure (agent-container, agent-grid, etc.)
- Processing and results components
- Authentication and wallet balance checks
- CSS variable system and design tokens
OPTIONAL CUSTOMIZATIONS:
- How it works steps (change steps parameter)
- Custom CSS in the designated section
- Additional form sections or validation rules
- Custom result formatting in JavaScript
#}
{% block title %}Agent Template Starter - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
<style>
/* Enhanced Agent Template Styles - Complete Framework */
.form-textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
font-size: 14px;
line-height: 1.5;
transition: all 0.2s ease;
background: var(--surface);
color: var(--on-surface);
font-family: inherit;
resize: vertical;
min-height: 120px;
}
.form-textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.form-textarea:hover {
border-color: var(--on-surface-variant);
}
/* Enhanced Form Sections */
.section-container {
margin-bottom: var(--spacing-xl);
padding: var(--spacing-lg);
background: var(--surface-variant);
border-radius: var(--radius-md);
border: 1px solid var(--outline-variant);
}
.section-subtitle {
font-size: 16px;
font-weight: 600;
color: var(--on-surface);
margin: 0 0 var(--spacing-lg) 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.section-subtitle::before {
content: '';
width: 3px;
height: 16px;
background: var(--primary);
border-radius: 2px;
}
/* Error styling */
.form-textarea.error,
.form-input.error {
border-color: var(--error);
}
.form-error {
color: var(--error);
font-size: 12px;
margin-top: var(--spacing-xs);
font-weight: 500;
}
/* Enhanced Results Display */
.results-content {
background: var(--surface-variant);
border-radius: var(--radius-md);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
line-height: 1.7;
color: var(--on-surface);
font-size: 15px;
}
/* Results Typography */
.results-content h1,
.results-content h2,
.results-content h3 {
color: var(--primary);
font-weight: 700;
margin: var(--spacing-xl) 0 var(--spacing-md) 0;
line-height: 1.3;
}
.results-content h1 {
font-size: 24px;
border-bottom: 3px solid var(--primary);
padding-bottom: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.results-content h2 {
font-size: 20px;
margin-top: var(--spacing-xl);
position: relative;
padding-left: var(--spacing-md);
}
.results-content h2::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--primary);
border-radius: 2px;
}
.results-content h3 {
font-size: 18px;
color: var(--on-surface);
font-weight: 600;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-sm);
border-left: 4px solid var(--primary);
margin: var(--spacing-lg) 0 var(--spacing-md) 0;
}
.results-content strong {
color: var(--primary);
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;
}
.toast.info {
border-color: var(--primary);
background: #f0f9ff;
color: #0369a1;
}
/* Enhanced File Upload Styling */
.file-upload-area {
border: 2px dashed var(--outline-variant);
border-radius: var(--radius-md);
padding: var(--spacing-xl);
text-align: center;
background: var(--surface);
transition: all 0.2s ease;
cursor: pointer;
position: relative;
}
.file-upload-area:hover,
.file-upload-area.dragover {
border-color: var(--primary);
background: var(--surface-variant);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.file-upload-area.file-selected {
border-color: var(--success);
background: #f0fdf4;
color: #16a34a;
cursor: default;
}
.file-upload-area.upload-error {
border-color: var(--error);
background: #fef2f2;
color: #dc2626;
}
.file-upload-area.uploading {
border-color: var(--primary);
background: var(--surface-variant);
pointer-events: none;
}
.upload-icon {
font-size: 48px;
margin-bottom: var(--spacing-sm);
opacity: 0.7;
}
.upload-text {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
color: var(--on-surface-variant);
}
.upload-text > div:first-child {
font-weight: 500;
color: var(--on-surface);
}
/* File Preview Section */
.file-preview {
display: none;
background: var(--surface-variant);
border: 1px solid var(--outline-variant);
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin-top: var(--spacing-md);
position: relative;
}
.file-preview.show {
display: block;
}
.file-preview-content {
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
}
.file-icon {
font-size: 32px;
flex-shrink: 0;
opacity: 0.8;
}
.file-details {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 600;
color: var(--on-surface);
margin-bottom: var(--spacing-xs);
word-break: break-all;
}
.file-meta {
font-size: 12px;
color: var(--on-surface-variant);
display: flex;
gap: var(--spacing-md);
flex-wrap: wrap;
}
.file-actions {
display: flex;
gap: var(--spacing-sm);
flex-shrink: 0;
}
.file-action-btn {
background: none;
border: 1px solid var(--outline);
border-radius: var(--radius-sm);
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--on-surface-variant);
}
.file-action-btn:hover {
background: var(--surface);
border-color: var(--primary);
color: var(--primary);
}
.file-action-btn.remove {
color: var(--error);
border-color: var(--error);
}
.file-action-btn.remove:hover {
background: #fef2f2;
}
/* Upload Progress */
.upload-progress {
display: none;
margin-top: var(--spacing-sm);
}
.upload-progress.show {
display: block;
}
.progress-bar {
width: 100%;
height: 4px;
background: var(--outline-variant);
border-radius: 2px;
overflow: hidden;
margin-bottom: var(--spacing-xs);
}
.progress-fill {
height: 100%;
background: var(--primary);
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
font-size: 12px;
color: var(--on-surface-variant);
text-align: center;
}
/* Validation Messages */
.validation-message {
display: none;
margin-top: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
}
.validation-message.show {
display: block;
}
.validation-message.error {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
.validation-message.success {
background: #f0fdf4;
color: #16a34a;
border: 1px solid #bbf7d0;
}
.validation-message.warning {
background: #fffbeb;
color: #d97706;
border: 1px solid #fed7aa;
}
/* Responsive Design */
@media (max-width: 768px) {
.toast {
left: 20px;
right: 20px;
max-width: none;
transform: translateY(-100%);
}
.toast.show {
transform: translateY(0);
}
.results-content {
padding: var(--spacing-md);
font-size: 14px;
}
.results-content h1 {
font-size: 20px;
}
.results-content h2 {
font-size: 18px;
}
.results-content h3 {
font-size: 16px;
padding: var(--spacing-sm) var(--spacing-md);
}
.section-container {
padding: var(--spacing-md);
}
.file-upload-area {
padding: var(--spacing-lg);
}
.file-preview-content {
flex-direction: column;
gap: var(--spacing-sm);
}
.file-actions {
align-self: stretch;
justify-content: center;
}
.validation-message {
font-size: 12px;
padding: var(--spacing-xs) var(--spacing-sm);
}
}
/* Add agent-specific CSS here if needed */
</style>
{% endblock %}
{% block content %}
<script>
// Set data attributes for JavaScript access
document.body.setAttribute('data-user-authenticated', '{{ user.is_authenticated|yesno:"true,false" }}');
document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
</script>
<div class="agent-container">
<!-- Agent Header Component - KEEP THIS -->
{% include "workflows/components/agent_header.html" with agent_title=agent_config.name agent_subtitle=agent_config.description %}
<!-- Quick Agent Access Panel Component - KEEP THIS -->
{% include "workflows/components/quick_agents_panel.html" %}
<!-- Main Agent Grid -->
<div class="agent-grid">
<!-- CUSTOMIZE THIS SECTION: Agent-Specific Form Widget -->
<div class="agent-widget widget-large" style="flex: 1; margin-right: clamp(0px, var(--spacing-lg), 2vw);">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">{{ agent_config.icon }}</span>
{# CUSTOMIZE: Change "Details" to something specific like "Configuration", "Input", etc. #}
{{ agent_config.name }} Details
</h3>
</div>
<div class="widget-content">
<form id="agentForm" method="POST">
{% csrf_token %}
{#
CUSTOMIZE: Replace this entire section with your agent-specific form fields
FORM FIELD EXAMPLES INCLUDED:
- Text input with validation
- Textarea with character limits
- Select dropdown with options
- Enhanced file upload with preview
VALIDATION FEATURES:
- Required field validation
- Real-time error display
- File type and size validation
- Success/error visual feedback
CUSTOMIZE FOR YOUR AGENT:
1. Replace field names (example_input -> your_field_name)
2. Update validation rules in JavaScript
3. Modify form labels and help text
4. Add/remove fields as needed
5. Update placeholder text and options
#}
<!-- EXAMPLE FORM SECTION - REPLACE WITH YOUR FIELDS -->
<div class="section-container">
<h4 class="section-subtitle">📝 Input Section</h4>
<div class="form-group">
<label class="form-label" for="example_input">Example Input Field *</label>
<input type="text"
id="example_input"
name="example_input"
class="form-input"
placeholder="Enter your input here..."
required>
<div class="form-help">Provide a helpful description for this field</div>
<div id="example_input-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label" for="example_textarea">Example Textarea *</label>
<textarea id="example_textarea"
name="example_textarea"
class="form-textarea"
placeholder="Enter detailed information..."
required
rows="4"></textarea>
<div class="form-help">Describe what kind of content goes here</div>
<div id="example_textarea-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label" for="example_select">Example Select *</label>
<select id="example_select" name="example_select" class="form-input" required>
<option value="">Select an option...</option>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
<div class="form-help">Choose the appropriate option</div>
<div id="example_select-error" class="form-error" style="display: none;"></div>
</div>
</div>
{#
OPTIONAL: Enhanced File Upload Section
FEATURES INCLUDED:
✅ Drag and drop file upload
✅ File preview with metadata (name, size, timestamp)
✅ Replace/remove file actions
✅ Real-time validation (file type, size limits)
✅ Progress indicator during upload
✅ Visual feedback for different states
✅ Mobile-responsive design
✅ Accessibility features (keyboard navigation, ARIA labels)
CUSTOMIZATION OPTIONS:
1. Change accepted file types in 'accept' attribute
2. Modify file size limits in JavaScript validation
3. Update help text and file format descriptions
4. Customize validation messages
5. Remove entire section if file upload not needed
REMOVE THIS SECTION IF YOUR AGENT DOESN'T NEED FILE UPLOAD
#}
<!-- ENHANCED FILE UPLOAD SECTION -->
<div class="section-container"> <!-- File upload section enabled -->
<h4 class="section-subtitle">📁 File Upload (Optional)</h4>
<div class="form-group">
<label class="form-label">📁 Upload File (Optional)</label>
<div class="file-upload-area" id="fileUploadArea" onclick="triggerFileSelect()"
role="button" tabindex="0" aria-label="Click to upload file or drag and drop"
onkeydown="if(event.key==='Enter'||event.key===' '){triggerFileSelect()}">
<div class="upload-text" id="uploadText">
<div class="upload-icon">📁</div>
<div><strong>Click to upload</strong> or drag and drop</div>
<div>Supported formats: PDF, DOC, TXT, etc.</div>
</div>
</div>
<input type="file" id="example_file" name="example_file" accept=".pdf,.doc,.docx,.txt" style="display: none;">
<!-- Upload Progress -->
<div class="upload-progress" id="uploadProgress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">Preparing upload...</div>
</div>
<!-- File Preview -->
<div class="file-preview" id="filePreview">
<div class="file-preview-content">
<div class="file-icon">📄</div>
<div class="file-details">
<div class="file-name" id="previewFileName"></div>
<div class="file-meta">
<span id="previewFileSize"></span>
<span id="previewFileType">Document</span>
<span id="previewTimestamp"></span>
</div>
</div>
<div class="file-actions">
<button type="button" class="file-action-btn" onclick="replaceFile()" title="Replace file">
🔄 Replace
</button>
<button type="button" class="file-action-btn remove" onclick="removeFile()" title="Remove file">
🗑️ Remove
</button>
</div>
</div>
</div>
<!-- Validation Messages -->
<div class="validation-message" id="validationMessage"></div>
<div class="form-help">Upload a file if needed for processing (Max size: 10MB)</div>
<div id="example_file-error" class="form-error" style="display: none;"></div>
</div>
</div>
{# END CUSTOMIZE SECTION #}
{#
SUBMIT BUTTON SECTION - KEEP THIS STRUCTURE UNCHANGED
FEATURES INCLUDED:
✅ Authentication check (redirects to login if not authenticated)
✅ Wallet balance validation (shows top-up if insufficient funds)
✅ Proper form submission handling
✅ Loading states and user feedback
✅ Price display from agent configuration
✅ Consistent styling across all agents
CUSTOMIZATION OPTIONS:
- Change action verb in button text ("Generate" -> "Analyze", "Create", etc.)
- Update button icon emoji to match your agent
DO NOT CHANGE:
- Authentication and balance check logic
- Template structure and Django template tags
- CSS classes and styling
- Error message formatting
#}
<!-- SUBMIT BUTTON WITH AUTHENTICATION & BALANCE CHECKS -->
<div style="margin-top: var(--spacing-lg);">
{% if user.is_authenticated %}
{% if user.wallet_balance >= agent_config.price %}
<button type="submit" class="btn btn-primary btn-full" id="generateBtn">
{# CUSTOMIZE: Change action verb like "Generate", "Analyze", "Process" #}
{{ agent_config.icon }} Generate with {{ agent_config.name }} ({{ agent_config.price }} AED)
</button>
{% else %}
<div style="background: #fef2f2; color: #dc2626; padding: var(--spacing-md); border-radius: var(--radius-md); margin-bottom: var(--spacing-md); font-size: 14px; font-weight: 500; text-align: center;">
Insufficient balance! You need {{ agent_config.price }} AED.
</div>
<a href="{% url 'wallet:wallet' %}" class="btn btn-primary btn-full" style="text-decoration: none;">
💰 Top Up Wallet
</a>
{% endif %}
{% else %}
<a href="{% url 'authentication:login' %}" class="btn btn-primary btn-full">
🔐 Login to Continue
</a>
{% endif %}
</div>
</form>
</div>
</div>
<!-- How It Works Widget - KEEP THIS, CUSTOMIZE steps parameter -->
{# CUSTOMIZE: Change "generic" to your agent-specific steps or keep as is #}
{% include "workflows/components/how_it_works_widget.html" with steps="generic" %}
</div>
<!-- Processing Status Component - KEEP THIS -->
{# CUSTOMIZE: Change status messages to match your agent's processing #}
{% include "workflows/components/processing_status.html" with status_title="Processing your request..." status_text="Please wait while we generate your content." %}
<!-- Results Component - KEEP THIS -->
{# CUSTOMIZE: Change results_title to match your agent's output #}
{% include "workflows/components/results_container.html" with results_title="Generated Results" %}
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/workflows-core.js' %}?v={{ timestamp }}"></script>
{# CUSTOMIZE: Replace 'agent-template-starter' with your agent slug #}
<script src="{% static 'js/agent-template-starter.js' %}?v={{ timestamp }}"></script>
{% endblock %}
{#
===============================================================================
SETUP INSTRUCTIONS FOR NEW AGENTS
===============================================================================
1. COPY FILES:
- Copy this template: agent-template-starter.html -> your-agent-name.html
- Copy JavaScript: agent-template-starter.js -> your-agent-name.js
- Place in: workflows/templates/workflows/ and static/js/ respectively
2. UPDATE AGENT CONFIG:
- Add your agent to workflows/config/agents.py
- Set name, description, price, icon, and webhook_url
- Example:
'your-agent-slug': {
'name': 'Your Agent Name',
'description': 'Description of what your agent does',
'price': 10.0,
'icon': '🤖',
'webhook_url': 'your-n8n-webhook-url',
}
3. CUSTOMIZE TEMPLATE:
- Update page title and meta information
- Replace example form fields with your agent's inputs
- Modify section titles and descriptions
- Update How It Works steps if needed
- Remove file upload section if not needed
4. CUSTOMIZE JAVASCRIPT:
- Change class name: AgentTemplateProcessor -> YourAgentProcessor
- Update agent slug and webhook URL
- Modify form validation rules for your fields
- Customize result formatting for your agent's output
- Update file validation if using file upload
5. ADD URL ROUTE:
- Add URL pattern in workflows/urls.py
- Point to your agent's view function
- Example: path('your-agent-slug/', views.your_agent_view, name='your_agent')
6. CREATE VIEW:
- Add view function in workflows/views.py
- Handle form processing and N8N integration
- Return appropriate JSON responses
- Follow existing agent patterns
7. TESTING:
- Test form validation and submission
- Verify file upload functionality (if used)
- Check responsive design on mobile
- Test authentication and wallet balance flows
- Verify N8N webhook integration
8. PRODUCTION:
- Update N8N webhook URLs to production
- Test with real user accounts
- Monitor error logs and performance
- Update documentation if needed
===============================================================================
AVAILABLE WORKFLOWSCORE FUNCTIONS (from workflows-core.js)
===============================================================================
Authentication & Balance:
- WorkflowsCore.checkAuthentication()
- WorkflowsCore.checkBalance(price)
- WorkflowsCore.updateWalletBalance(balance)
UI Feedback:
- WorkflowsCore.showToast(message, type)
- WorkflowsCore.showProcessing(title)
- WorkflowsCore.hideProcessing()
- WorkflowsCore.showResults(content, title)
Form Validation:
- WorkflowsCore.showFieldError(fieldName, message)
- WorkflowsCore.clearFieldError(fieldName)
- WorkflowsCore.clearAllFieldErrors()
Utilities:
- WorkflowsCore.copyToClipboard(text, message)
- WorkflowsCore.downloadAsFile(content, filename, message)
- WorkflowsCore.generateSessionId()
- WorkflowsCore.formatFileSize(bytes)
Processing:
- WorkflowsCore.startPolling(requestId)
- WorkflowsCore.stopPolling()
- WorkflowsCore.pollForResults(requestId, callback)
===============================================================================
#}

View File

@ -1,9 +0,0 @@
<div class="agent-header">
<div>
<h1 class="agent-title">{{ agent_title }}</h1>
<p class="agent-subtitle">{{ agent_subtitle }}</p>
</div>
<div class="header-controls">
{% include "workflows/components/wallet_card.html" %}
</div>
</div>

View File

@ -1,59 +0,0 @@
<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">
{% if steps == "data" %}
<ol class="info-list">
<li>Upload your data file</li>
<li>Choose analysis type</li>
<li>Get AI-powered insights</li>
<li>Copy or download results</li>
</ol>
{% elif steps == "weather" %}
<ol class="info-list">
<li>Enter any city name worldwide</li>
<li>Choose your preferred report type</li>
<li>Get real-time weather data</li>
<li>Copy or download detailed reports</li>
</ol>
{% elif steps == "social_ads" %}
<ol class="info-list">
<li>Choose your platform and language</li>
<li>Describe your content and audience</li>
<li>Get AI-generated social ads</li>
<li>Copy or download your campaigns</li>
</ol>
{% elif steps == "job_posting" %}
<ol class="info-list">
<li>Enter job title and company details</li>
<li>Describe role and requirements</li>
<li>Get professional job posting</li>
<li>Copy or download the posting</li>
</ol>
{% elif steps == "five_whys" %}
<ol class="info-list">
<li>Describe your problem clearly</li>
<li>Choose analysis language</li>
<li>Get Five Whys analysis</li>
<li>Copy or download the results</li>
</ol>
{% else %}
<ol class="info-list">
<li>Fill in the required information</li>
<li>Choose your preferences</li>
<li>Get AI-powered results</li>
<li>Copy or download output</li>
</ol>
{% endif %}
<button class="quick-agent-toggle btn btn-secondary btn-full" onclick="toggleQuickAgents()"
title="Quick access to other agents">
<span class="toggle-icon">🚀</span>
<span class="toggle-text">Explore Other Agents</span>
</button>
</div>
</div>

View File

@ -1,13 +0,0 @@
<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">{{ status_title|default:"Processing your request..." }}</div>
<div class="status-text" id="statusText">{{ status_text|default:"Please wait while we analyze your data..." }}</div>
</div>
</div>

View File

@ -1,51 +0,0 @@
<div class="quick-agents-overlay" id="quickAgentsOverlay" onclick="closeQuickAgents()" aria-hidden="true"></div>
<div class="quick-agents-panel" id="quickAgentsPanel" role="dialog" aria-labelledby="quickAgentsTitle" aria-hidden="true">
<div class="quick-agents-header">
<h3 id="quickAgentsTitle">Quick Access to Other Agents</h3>
<button class="close-panel" onclick="toggleQuickAgents()" aria-label="Close quick agents panel">×</button>
</div>
<div class="quick-agents-grid">
{% if available_agents %}
{% for agent_slug, agent_info in available_agents.items %}
<a href="/agents/{{ agent_slug }}/" class="quick-agent-card">
<div class="agent-icon">{{ agent_info.icon }}</div>
<div class="agent-info">
<h4>{{ agent_info.name }}</h4>
<p>{{ agent_info.description|truncatewords:4 }}</p>
</div>
</a>
{% endfor %}
{% else %}
<!-- Fallback to hardcoded working agents if available_agents not provided -->
<a href="/agents/social-ads-generator/" class="quick-agent-card">
<div class="agent-icon">📱</div>
<div class="agent-info">
<h4>Social Ads Generator</h4>
<p>Create social media ads</p>
</div>
</a>
<a href="/agents/job-posting-generator/" class="quick-agent-card">
<div class="agent-icon">💼</div>
<div class="agent-info">
<h4>Job Posting Generator</h4>
<p>Create professional job posts</p>
</div>
</a>
<a href="/agents/pdf-summarizer/" class="quick-agent-card">
<div class="agent-icon">📄</div>
<div class="agent-info">
<h4>PDF Summarizer</h4>
<p>Analyze and summarize PDFs</p>
</div>
</a>
{% endif %}
</div>
<div class="quick-agents-footer">
<a href="{% url 'workflows:marketplace' %}" class="view-all-agents">View All Agents →</a>
</div>
</div>

View File

@ -1,21 +0,0 @@
<div class="agent-widget widget-wide results-container" id="resultsContainer" style="display: none;">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">📊</span>
{{ results_title|default:"Results" }}
</h3>
<span class="status-badge">Success</span>
</div>
<div class="widget-content">
<div class="results-content" id="resultsContent">
<!-- Results will be populated here by JavaScript -->
</div>
<div class="results-actions action-buttons">
<button onclick="copyResults()" class="btn btn-primary">📋 Copy Results</button>
<button onclick="downloadResults()" class="btn btn-secondary">💾 Download</button>
<button onclick="resetForm()" class="btn btn-secondary">🔄 New Request</button>
</div>
</div>
</div>

View File

@ -1,17 +0,0 @@
<div class="wallet-card widget-small" style="margin-bottom: 0;">
<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">{{ user.wallet_balance|floatformat:2 }}</span> AED
</div>
<div class="balance-label">Available Balance</div>
</div>
<div style="margin-top: 12px;">
<a href="{% url 'wallet:wallet_topup' %}" class="wallet-topup-btn" style="display: block; 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; text-decoration: none; text-align: center; box-sizing: border-box;">
💳 Top Up Wallet
</a>
</div>
</div>

View File

@ -1,468 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Data Analyzer - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
<style>
/* Data Analyzer Specific Styles */
.file-upload-area {
border: 2px dashed var(--outline-variant);
border-radius: var(--radius-md);
padding: var(--spacing-xl);
text-align: center;
background: var(--surface);
transition: all 0.2s ease;
cursor: pointer;
position: relative;
}
.file-upload-area:hover,
.file-upload-area.dragover {
border-color: var(--primary);
background: var(--surface-variant);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.file-upload-area.file-selected {
border-color: var(--success);
background: #f0fdf4;
color: #16a34a;
cursor: default;
}
.file-upload-area.upload-error {
border-color: var(--error);
background: #fef2f2;
color: #dc2626;
}
.file-upload-area.uploading {
border-color: var(--primary);
background: var(--surface-variant);
pointer-events: none;
}
/* File Preview Section */
.file-preview {
display: none;
background: var(--surface-variant);
border: 1px solid var(--outline-variant);
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin-top: var(--spacing-md);
position: relative;
}
.file-preview.show {
display: block;
}
.file-preview-content {
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
}
.file-icon {
font-size: 32px;
flex-shrink: 0;
opacity: 0.8;
}
.file-details {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 600;
color: var(--on-surface);
margin-bottom: var(--spacing-xs);
word-break: break-all;
}
.file-meta {
font-size: 12px;
color: var(--on-surface-variant);
display: flex;
gap: var(--spacing-md);
flex-wrap: wrap;
}
.file-actions {
display: flex;
gap: var(--spacing-sm);
flex-shrink: 0;
}
.file-action-btn {
background: none;
border: 1px solid var(--outline);
border-radius: var(--radius-sm);
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--on-surface-variant);
}
.file-action-btn:hover {
background: var(--surface);
border-color: var(--primary);
color: var(--primary);
}
.file-action-btn.remove {
color: var(--error);
border-color: var(--error);
}
.file-action-btn.remove:hover {
background: #fef2f2;
}
/* Upload Progress */
.upload-progress {
display: none;
margin-top: var(--spacing-sm);
}
.upload-progress.show {
display: block;
}
.progress-bar {
width: 100%;
height: 4px;
background: var(--outline-variant);
border-radius: 2px;
overflow: hidden;
margin-bottom: var(--spacing-xs);
}
.progress-fill {
height: 100%;
background: var(--primary);
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
font-size: 12px;
color: var(--on-surface-variant);
text-align: center;
}
/* Validation Messages */
.validation-message {
display: none;
margin-top: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
}
.validation-message.show {
display: block;
}
.validation-message.error {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
.validation-message.success {
background: #f0fdf4;
color: #16a34a;
border: 1px solid #bbf7d0;
}
.validation-message.warning {
background: #fffbeb;
color: #d97706;
border: 1px solid #fed7aa;
}
.upload-icon {
font-size: 48px;
margin-bottom: var(--spacing-sm);
opacity: 0.7;
}
.upload-text {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
color: var(--on-surface-variant);
}
.upload-text > div:first-child {
font-weight: 500;
color: var(--on-surface);
}
.radio-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: var(--spacing-md);
margin-top: var(--spacing-sm);
}
.radio-card {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
background: var(--surface);
position: relative;
}
.radio-card:hover {
border-color: var(--primary);
background: var(--surface-variant);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.radio-card.selected {
border-color: var(--primary);
background: rgba(0, 0, 0, 0.02);
}
.radio-card input[type="radio"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.radio-button {
width: 20px;
height: 20px;
border: 2px solid var(--outline);
border-radius: 50%;
position: relative;
transition: all 0.2s ease;
flex-shrink: 0;
}
.radio-card.selected .radio-button {
border-color: var(--primary);
}
.radio-card.selected .radio-button::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
background: var(--primary);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.radio-label {
font-size: 14px;
font-weight: 500;
color: var(--on-surface);
cursor: pointer;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* Responsive Design */
@media (max-width: 768px) {
.radio-grid {
grid-template-columns: 1fr;
}
.file-upload-area {
padding: var(--spacing-lg);
}
}
</style>
{% endblock %}
{% block content %}
<script>
// Set data attributes for JavaScript access
document.body.setAttribute('data-user-authenticated', '{{ user.is_authenticated|yesno:"true,false" }}');
document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
</script>
<div class="agent-container">
<!-- Agent Header Component -->
{% include "workflows/components/agent_header.html" with agent_title="Data Analyzer" agent_subtitle="AI-powered analysis of your data files with comprehensive insights" %}
<!-- Quick Agent Access Panel Component -->
{% include "workflows/components/quick_agents_panel.html" %}
<!-- Main Agent Grid -->
<div class="agent-grid">
<!-- Data Analysis Form Widget -->
<div class="agent-widget widget-large" style="flex: 1; margin-right: clamp(0px, var(--spacing-lg), 2vw);">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">📊</span>
Data Analysis Configuration
</h3>
</div>
<div class="widget-content">
<form id="agentForm" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<!-- File Upload Section -->
<div class="form-group">
<label class="form-label">📁 Upload Data File *</label>
<div class="file-upload-area" id="fileUploadArea" onclick="triggerFileSelect()"
role="button" tabindex="0" aria-label="Click to upload data file or drag and drop"
onkeydown="if(event.key==='Enter'||event.key===' '){triggerFileSelect()}">
<div class="upload-text" id="uploadText">
<div class="upload-icon">📁</div>
<div><strong>Click to upload</strong> or drag and drop</div>
<div>PDF files only</div>
</div>
</div>
<input type="file" id="dataFile" name="file" accept=".pdf" style="display: none;" required>
<!-- Upload Progress -->
<div class="upload-progress" id="uploadProgress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">Preparing upload...</div>
</div>
<!-- File Preview -->
<div class="file-preview" id="filePreview">
<div class="file-preview-content">
<div class="file-icon">📄</div>
<div class="file-details">
<div class="file-name" id="previewFileName"></div>
<div class="file-meta">
<span id="previewFileSize"></span>
<span id="previewFileType">PDF Document</span>
<span id="previewTimestamp"></span>
</div>
</div>
<div class="file-actions">
<button type="button" class="file-action-btn" onclick="replaceFile()" title="Replace file">
🔄 Replace
</button>
<button type="button" class="file-action-btn remove" onclick="removeFile()" title="Remove file">
🗑️ Remove
</button>
</div>
</div>
</div>
<!-- Validation Messages -->
<div class="validation-message" id="validationMessage"></div>
<div class="form-help">Supported format: PDF files only. Max size: 10MB</div>
<div id="dataFile-error" class="form-error" style="display: none;"></div>
</div>
<!-- Analysis Type Selection -->
<div class="form-group">
<label class="form-label">📈 Analysis Type *</label>
<div class="radio-grid">
<div class="radio-card selected" onclick="selectRadio('summary')">
<input type="radio" id="summary" name="analysisType" value="summary" checked>
<div class="radio-button"></div>
<label for="summary" class="radio-label">📋 Summary</label>
</div>
<div class="radio-card" onclick="selectRadio('detailed')">
<input type="radio" id="detailed" name="analysisType" value="detailed">
<div class="radio-button"></div>
<label for="detailed" class="radio-label">📈 Detailed</label>
</div>
<div class="radio-card" onclick="selectRadio('statistical')">
<input type="radio" id="statistical" name="analysisType" value="statistical">
<div class="radio-button"></div>
<label for="statistical" class="radio-label">🔢 Statistical</label>
</div>
</div>
<div class="form-help">Choose the type of analysis for your data file</div>
<div id="analysisType-error" class="form-error" style="display: none;"></div>
</div>
<!-- Submit Button -->
<div style="margin-top: var(--spacing-lg);">
{% if user.is_authenticated %}
{% if user.wallet_balance >= agent_config.price %}
<button type="submit" class="btn btn-primary btn-full" id="generateBtn">
🚀 Analyze Data ({{ agent_config.price }} AED)
</button>
{% else %}
<div style="background: #fef2f2; color: #dc2626; padding: var(--spacing-md); border-radius: var(--radius-md); margin-bottom: var(--spacing-md); font-size: 14px; font-weight: 500; text-align: center;">
Insufficient balance! You need {{ agent_config.price }} AED.
</div>
<a href="{% url 'wallet:wallet' %}" class="btn btn-primary btn-full" style="text-decoration: none;">
💰 Top Up Wallet
</a>
{% endif %}
{% else %}
<a href="{% url 'authentication:login' %}" class="btn btn-primary btn-full">
🔐 Login to Continue
</a>
{% endif %}
</div>
</form>
</div>
</div>
<!-- How It Works Widget -->
<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>Upload your PDF file</li>
<li>Choose analysis type and preferences</li>
<li>Our AI analyzes your data</li>
<li>Get comprehensive insights and reports</li>
</ol>
<!-- Other Agents Button -->
<button class="quick-agent-toggle btn btn-secondary btn-full" 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 -->
{% include "workflows/components/processing_status.html" with status_title="Analyzing Your Data..." status_text="Please wait while our AI processes your file..." %}
<!-- Results Component -->
{% include "workflows/components/results_container.html" with results_title="Analysis Results" %}
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/workflows-core.js' %}?v={{ timestamp }}"></script>
<script src="{% static 'js/data-analyzer.js' %}?v={{ timestamp }}"></script>
{% endblock %}

View File

@ -1,289 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Job Posting Generator - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
<style>
/* Job Posting Generator Specific Styles */
.form-textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
font-size: 14px;
line-height: 1.5;
transition: all 0.2s ease;
background: var(--surface);
color: var(--on-surface);
font-family: inherit;
resize: vertical;
min-height: 120px;
}
.form-textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.form-textarea:hover {
border-color: var(--on-surface-variant);
}
/* Enhanced Form Sections */
.section-container {
margin-bottom: var(--spacing-xl);
padding: var(--spacing-lg);
background: var(--surface-variant);
border-radius: var(--radius-md);
border: 1px solid var(--outline-variant);
}
.section-subtitle {
font-size: 16px;
font-weight: 600;
color: var(--on-surface);
margin: 0 0 var(--spacing-lg) 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* Job Posting Results Styling */
.job-posting-content {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
color: var(--on-surface);
max-width: none;
}
.job-section-title {
color: var(--primary);
font-size: 1.25rem;
font-weight: 600;
margin: 1.5rem 0 0.75rem 0 !important;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--outline-variant);
}
.job-section-title:first-child {
margin-top: 0 !important;
}
.job-paragraph {
margin: 1rem 0;
text-align: justify;
color: var(--on-surface);
}
.job-list {
margin: 1rem 0;
padding-left: 1.5rem;
}
.job-list li {
margin: 0.5rem 0;
line-height: 1.5;
color: var(--on-surface);
}
.job-list li::marker {
color: var(--primary);
}
/* Responsive Design */
@media (max-width: 768px) {
.job-section-title {
font-size: 1.1rem;
}
.section-container {
padding: var(--spacing-md);
}
}
</style>
{% endblock %}
{% block content %}
<script>
// Set data attributes for JavaScript access
document.body.setAttribute('data-user-authenticated', '{{ user.is_authenticated|yesno:"true,false" }}');
document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
</script>
<div class="agent-container">
<!-- Agent Header Component -->
{% include "workflows/components/agent_header.html" with agent_title="Job Posting Generator" agent_subtitle="Create professional job postings that attract top talent" %}
<!-- Quick Agent Access Panel Component -->
{% include "workflows/components/quick_agents_panel.html" %}
<!-- Main Agent Grid -->
<div class="agent-grid">
<!-- Job Posting Form Widget -->
<div class="agent-widget widget-large" style="flex: 1; margin-right: clamp(0px, var(--spacing-lg), 2vw);">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">💼</span>
Job Posting Configuration
</h3>
</div>
<div class="widget-content">
<form id="agentForm" method="POST">
{% csrf_token %}
<!-- Basic Job Information -->
<div class="section-container">
<h4 class="section-subtitle">
<span>📝</span>
Basic Information
</h4>
<div class="form-group">
<label class="form-label">Job Title *</label>
<input type="text" name="job_title" id="job_title" class="form-input"
placeholder="e.g., Senior Software Engineer"
value="Senior Full Stack Developer" required>
<div id="job_title-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label">Company Name *</label>
<input type="text" name="company_name" id="company_name" class="form-input"
placeholder="e.g., TechCorp Inc."
value="Quantum Technologies Inc." required>
<div id="company_name-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label">Job Description *</label>
<textarea name="job_description" id="job_description" class="form-textarea"
placeholder="Describe the role, requirements, and company culture..."
rows="4" required>We are looking for a talented Senior Full Stack Developer to join our innovative team. You will work on cutting-edge projects using modern technologies like React, Node.js, and Python. Strong problem-solving skills and experience with cloud platforms preferred.</textarea>
<div class="form-help">Provide a detailed description of the role and requirements</div>
<div id="job_description-error" class="form-error" style="display: none;"></div>
</div>
</div>
<!-- Position Details -->
<div class="section-container">
<h4 class="section-subtitle">
<span>🎯</span>
Position Details
</h4>
<div class="form-group">
<label class="form-label">Seniority Level *</label>
<select name="seniority_level" id="seniority_level" class="form-input" required>
<option value="">Select level...</option>
<option value="entry">Entry Level</option>
<option value="mid">Mid Level</option>
<option value="senior" selected>Senior Level</option>
<option value="lead">Lead/Principal</option>
<option value="executive">Executive</option>
</select>
<div id="seniority_level-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label">Contract Type *</label>
<select name="contract_type" id="contract_type" class="form-input" required>
<option value="">Select type...</option>
<option value="full-time" selected>Full-time</option>
<option value="part-time">Part-time</option>
<option value="contract">Contract</option>
<option value="freelance">Freelance</option>
<option value="internship">Internship</option>
</select>
<div id="contract_type-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label">Location *</label>
<input type="text" name="location" id="location" class="form-input"
placeholder="e.g., Dubai, UAE or Remote"
value="Dubai, UAE (Remote)" required>
<div id="location-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label">Language</label>
<select name="language" id="language" class="form-input">
<option value="English">English</option>
<option value="Arabic">Arabic</option>
<option value="Spanish">Spanish</option>
<option value="French">French</option>
<option value="German">German</option>
</select>
</div>
</div>
<!-- Submit Button -->
<div style="margin-top: var(--spacing-lg);">
{% if user.is_authenticated %}
{% if user.wallet_balance >= agent_config.price %}
<button type="submit" class="btn btn-primary btn-full" id="generateBtn">
💼 Generate Job Posting ({{ agent_config.price }} AED)
</button>
{% else %}
<div style="background: #fef2f2; color: #dc2626; padding: var(--spacing-md); border-radius: var(--radius-md); margin-bottom: var(--spacing-md); font-size: 14px; font-weight: 500; text-align: center;">
Insufficient balance! You need {{ agent_config.price }} AED.
</div>
<a href="{% url 'wallet:wallet' %}" class="btn btn-primary btn-full" style="text-decoration: none;">
💰 Top Up Wallet
</a>
{% endif %}
{% else %}
<a href="{% url 'authentication:login' %}" class="btn btn-primary btn-full">
🔐 Login to Continue
</a>
{% endif %}
</div>
</form>
</div>
</div>
<!-- How It Works Widget -->
<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 job requirements</li>
<li>Configure position details</li>
<li>AI processes your information</li>
<li>Get professional job posting</li>
</ol>
<!-- Other Agents Button -->
<button class="quick-agent-toggle btn btn-secondary btn-full" 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 -->
{% include "workflows/components/processing_status.html" with status_title="Creating Job Posting..." status_text="Please wait while we generate your professional job posting..." %}
<!-- Results Component -->
{% include "workflows/components/results_container.html" with results_title="Generated Job Posting" %}
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/workflows-core.js' %}?v={{ timestamp }}"></script>
<script src="{% static 'js/job-posting-generator.js' %}?v={{ timestamp }}"></script>
{% endblock %}

View File

@ -1,376 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}AI Agent Marketplace - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}">
<style>
/* Marketplace Specific Styles */
.marketplace-header {
text-align: center;
margin-bottom: var(--spacing-xl);
padding: var(--spacing-xl) 0;
}
.marketplace-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--on-surface);
margin-bottom: var(--spacing-md);
}
.marketplace-subtitle {
font-size: 1.1rem;
color: var(--on-surface-variant);
max-width: 600px;
margin: 0 auto;
}
.marketplace-filters {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.search-box {
flex: 1;
max-width: 400px;
position: relative;
}
.search-input {
width: 100%;
padding: 12px 16px 12px 44px;
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
font-size: 14px;
background: var(--surface);
color: var(--on-surface);
}
.search-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: var(--on-surface-variant);
}
.category-filter {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.category-btn {
padding: 8px 16px;
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
background: var(--surface);
color: var(--on-surface-variant);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
text-transform: capitalize;
}
.category-btn:hover {
border-color: var(--primary);
background: var(--surface-variant);
transform: translateY(-1px);
}
.category-btn.active {
border-color: var(--primary);
background: var(--primary);
color: white;
}
.marketplace-stats {
text-align: center;
margin-bottom: var(--spacing-lg);
color: var(--on-surface-variant);
font-size: 14px;
}
.category-section {
margin-bottom: var(--spacing-xl);
}
.category-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--on-surface);
margin-bottom: var(--spacing-lg);
text-transform: capitalize;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.category-title::before {
content: '';
width: 4px;
height: 24px;
background: var(--primary);
border-radius: 2px;
}
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--spacing-lg);
}
.agent-card {
background: var(--surface);
border: 2px solid var(--outline-variant);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
transition: all 0.3s ease;
text-decoration: none;
color: inherit;
display: block;
position: relative;
overflow: hidden;
}
.agent-card:hover {
border-color: var(--primary);
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
background: var(--surface-variant);
}
.agent-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.agent-icon {
font-size: 2.5rem;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface-variant);
border-radius: var(--radius-md);
border: 2px solid var(--outline-variant);
}
.agent-info h3 {
font-size: 1.2rem;
font-weight: 600;
color: var(--on-surface);
margin: 0 0 var(--spacing-xs) 0;
}
.agent-price {
font-size: 1rem;
font-weight: 700;
color: var(--primary);
margin: 0;
}
.agent-description {
color: var(--on-surface-variant);
line-height: 1.5;
margin-bottom: var(--spacing-lg);
font-size: 14px;
}
.agent-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.agent-category {
background: var(--surface-variant);
color: var(--on-surface-variant);
padding: 4px 12px;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 500;
text-transform: capitalize;
}
.try-btn {
background: var(--primary);
color: white;
padding: 8px 16px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.try-btn:hover {
background: var(--primary-dark);
transform: scale(1.05);
}
.no-results {
text-align: center;
padding: var(--spacing-xl);
color: var(--on-surface-variant);
}
.no-results-icon {
font-size: 4rem;
margin-bottom: var(--spacing-md);
opacity: 0.5;
}
/* Responsive Design */
@media (max-width: 768px) {
.marketplace-title {
font-size: 2rem;
}
.marketplace-filters {
flex-direction: column;
align-items: stretch;
}
.search-box {
max-width: none;
}
.agents-grid {
grid-template-columns: 1fr;
}
.category-filter {
justify-content: center;
}
}
</style>
{% endblock %}
{% block content %}
<div class="agent-container">
<!-- Marketplace Header -->
<div class="marketplace-header">
<h1 class="marketplace-title">🤖 AI Agent Marketplace</h1>
<p class="marketplace-subtitle">
Discover powerful AI agents to automate your tasks, boost productivity, and streamline your workflow
</p>
</div>
<!-- Search Box -->
<div class="search-box" style="margin-bottom: var(--spacing-lg); max-width: 500px; margin-left: auto; margin-right: auto;">
<form method="GET" style="position: relative;">
<span class="search-icon">🔍</span>
<input type="text" name="search" class="search-input"
placeholder="Search agents..."
value="{{ search_query }}"
onchange="this.form.submit()">
{% if selected_category %}
<input type="hidden" name="category" value="{{ selected_category }}">
{% endif %}
</form>
</div>
<!-- Category Filter Buttons -->
<div class="category-filter" style="display: flex; gap: var(--spacing-sm); flex-wrap: wrap; justify-content: center; margin-bottom: var(--spacing-xl);">
<a href="{% url 'workflows:marketplace' %}"
class="category-btn {% if not selected_category %}active{% endif %}">
All
</a>
{% for category in all_categories %}
<a href="?category={{ category }}{% if search_query %}&search={{ search_query }}{% endif %}"
class="category-btn {% if selected_category == category %}active{% endif %}">
{{ category }}
</a>
{% endfor %}
</div>
<!-- Stats -->
<div class="marketplace-stats">
{% if search_query %}
Search results for "{{ search_query }}" •
{% endif %}
{% if selected_category %}
{{ selected_category|capfirst }} category •
{% endif %}
{{ total_agents }} agent{{ total_agents|pluralize }} available
</div>
<!-- Agents Grid -->
{% if agents_by_category %}
{% if not selected_category %}
<!-- Show all agents without category headings when "All" is selected -->
<div class="agents-grid">
{% for category, agents in agents_by_category.items %}
{% for agent in agents %}
<a href="{% url 'workflows:agent' agent.slug %}" class="agent-card">
<div class="agent-header">
<div class="agent-icon">{{ agent.icon }}</div>
<div class="agent-info">
<h3>{{ agent.name }}</h3>
<p class="agent-price">{{ agent.price }} AED</p>
</div>
</div>
<p class="agent-description">{{ agent.description }}</p>
<div class="agent-footer">
<span class="agent-category">{{ agent.category }}</span>
<button class="try-btn">Try Now →</button>
</div>
</a>
{% endfor %}
{% endfor %}
</div>
{% else %}
<!-- Show agents with category headings when specific category is selected -->
{% for category, agents in agents_by_category.items %}
<div class="category-section">
<h2 class="category-title">{{ category }}</h2>
<div class="agents-grid">
{% for agent in agents %}
<a href="{% url 'workflows:agent' agent.slug %}" class="agent-card">
<div class="agent-header">
<div class="agent-icon">{{ agent.icon }}</div>
<div class="agent-info">
<h3>{{ agent.name }}</h3>
<p class="agent-price">{{ agent.price }} AED</p>
</div>
</div>
<p class="agent-description">{{ agent.description }}</p>
<div class="agent-footer">
<span class="agent-category">{{ agent.category }}</span>
<button class="try-btn">Try Now →</button>
</div>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
{% else %}
<div class="no-results">
<div class="no-results-icon">🔍</div>
<h3>No agents found</h3>
<p>Try adjusting your search or browse all categories</p>
<a href="{% url 'workflows:marketplace' %}" class="btn btn-primary">Browse All Agents</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,341 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Social Ads Generator - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
<style>
/* Enhanced Social Ads Generator Styles - Hybrid Integration */
.form-textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
font-size: 14px;
line-height: 1.5;
transition: all 0.2s ease;
background: var(--surface);
color: var(--on-surface);
font-family: inherit;
resize: vertical;
min-height: 120px;
}
.form-textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.form-textarea:hover {
border-color: var(--on-surface-variant);
}
/* Enhanced Form Sections */
.section-container {
margin-bottom: var(--spacing-xl);
padding: var(--spacing-lg);
background: var(--surface-variant);
border-radius: var(--radius-md);
border: 1px solid var(--outline-variant);
}
.section-subtitle {
font-size: 16px;
font-weight: 600;
color: var(--on-surface);
margin: 0 0 var(--spacing-lg) 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.section-subtitle::before {
content: '';
width: 3px;
height: 16px;
background: var(--primary);
border-radius: 2px;
}
/* Error styling */
.form-textarea.error,
.form-input.error {
border-color: var(--error);
}
.form-error {
color: var(--error);
font-size: 12px;
margin-top: var(--spacing-xs);
font-weight: 500;
}
/* Enhanced Results Display */
.results-content {
background: var(--surface-variant);
border-radius: var(--radius-md);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
line-height: 1.7;
color: var(--on-surface);
font-size: 15px;
}
/* Results Typography */
.results-content h1,
.results-content h2,
.results-content h3 {
color: var(--primary);
font-weight: 700;
margin: var(--spacing-xl) 0 var(--spacing-md) 0;
line-height: 1.3;
}
.results-content h1 {
font-size: 24px;
border-bottom: 3px solid var(--primary);
padding-bottom: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.results-content h2 {
font-size: 20px;
margin-top: var(--spacing-xl);
position: relative;
padding-left: var(--spacing-md);
}
.results-content h2::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--primary);
border-radius: 2px;
}
.results-content h3 {
font-size: 18px;
color: var(--on-surface);
font-weight: 600;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-sm);
border-left: 4px solid var(--primary);
margin: var(--spacing-lg) 0 var(--spacing-md) 0;
}
.results-content strong {
color: var(--primary);
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;
}
.toast.info {
border-color: var(--primary);
background: #f0f9ff;
color: #0369a1;
}
/* Responsive Design */
@media (max-width: 768px) {
.toast {
left: 20px;
right: 20px;
max-width: none;
transform: translateY(-100%);
}
.toast.show {
transform: translateY(0);
}
.results-content {
padding: var(--spacing-md);
font-size: 14px;
}
.results-content h1 {
font-size: 20px;
}
.results-content h2 {
font-size: 18px;
}
.results-content h3 {
font-size: 16px;
padding: var(--spacing-sm) var(--spacing-md);
}
.section-container {
padding: var(--spacing-md);
}
}
</style>
{% endblock %}
{% block content %}
<script>
// Set data attributes for JavaScript access
document.body.setAttribute('data-user-authenticated', '{{ user.is_authenticated|yesno:"true,false" }}');
document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
</script>
<div class="agent-container">
<!-- Agent Header Component -->
{% include "workflows/components/agent_header.html" with agent_title="Social Ads Generator" agent_subtitle="Create compelling social media advertisements optimized for different platforms" %}
<!-- Quick Agent Access Panel Component -->
{% include "workflows/components/quick_agents_panel.html" %}
<!-- Main Agent Grid -->
<div class="agent-grid">
<!-- Social Ads Form Widget -->
<div class="agent-widget widget-large" style="flex: 1; margin-right: clamp(0px, var(--spacing-lg), 2vw);">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">📢</span>
Social Ads Details
</h3>
</div>
<div class="widget-content">
<form id="agentForm" method="POST">
{% csrf_token %}
<!-- Content Information Section -->
<div class="section-container content-info">
<h4 class="section-subtitle">📝 Content Information</h4>
<div class="form-group">
<label class="form-label" for="description">📝 Describe what you'd like to generate *</label>
<textarea id="description" name="description" class="form-textarea"
placeholder="Describe the product, service, or campaign you want to create an ad for. Include key features, target audience, and any specific messaging you want to emphasize."
required rows="4">Revolutionary AI-powered task management app that helps teams boost productivity by 300%. Features smart scheduling, automated workflows, and real-time collaboration. Perfect for startups and growing businesses looking to streamline operations.</textarea>
<div class="form-help">Provide clear, specific information about your product or service for better ad copy</div>
<div id="description-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label" for="language">🌐 Language</label>
<select id="language" name="language" class="form-input">
<option value="English" selected>English</option>
<option value="Arabic">Arabic (العربية)</option>
<option value="Spanish">Spanish (Español)</option>
<option value="French">French (Français)</option>
<option value="German">German (Deutsch)</option>
<option value="Chinese">Chinese (中文)</option>
</select>
<div class="form-help">Select the primary language for the ad copy</div>
</div>
</div>
<!-- Platform & Formatting Section -->
<div class="section-container platform-info">
<h4 class="section-subtitle">📱 Platform & Formatting</h4>
<div class="form-group">
<label class="form-label" for="social_platform">📱 For Social Media Platform *</label>
<select id="social_platform" name="social_platform" class="form-input" required>
<option value="">Select a platform...</option>
<option value="facebook">Facebook</option>
<option value="instagram" selected>Instagram</option>
<option value="linkedin">LinkedIn</option>
<option value="twitter">X (Twitter)</option>
<option value="tiktok">TikTok</option>
<option value="youtube">YouTube</option>
</select>
<div class="form-help">Choose the social media platform for optimization</div>
<div id="social_platform-error" class="form-error" style="display: none;"></div>
</div>
<div class="form-group">
<label class="form-label" for="include_emoji">😊 Include Emoji *</label>
<select id="include_emoji" name="include_emoji" class="form-input" required>
<option value="">Select an option...</option>
<option value="yes" selected>Yes</option>
<option value="no">No</option>
</select>
<div class="form-help">Whether to include emojis in the ad copy</div>
<div id="include_emoji-error" class="form-error" style="display: none;"></div>
</div>
</div>
<!-- Submit Button -->
<div style="margin-top: var(--spacing-lg);">
{% if user.is_authenticated %}
{% if user.wallet_balance >= agent_config.price %}
<button type="submit" class="btn btn-primary btn-full" id="generateBtn">
📢 Generate Social Ads ({{ agent_config.price }} AED)
</button>
{% else %}
<div style="background: #fef2f2; color: #dc2626; padding: var(--spacing-md); border-radius: var(--radius-md); margin-bottom: var(--spacing-md); font-size: 14px; font-weight: 500; text-align: center;">
Insufficient balance! You need {{ agent_config.price }} AED.
</div>
<a href="{% url 'wallet:wallet' %}" class="btn btn-primary btn-full" style="text-decoration: none;">
💰 Top Up Wallet
</a>
{% endif %}
{% else %}
<a href="{% url 'authentication:login' %}" class="btn btn-primary btn-full">
🔐 Login to Continue
</a>
{% endif %}
</div>
</form>
</div>
</div>
<!-- How It Works Widget -->
{% include "workflows/components/how_it_works_widget.html" with steps="social_ads" %}
</div>
<!-- Processing Status Component -->
{% include "workflows/components/processing_status.html" with status_title="Creating Social Ads..." status_text="Please wait while we generate your ad copy..." %}
<!-- Results Component -->
{% include "workflows/components/results_container.html" with results_title="Generated Social Ads" %}
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/workflows-core.js' %}?v={{ timestamp }}"></script>
<script src="{% static 'js/social-ads.js' %}?v={{ timestamp }}"></script>
{% endblock %}

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,20 +0,0 @@
from django.urls import path, re_path
from . import views
app_name = 'workflows'
urlpatterns = [
# Marketplace view at /agents/
path('', views.marketplace_view, name='marketplace'),
# Universal agent handler - matches any agent slug
re_path(r'^(?P<agent_slug>[\w-]+)/$', views.workflow_handler, name='agent'),
# API endpoints
path('api/process/', views.process_workflow_api, name='process_api'),
path('api/status/<uuid:request_id>/', views.workflow_status, name='status'),
# User workflow management
path('history/', views.user_workflows, name='history'),
path('analytics/', views.workflow_analytics, name='analytics'),
]

View File

@ -1,484 +0,0 @@
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django_ratelimit.decorators import ratelimit
from django_ratelimit import UNSAFE
import json
import time
import requests
from datetime import datetime
from .models import WorkflowRequest, WorkflowResponse, WorkflowAnalytics
from .config.agents import get_agent_config, format_message_for_n8n, get_available_agents, get_all_agents
import logging
logger = logging.getLogger(__name__)
def marketplace_view(request):
"""Agent marketplace using AGENT_CONFIGS - no database dependency"""
agents_data = get_all_agents()
# Group agents by category
agents_by_category = {}
all_categories = set()
for agent_slug, agent_config in agents_data.items():
category = agent_config.get('category', 'utilities')
all_categories.add(category)
if category not in agents_by_category:
agents_by_category[category] = []
# Add slug to agent data for URL generation
agent_data = agent_config.copy()
agent_data['slug'] = agent_slug
agents_by_category[category].append(agent_data)
# Filter by category if specified
selected_category = request.GET.get('category')
if selected_category and selected_category in all_categories:
agents_by_category = {selected_category: agents_by_category[selected_category]}
# Search functionality
search_query = request.GET.get('search', '').strip()
if search_query:
filtered_agents = {}
for category, agents in agents_by_category.items():
filtered_agents[category] = [
agent for agent in agents
if search_query.lower() in agent['name'].lower() or
search_query.lower() in agent['description'].lower()
]
agents_by_category = {k: v for k, v in filtered_agents.items() if v}
context = {
'agents_by_category': agents_by_category,
'all_categories': sorted(all_categories),
'selected_category': selected_category,
'search_query': search_query,
'total_agents': len(agents_data)
}
return render(request, 'workflows/marketplace.html', context)
def send_file_to_webhook(webhook_url, uploaded_file, form_data, timeout=60):
"""Send file to N8N webhook endpoint"""
logger.info(f"🔍 DEBUG: send_file_to_webhook called!")
logger.info(f"🔍 DEBUG: webhook_url={webhook_url}")
logger.info(f"🔍 DEBUG: uploaded_file={uploaded_file}")
logger.info(f"🔍 DEBUG: form_data={form_data}")
try:
# Reset file pointer to beginning
uploaded_file.seek(0)
file_content = uploaded_file.read()
logger.info(f"✅ Sending file to webhook: {webhook_url}")
logger.info(f"✅ File size: {len(file_content)} bytes")
logger.info(f"✅ File name: {uploaded_file.name}")
# Prepare multipart form data
files = {
'file': (uploaded_file.name, file_content, 'application/pdf')
}
# Add analysis type if provided
data = {}
if 'analysisType' in form_data:
data['analysisType'] = form_data['analysisType']
start_time = time.time()
response = requests.post(webhook_url, files=files, data=data, timeout=timeout)
processing_time = time.time() - start_time
logger.info(f"Webhook response status: {response.status_code}")
logger.info(f"Processing time: {processing_time:.2f}s")
logger.info(f"Response preview: {response.text[:200]}...")
response.raise_for_status()
# Parse JSON response from webhook
if response.text.strip():
try:
result_data = response.json()
logger.info(f"Webhook returned JSON: {result_data}")
# Store the analysis results in the workflow request
if 'sections' in result_data:
# Success response with analysis sections
return {'success': True, 'data': result_data}
elif result_data.get('status') == 'error':
# Error response from N8N
logger.error(f"N8N webhook error: {result_data.get('error_message', 'Unknown error')}")
return {'success': False, 'error': result_data.get('error_message', 'Analysis failed')}
else:
# Unexpected response format
logger.warning(f"Unexpected webhook response format: {result_data}")
return {'success': True, 'data': result_data}
except ValueError as e:
logger.error(f"Invalid JSON response from webhook: {e}")
logger.info(f"Raw response: {response.text[:500]}")
return {'success': False, 'error': 'Invalid response from analysis service'}
else:
logger.warning("Webhook returned empty response")
return {'success': False, 'error': 'Empty response from analysis service'}
except requests.exceptions.Timeout:
logger.error(f"Webhook timeout after {timeout}s")
return False
except requests.exceptions.ConnectionError:
logger.error(f"Cannot connect to webhook: {webhook_url}")
return False
except requests.exceptions.RequestException as e:
logger.error(f"Webhook request failed: {e}")
return False
except Exception as e:
logger.error(f"Unexpected error sending to webhook: {e}")
return False
@login_required
def workflow_handler(request, agent_slug):
"""Universal handler for all workflow agents with individual templates"""
# Get agent configuration
agent_config = get_agent_config(agent_slug)
if not agent_config:
raise Http404("Agent configuration not found")
# Agent config serves as the agent data (no database dependency)
agent = {
'slug': agent_slug,
'name': agent_config['name'],
'price': agent_config['price'],
'icon': agent_config['icon'],
'description': agent_config['description']
}
if request.method == 'POST':
return process_workflow_request(request, agent_slug, agent_config, agent)
# Determine template path - use individual templates
template_mapping = {
'social-ads-generator': 'workflows/social-ads-generator.html',
'job-posting-generator': 'workflows/job-posting-generator.html',
'five-whys-analyzer': 'workflows/five-whys-analyzer.html',
'weather-reporter': 'workflows/weather-reporter.html',
'template-demo': 'workflows/agent-template-starter.html',
'data-analyzer': 'workflows/data-analyzer.html',
}
template_name = template_mapping.get(agent_slug)
if not template_name:
raise Http404("Template not found for agent")
# Add timestamp for cache busting and available agents for navigation
context = {
'agent': agent,
'agent_config': agent_config,
'available_agents': get_available_agents(),
'timestamp': int(time.time()),
}
return render(request, template_name, context)
def process_workflow_request(request, agent_slug, agent_config, agent):
"""Process workflow request (called from workflow_handler)"""
logger.info(f"🔍 DEBUG: process_workflow_request called with agent_slug={agent_slug}")
logger.info(f"🔍 DEBUG: request.method={request.method}")
logger.info(f"🔍 DEBUG: request.FILES={dict(request.FILES)}")
logger.info(f"🔍 DEBUG: request.POST={dict(request.POST)}")
# Template mapping for error returns
template_mapping = {
'social-ads-generator': 'workflows/social-ads-generator.html',
'job-posting-generator': 'workflows/job-posting-generator.html',
'five-whys-analyzer': 'workflows/five-whys-analyzer.html',
'weather-reporter': 'workflows/weather-reporter.html',
'template-demo': 'workflows/agent-template-starter.html',
'data-analyzer': 'workflows/data-analyzer.html',
}
try:
# Extract form data
form_data = {}
for key, value in request.POST.items():
if key != 'csrfmiddlewaretoken':
form_data[key] = value
# Handle file uploads (but don't store file objects in form_data for JSON serialization)
logger.info(f"DEBUG: request.FILES = {request.FILES}")
logger.info(f"DEBUG: request.FILES.keys() = {list(request.FILES.keys())}")
uploaded_files = {}
for key, file in request.FILES.items():
logger.info(f"DEBUG: Found file - {key}: {file.name} ({file.size} bytes)")
# Store file metadata only (not the file object itself)
form_data[f"{key}_name"] = file.name
form_data[f"{key}_size"] = file.size
# Keep actual file object separate for processing
uploaded_files[key] = file
logger.info(f"DEBUG: Final form_data keys = {list(form_data.keys())}")
logger.info(f"DEBUG: Uploaded files = {list(uploaded_files.keys())}")
# Basic validation - ensure we have form data
if not form_data:
context = {
'agent': agent,
'agent_config': agent_config,
'error': 'No form data provided.',
'timestamp': int(time.time()),
}
template_name = template_mapping.get(agent_slug)
return render(request, template_name, context)
# Check user balance
if not request.user.has_sufficient_balance(agent_config['price']):
context = {
'agent': agent,
'agent_config': agent_config,
'balance_error': f'Insufficient balance. You need {agent_config["price"]} AED.',
'form_data': form_data,
'timestamp': int(time.time()),
}
template_name = template_mapping.get(agent_slug)
return render(request, template_name, context)
# Create workflow request record
workflow_request = WorkflowRequest.objects.create(
user=request.user,
agent_slug=agent_slug,
input_data=form_data,
status='processing'
)
# Actually send request to webhook (for agents that support file upload)
logger.info(f"🔍 DEBUG: Checking webhook conditions - agent_slug={agent_slug}, has_file={'document_file' in uploaded_files or 'file' in uploaded_files}")
file_upload_agents = ['data-analyzer']
has_file = 'file' in uploaded_files or 'document_file' in uploaded_files
if agent_slug in file_upload_agents and has_file:
try:
# Check if this is an AJAX request (like the original system)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# Send file to webhook in background and return JSON response
# Get the uploaded file (different field names for different agents)
uploaded_file = uploaded_files.get('file') or uploaded_files.get('document_file')
webhook_result = send_file_to_webhook(
agent_config['webhook_url'],
uploaded_file,
form_data
)
if webhook_result and webhook_result.get('success'):
# Deduct user balance for successful processing
request.user.deduct_balance(agent_config['price'])
workflow_request.status = 'completed'
workflow_request.save()
# Store analysis results in the workflow request
analysis_data = webhook_result.get('data', {})
try:
# Create WorkflowResponse with analysis data
WorkflowResponse.objects.create(
request=workflow_request,
formatted_output=analysis_data,
success=True,
processing_time=1.53 # Could get this from webhook timing
)
except Exception as resp_error:
logger.warning(f"Could not save response data: {resp_error}")
# Continue anyway - the main processing worked
return JsonResponse({
'success': True,
'request_id': str(workflow_request.id),
'wallet_balance': float(request.user.wallet_balance),
'report_text': analysis_data.get('sections', [{}])[0].get('content', 'Analysis completed'),
'analysis_results': analysis_data
})
else:
workflow_request.status = 'failed'
workflow_request.save()
error_msg = webhook_result.get('error', 'Failed to process file') if webhook_result else 'Connection failed'
return JsonResponse({
'success': False,
'error': error_msg
})
else:
# Non-AJAX request, return HTML template
context = {
'agent': agent,
'agent_config': agent_config,
'processing': True,
'request_id': workflow_request.id,
'timestamp': int(time.time()),
}
except Exception as e:
logger.error(f"Webhook error for {agent_slug}: {e}")
workflow_request.status = 'failed'
workflow_request.save()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': False,
'error': 'Service temporarily unavailable. Please try again later.'
})
else:
context = {
'agent': agent,
'agent_config': agent_config,
'error': 'Service temporarily unavailable. Please try again later.',
'timestamp': int(time.time()),
}
else:
# For other agents or no file upload, show processing message
context = {
'agent': agent,
'agent_config': agent_config,
'processing': True,
'request_id': workflow_request.id,
'timestamp': int(time.time()),
}
template_name = template_mapping.get(agent_slug)
return render(request, template_name, context)
except Exception as e:
logger.error(f"Workflow processing error for {agent_slug}: {e}", exc_info=True)
context = {
'agent': agent,
'agent_config': agent_config,
'error': 'An error occurred while processing your request. Please try again.',
'timestamp': int(time.time()),
}
template_name = template_mapping.get(agent_slug)
return render(request, template_name, context)
@login_required
@require_http_methods(["POST"])
@ratelimit(key='user', rate='20/m', method='POST', block=False)
def process_workflow_api(request):
"""API endpoint for processing workflows (alternative to direct N8N calls)"""
# Check if rate limited
if getattr(request, 'limited', False):
logger.warning(f"Workflow API rate limit exceeded for user {request.user.id}")
return JsonResponse({'error': 'Too many requests. Please wait a moment.'}, status=429)
try:
# Parse JSON request
data = json.loads(request.body)
agent_slug = data.get('agent_slug')
form_data = data.get('form_data', {})
if not agent_slug:
return JsonResponse({'error': 'Agent slug is required'}, status=400)
# Get agent configuration
agent_config = get_agent_config(agent_slug)
if not agent_config:
return JsonResponse({'error': 'Agent not found'}, status=404)
# Basic validation - ensure we have form data
if not form_data:
return JsonResponse({'error': 'No form data provided'}, status=400)
# Check user balance
if not request.user.has_sufficient_balance(agent_config['price']):
return JsonResponse({
'error': 'Insufficient balance',
'required': float(agent_config['price']),
'current': float(request.user.wallet_balance)
}, status=400)
# Create workflow request
workflow_request = WorkflowRequest.objects.create(
user=request.user,
agent_slug=agent_slug,
input_data=form_data,
status='processing'
)
# In a real implementation, this would call N8N
# For now, return processing status
return JsonResponse({
'success': True,
'request_id': str(workflow_request.id),
'status': 'processing',
'message': 'Request received and processing'
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON payload'}, status=400)
except Exception as e:
logger.error(f"Workflow API error: {e}", exc_info=True)
return JsonResponse({'error': 'Internal server error'}, status=500)
@login_required
def workflow_status(request, request_id):
"""Get workflow processing status"""
try:
workflow_request = get_object_or_404(
WorkflowRequest,
id=request_id,
user=request.user
)
response_data = {
'request_id': str(workflow_request.id),
'status': workflow_request.status,
'created_at': workflow_request.created_at.isoformat(),
}
# Include response data if completed
if hasattr(workflow_request, 'response') and workflow_request.response:
response_data['output'] = workflow_request.response.formatted_output
response_data['processing_time'] = float(workflow_request.response.processing_time or 0)
response_data['success'] = workflow_request.response.success
return JsonResponse(response_data)
except Exception as e:
logger.error(f"Status check error: {e}", exc_info=True)
return JsonResponse({'error': 'Failed to get status'}, status=500)
@login_required
def user_workflows(request):
"""Show user's workflow history"""
workflows = WorkflowRequest.objects.filter(user=request.user).order_by('-created_at')[:50]
context = {
'workflows': workflows,
}
return render(request, 'workflows/history.html', context)
@login_required
def workflow_analytics(request):
"""Show workflow analytics for the user"""
# Get user's workflow analytics
analytics = WorkflowAnalytics.objects.filter(user=request.user).order_by('-date')[:30]
# Calculate summary statistics
total_workflows = WorkflowRequest.objects.filter(user=request.user).count()
successful_workflows = WorkflowAnalytics.objects.filter(user=request.user, success=True).count()
success_rate = (successful_workflows / total_workflows * 100) if total_workflows > 0 else 0
context = {
'analytics': analytics,
'total_workflows': total_workflows,
'successful_workflows': successful_workflows,
'success_rate': success_rate,
}
return render(request, 'workflows/analytics.html', context)