Add frontend, agents apps and enhance API with authentication

- Add new Django apps: agents and frontend with URL routing
- Configure static files and templates directories in settings
- Add CSRF exemption and authentication to user endpoints
- Switch Stripe currency from USD to AED
- Add user management script and static assets
- Include REST framework token authentication

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
thecyberlearn 2025-07-30 23:22:59 +05:30
parent 060c3a9c78
commit 213486946e
39 changed files with 4327 additions and 5 deletions

0
agents/__init__.py Normal file
View File

112
agents/admin.py Normal file
View File

@ -0,0 +1,112 @@
from django.contrib import admin
from django.utils.html import format_html
from .models import AgentCategory, Agent, AgentExecution
@admin.register(AgentCategory)
class AgentCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'agent_count', 'is_active', 'sort_order', 'created_at')
list_filter = ('is_active', 'created_at')
search_fields = ('name', 'description')
prepopulated_fields = {'slug': ('name',)}
ordering = ('sort_order', 'name')
def agent_count(self, obj):
return obj.agents.filter(is_active=True).count()
agent_count.short_description = 'Active Agents'
@admin.register(Agent)
class AgentAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'price', 'usage_count', 'is_active', 'is_featured', 'created_at')
list_filter = ('category', 'is_active', 'is_featured', 'created_at')
search_fields = ('name', 'description', 'short_description')
prepopulated_fields = {'slug': ('name',)}
ordering = ('-is_featured', '-usage_count', 'name')
fieldsets = (
('Basic Information', {
'fields': ('name', 'slug', 'category', 'price', 'icon')
}),
('Descriptions', {
'fields': ('short_description', 'description')
}),
('Form Configuration', {
'fields': ('form_schema',),
'classes': ('collapse',),
'description': 'JSON schema defining the form fields for this agent'
}),
('n8n Integration', {
'fields': ('n8n_webhook_url', 'result_format'),
'classes': ('collapse',)
}),
('Status & Settings', {
'fields': ('is_active', 'is_featured')
}),
('Statistics', {
'fields': ('usage_count',),
'classes': ('collapse',)
})
)
readonly_fields = ('usage_count',)
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
# Add help text for form_schema field
if 'form_schema' in form.base_fields:
form.base_fields['form_schema'].help_text = format_html('''
<p>JSON schema example:</p>
<pre>{
"fields": [
{
"name": "description",
"type": "textarea",
"label": "Content Description",
"required": true,
"placeholder": "Describe what you want to generate..."
},
{
"name": "language",
"type": "select",
"label": "Language",
"options": ["English", "Arabic"],
"required": true
}
]
}</pre>
''')
return form
@admin.register(AgentExecution)
class AgentExecutionAdmin(admin.ModelAdmin):
list_display = ('user', 'agent', 'status', 'price_paid', 'created_at', 'completed_at')
list_filter = ('status', 'agent', 'created_at')
search_fields = ('user__email', 'agent__name')
ordering = ('-created_at',)
fieldsets = (
('Execution Info', {
'fields': ('user', 'agent', 'status', 'price_paid')
}),
('Input Data', {
'fields': ('input_data',),
'classes': ('collapse',)
}),
('Output Data', {
'fields': ('output_data', 'error_message'),
'classes': ('collapse',)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at', 'completed_at'),
'classes': ('collapse',)
})
)
readonly_fields = ('created_at', 'updated_at')
def get_readonly_fields(self, request, obj=None):
if obj: # Editing existing object
return self.readonly_fields + ('user', 'agent', 'price_paid')
return self.readonly_fields

6
agents/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AgentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'agents'

View File

View File

View File

@ -0,0 +1,373 @@
from django.core.management.base import BaseCommand
from agents.models import AgentCategory, Agent
class Command(BaseCommand):
help = 'Create sample agents for testing the marketplace'
def handle(self, *args, **options):
self.stdout.write('Creating sample agents...')
# Create categories
categories_data = [
{
'name': 'Content Creation',
'slug': 'content-creation',
'description': 'AI agents for creating various types of content',
'icon': '✍️'
},
{
'name': 'Business & Marketing',
'slug': 'business-marketing',
'description': 'AI agents for business and marketing tasks',
'icon': '📈'
},
{
'name': 'Data & Analytics',
'slug': 'data-analytics',
'description': 'AI agents for data analysis and insights',
'icon': '📊'
},
{
'name': 'HR & Recruitment',
'slug': 'hr-recruitment',
'description': 'AI agents for human resources and recruitment',
'icon': '👥'
}
]
categories = {}
for cat_data in categories_data:
category, created = AgentCategory.objects.get_or_create(
slug=cat_data['slug'],
defaults=cat_data
)
categories[cat_data['slug']] = category
if created:
self.stdout.write(f'Created category: {category.name}')
# Create sample agents
agents_data = [
{
'name': 'Professional Job Posting Creator',
'slug': 'job-posting-creator',
'short_description': 'Create professional, attractive job postings that attract top talent',
'description': 'This AI agent helps you craft compelling job postings that stand out in the competitive talent market. It analyzes your requirements and creates comprehensive job descriptions with proper formatting, compelling language, and industry best practices.',
'category': categories['hr-recruitment'],
'price': 25.00,
'icon': '💼',
'is_featured': True,
'n8n_webhook_url': 'http://localhost:5678/webhook/job-posting-webhook',
'form_schema': {
'fields': [
{
'name': 'position_title',
'label': 'Position Title',
'type': 'text',
'required': True,
'placeholder': 'e.g., Senior Full Stack Developer'
},
{
'name': 'company_name',
'label': 'Company Name',
'type': 'text',
'required': True,
'placeholder': 'e.g., Quantum Technologies Inc.'
},
{
'name': 'job_description',
'label': 'Job Description',
'type': 'textarea',
'required': True,
'placeholder': 'Describe the role, responsibilities, and requirements...'
},
{
'name': 'seniority_level',
'label': 'Seniority Level',
'type': 'select',
'required': True,
'options': ['Entry Level', 'Mid Level', 'Senior', 'Executive']
},
{
'name': 'contract_type',
'label': 'Contract Type',
'type': 'select',
'required': True,
'options': ['Full-time', 'Part-time', 'Contract', 'Internship', 'Freelance']
},
{
'name': 'location',
'label': 'Location',
'type': 'text',
'required': True,
'placeholder': 'e.g., Dubai, UAE (Remote)'
},
{
'name': 'language',
'label': 'Language',
'type': 'select',
'required': True,
'options': ['English', 'Arabic', 'Spanish', 'French', 'German']
}
]
}
},
{
'name': 'Social Media Ad Creator',
'slug': 'social-media-ad-creator',
'short_description': 'Generate compelling social media advertisements for multiple platforms',
'description': 'Transform your product or service into engaging social media advertisements. This agent creates platform-specific ad copy, suggests visuals, and optimizes for maximum engagement and conversions across Facebook, Instagram, LinkedIn, and Twitter.',
'category': categories['business-marketing'],
'price': 35.00,
'icon': '📱',
'is_featured': True,
'n8n_webhook_url': 'http://localhost:5678/webhook/social-ads-webhook',
'form_schema': {
'fields': [
{
'name': 'product_service',
'label': 'Product/Service Name',
'type': 'text',
'required': True,
'placeholder': 'e.g., Premium Fitness App'
},
{
'name': 'target_audience',
'label': 'Target Audience',
'type': 'text',
'required': True,
'placeholder': 'e.g., Fitness enthusiasts aged 25-40'
},
{
'name': 'key_benefits',
'label': 'Key Benefits/Features',
'type': 'textarea',
'required': True,
'placeholder': 'List the main benefits and features...'
},
{
'name': 'platform',
'label': 'Social Media Platform',
'type': 'select',
'required': True,
'options': ['Facebook', 'Instagram', 'LinkedIn', 'Twitter', 'TikTok', 'All Platforms']
},
{
'name': 'ad_objective',
'label': 'Campaign Objective',
'type': 'select',
'required': True,
'options': ['Brand Awareness', 'Lead Generation', 'Sales/Conversions', 'Traffic', 'Engagement']
}
]
}
},
{
'name': 'Business Data Analyzer',
'slug': 'business-data-analyzer',
'short_description': 'Analyze business data and generate actionable insights with visualizations',
'description': 'Upload your business data and get comprehensive analysis with actionable insights. This agent processes various data formats, identifies trends, creates visualizations, and provides strategic recommendations to improve your business performance.',
'category': categories['data-analytics'],
'price': 45.00,
'icon': '📈',
'is_featured': False,
'n8n_webhook_url': 'http://localhost:5678/webhook/data-analyzer-webhook',
'form_schema': {
'fields': [
{
'name': 'data_type',
'label': 'Data Type',
'type': 'select',
'required': True,
'options': ['Sales Data', 'Customer Data', 'Financial Data', 'Marketing Data', 'Operational Data']
},
{
'name': 'data_description',
'label': 'Data Description',
'type': 'textarea',
'required': True,
'placeholder': 'Describe your data and what insights you are looking for...'
},
{
'name': 'time_period',
'label': 'Time Period',
'type': 'select',
'required': True,
'options': ['Last Month', 'Last Quarter', 'Last 6 Months', 'Last Year', 'Custom Range']
},
{
'name': 'analysis_focus',
'label': 'Analysis Focus',
'type': 'select',
'required': True,
'options': ['Trends & Patterns', 'Performance Metrics', 'Comparative Analysis', 'Predictive Insights', 'All Areas']
}
]
}
},
{
'name': 'Blog Content Generator',
'slug': 'blog-content-generator',
'short_description': 'Create SEO-optimized blog posts and articles on any topic',
'description': 'Generate high-quality, SEO-optimized blog content that engages your audience and drives traffic. This agent researches topics, creates outlines, writes compelling content, and suggests relevant keywords and meta descriptions.',
'category': categories['content-creation'],
'price': 30.00,
'icon': '📝',
'is_featured': False,
'n8n_webhook_url': 'http://localhost:5678/webhook/blog-generator-webhook',
'form_schema': {
'fields': [
{
'name': 'topic',
'label': 'Blog Topic',
'type': 'text',
'required': True,
'placeholder': 'e.g., Future of Artificial Intelligence in Healthcare'
},
{
'name': 'target_keywords',
'label': 'Target Keywords',
'type': 'text',
'required': False,
'placeholder': 'e.g., AI healthcare, medical AI, healthcare technology'
},
{
'name': 'word_count',
'label': 'Target Word Count',
'type': 'select',
'required': True,
'options': ['500-800 words', '800-1200 words', '1200-1800 words', '1800+ words']
},
{
'name': 'tone',
'label': 'Writing Tone',
'type': 'select',
'required': True,
'options': ['Professional', 'Conversational', 'Academic', 'Casual', 'Technical']
},
{
'name': 'audience',
'label': 'Target Audience',
'type': 'text',
'required': True,
'placeholder': 'e.g., Healthcare professionals, Tech enthusiasts, General readers'
}
]
}
},
{
'name': 'Email Marketing Campaign Creator',
'slug': 'email-campaign-creator',
'short_description': 'Design and write effective email marketing campaigns that convert',
'description': 'Create compelling email marketing campaigns that drive engagement and conversions. This agent designs email sequences, writes persuasive copy, suggests subject lines, and optimizes for deliverability and click-through rates.',
'category': categories['business-marketing'],
'price': 40.00,
'icon': '📧',
'is_featured': False,
'n8n_webhook_url': 'http://localhost:5678/webhook/email-campaign-webhook',
'form_schema': {
'fields': [
{
'name': 'campaign_goal',
'label': 'Campaign Goal',
'type': 'select',
'required': True,
'options': ['Product Launch', 'Lead Nurturing', 'Customer Retention', 'Sales Promotion', 'Event Promotion']
},
{
'name': 'product_service',
'label': 'Product/Service',
'type': 'text',
'required': True,
'placeholder': 'What are you promoting?'
},
{
'name': 'target_audience',
'label': 'Target Audience',
'type': 'text',
'required': True,
'placeholder': 'Describe your email subscribers/target audience'
},
{
'name': 'email_count',
'label': 'Number of Emails',
'type': 'select',
'required': True,
'options': ['Single Email', '3-Email Series', '5-Email Series', '7-Email Series']
},
{
'name': 'brand_voice',
'label': 'Brand Voice',
'type': 'select',
'required': True,
'options': ['Professional', 'Friendly', 'Authoritative', 'Playful', 'Luxury']
}
]
}
},
{
'name': 'Resume & CV Optimizer',
'slug': 'resume-cv-optimizer',
'short_description': 'Optimize resumes and CVs for specific job applications and ATS systems',
'description': 'Transform your resume into a powerful job-winning tool. This agent analyzes job descriptions, optimizes your resume for ATS systems, suggests improvements, and tailors content to match specific positions and industries.',
'category': categories['hr-recruitment'],
'price': 20.00,
'icon': '📄',
'is_featured': False,
'n8n_webhook_url': 'http://localhost:5678/webhook/resume-optimizer-webhook',
'form_schema': {
'fields': [
{
'name': 'current_resume',
'label': 'Current Resume Content',
'type': 'textarea',
'required': True,
'placeholder': 'Paste your current resume content here...'
},
{
'name': 'job_description',
'label': 'Target Job Description',
'type': 'textarea',
'required': True,
'placeholder': 'Paste the job description you are applying for...'
},
{
'name': 'industry',
'label': 'Industry',
'type': 'select',
'required': True,
'options': ['Technology', 'Finance', 'Healthcare', 'Marketing', 'Sales', 'Education', 'Engineering', 'Other']
},
{
'name': 'experience_level',
'label': 'Experience Level',
'type': 'select',
'required': True,
'options': ['Entry Level (0-2 years)', 'Mid Level (3-5 years)', 'Senior (6-10 years)', 'Executive (10+ years)']
}
]
}
}
]
# Create agents
for agent_data in agents_data:
agent, created = Agent.objects.get_or_create(
slug=agent_data['slug'],
defaults=agent_data
)
if created:
self.stdout.write(f'Created agent: {agent.name}')
else:
# Update existing agent with new data
for key, value in agent_data.items():
if key != 'slug':
setattr(agent, key, value)
agent.save()
self.stdout.write(f'Updated agent: {agent.name}')
self.stdout.write(
self.style.SUCCESS(
f'Successfully created/updated {len(categories_data)} categories and {len(agents_data)} agents'
)
)

View File

@ -0,0 +1,79 @@
# Generated by Django 5.0.8 on 2025-07-30 10:09
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='AgentCategory',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('slug', models.SlugField(unique=True)),
('description', models.TextField(blank=True)),
('icon', models.CharField(blank=True, help_text='Icon name or emoji', max_length=50)),
('is_active', models.BooleanField(default=True)),
('sort_order', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name_plural': 'Agent Categories',
'ordering': ['sort_order', 'name'],
},
),
migrations.CreateModel(
name='Agent',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=200)),
('slug', models.SlugField(unique=True)),
('description', models.TextField()),
('short_description', models.CharField(help_text='Brief description for cards', max_length=255)),
('price', models.DecimalField(decimal_places=2, help_text='Price in AED', max_digits=10)),
('icon', models.CharField(blank=True, help_text='Icon name or emoji', max_length=50)),
('form_schema', models.JSONField(default=dict, help_text='JSON schema defining the form fields for this agent')),
('n8n_webhook_url', models.URLField(help_text='n8n webhook URL for this agent')),
('result_format', models.JSONField(blank=True, default=dict, help_text='Configuration for formatting the response from n8n')),
('is_active', models.BooleanField(default=True)),
('is_featured', models.BooleanField(default=False)),
('usage_count', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agents', to='agents.agentcategory')),
],
options={
'ordering': ['-is_featured', '-usage_count', 'name'],
},
),
migrations.CreateModel(
name='AgentExecution',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('input_data', models.JSONField(help_text='Form data submitted by user')),
('output_data', models.JSONField(blank=True, help_text='Response from n8n workflow', null=True)),
('price_paid', models.DecimalField(decimal_places=2, max_digits=10)),
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('success', 'Success'), ('failed', 'Failed')], default='pending', max_length=20)),
('error_message', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='agents.agent')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_executions', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
]

View File

113
agents/models.py Normal file
View File

@ -0,0 +1,113 @@
import uuid
from django.db import models
from django.conf import settings
from django.utils.text import slugify
from decimal import Decimal
class AgentCategory(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
icon = models.CharField(max_length=50, blank=True, help_text="Icon name or emoji")
is_active = models.BooleanField(default=True)
sort_order = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['sort_order', 'name']
verbose_name_plural = 'Agent Categories'
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Agent(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = models.TextField()
short_description = models.CharField(max_length=255, help_text="Brief description for cards")
category = models.ForeignKey(AgentCategory, on_delete=models.CASCADE, related_name='agents')
price = models.DecimalField(max_digits=10, decimal_places=2, help_text="Price in AED")
icon = models.CharField(max_length=50, blank=True, help_text="Icon name or emoji")
# Dynamic form configuration
form_schema = models.JSONField(
default=dict,
help_text="JSON schema defining the form fields for this agent"
)
# n8n integration
n8n_webhook_url = models.URLField(help_text="n8n webhook URL for this agent")
# Response configuration
result_format = models.JSONField(
default=dict,
blank=True,
help_text="Configuration for formatting the response from n8n"
)
# Status and metadata
is_active = models.BooleanField(default=True)
is_featured = models.BooleanField(default=False)
usage_count = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-is_featured', '-usage_count', 'name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def increment_usage(self):
self.usage_count += 1
self.save(update_fields=['usage_count'])
class AgentExecution(models.Model):
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('success', 'Success'),
('failed', 'Failed'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='agent_executions')
agent = models.ForeignKey(Agent, on_delete=models.CASCADE, related_name='executions')
# Execution data
input_data = models.JSONField(help_text="Form data submitted by user")
output_data = models.JSONField(null=True, blank=True, help_text="Response from n8n workflow")
# Billing
price_paid = models.DecimalField(max_digits=10, decimal_places=2)
# Status tracking
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
error_message = models.TextField(null=True, blank=True)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
completed_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.user.email} - {self.agent.name} - {self.status}"

3
agents/tests.py Normal file
View File

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

13
agents/urls.py Normal file
View File

@ -0,0 +1,13 @@
from django.urls import path
from . import views
urlpatterns = [
# Agent execution endpoints
path('execute/', views.execute_agent, name='execute_agent'),
path('executions/', views.get_agent_executions, name='agent_executions'),
path('executions/<uuid:execution_id>/', views.get_execution_detail, name='execution_detail'),
# Agent listing and details
path('list/', views.list_agents, name='list_agents'),
path('detail/<slug:slug>/', views.get_agent_detail, name='agent_api_detail'),
]

367
agents/views.py Normal file
View File

@ -0,0 +1,367 @@
import uuid
import json
import requests
from decimal import Decimal
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from .models import Agent, AgentExecution
from users.models import User
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@csrf_exempt
def execute_agent(request):
"""Execute an agent with user input data"""
try:
# Get request data
agent_slug = request.data.get('agent_slug')
input_data = request.data.get('input_data', {})
if not agent_slug:
return Response(
{'error': 'agent_slug is required'},
status=status.HTTP_400_BAD_REQUEST
)
# Get the agent
agent = get_object_or_404(Agent, slug=agent_slug, is_active=True)
# Check user wallet balance
user = request.user
if user.wallet_balance < agent.price:
return Response(
{'error': f'Insufficient balance. You need {agent.price} AED but have {user.wallet_balance} AED. Please top up your wallet.'},
status=status.HTTP_400_BAD_REQUEST
)
# Create agent execution record
execution = AgentExecution.objects.create(
user=user,
agent=agent,
input_data=input_data,
price_paid=agent.price,
status='processing'
)
# Deduct amount from wallet
user.deduct_balance(agent.price)
# Generate session ID for n8n
session_id = f"session_{int(timezone.now().timestamp() * 1000)}_{execution.id.hex[:8]}"
# Format input data as message text
message_text = format_input_data_as_text(input_data, agent)
# Prepare webhook payload matching n8n format
webhook_payload = {
"sessionId": session_id,
"message": {
"text": message_text
},
"webhookUrl": agent.n8n_webhook_url,
"executionMode": "production",
"agentId": str(agent.id),
"executionId": str(execution.id),
"userId": str(user.id)
}
try:
# Send request to n8n webhook
response = requests.post(
agent.n8n_webhook_url,
json=webhook_payload,
headers={
'Content-Type': 'application/json',
'User-Agent': 'NetCop-AI-Agent/1.0'
},
timeout=30
)
if response.status_code == 200:
# Parse n8n response
try:
response_data = response.json()
except:
response_data = {"raw_response": response.text}
# Update execution with success
execution.output_data = response_data
execution.status = 'success'
execution.completed_at = timezone.now()
execution.save()
# Increment agent usage count
agent.increment_usage()
return Response({
'status': 'success',
'execution_id': str(execution.id),
'output_data': response_data,
'price_paid': float(agent.price),
'remaining_balance': float(user.wallet_balance)
})
else:
# Handle n8n error with user-friendly message
try:
error_data = response.json()
if response.status_code == 404:
user_error = f"This AI agent is temporarily unavailable. Please try again later or contact support."
elif 'webhook' in error_data.get('message', '').lower():
user_error = f"AI service is currently offline. Please try again in a few minutes."
else:
user_error = f"AI processing failed. Please try again or contact support."
except:
user_error = f"AI service is currently unavailable. Please try again later."
# Log technical details for debugging
technical_error = f"n8n webhook failed with status {response.status_code}: {response.text}"
print(f"Agent execution error: {technical_error}")
# Update execution with technical error for admin
execution.status = 'failed'
execution.error_message = technical_error
execution.completed_at = timezone.now()
execution.save()
# Refund user wallet
user.add_balance(agent.price)
return Response(
{'error': user_error},
status=status.HTTP_502_BAD_GATEWAY
)
except requests.exceptions.Timeout:
user_error = "AI processing is taking longer than expected. Please try again."
technical_error = "n8n webhook request timed out"
print(f"Agent execution timeout: {technical_error}")
execution.status = 'failed'
execution.error_message = technical_error
execution.completed_at = timezone.now()
execution.save()
# Refund user wallet
user.add_balance(agent.price)
return Response(
{'error': user_error},
status=status.HTTP_504_GATEWAY_TIMEOUT
)
except requests.exceptions.RequestException as e:
user_error = "Unable to connect to AI service. Please check your internet connection and try again."
technical_error = f"Failed to connect to n8n webhook: {str(e)}"
print(f"Agent execution connection error: {technical_error}")
execution.status = 'failed'
execution.error_message = technical_error
execution.completed_at = timezone.now()
execution.save()
# Refund user wallet
user.add_balance(agent.price)
return Response(
{'error': user_error},
status=status.HTTP_502_BAD_GATEWAY
)
except Exception as e:
return Response(
{'error': f'Internal server error: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def format_input_data_as_text(input_data, agent):
"""Format user input data as a natural text message for n8n"""
# Try to create a natural language description
if not input_data:
return f"Execute {agent.name} with default parameters."
# Get form schema for context
form_schema = agent.form_schema
field_labels = {}
if form_schema and 'fields' in form_schema:
for field in form_schema['fields']:
field_labels[field['name']] = field.get('label', field['name'])
# Build natural text from input data
text_parts = [f"Execute {agent.name} with the following parameters:"]
for key, value in input_data.items():
if value: # Only include non-empty values
label = field_labels.get(key, key.replace('_', ' ').title())
text_parts.append(f"{label}: {value}")
return ". ".join(text_parts) + "."
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_agent_executions(request):
"""Get user's agent execution history"""
try:
executions = AgentExecution.objects.filter(
user=request.user
).select_related('agent').order_by('-created_at')
# Pagination
page_size = 20
page = int(request.GET.get('page', 1))
start = (page - 1) * page_size
end = start + page_size
executions_data = []
for execution in executions[start:end]:
executions_data.append({
'id': str(execution.id),
'agent_name': execution.agent.name,
'agent_slug': execution.agent.slug,
'status': execution.status,
'price_paid': float(execution.price_paid),
'created_at': execution.created_at.isoformat(),
'completed_at': execution.completed_at.isoformat() if execution.completed_at else None,
'has_output': bool(execution.output_data),
'error_message': execution.error_message
})
return Response({
'executions': executions_data,
'page': page,
'has_next': len(executions) > end,
'total_count': executions.count()
})
except Exception as e:
return Response(
{'error': f'Failed to fetch executions: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_execution_detail(request, execution_id):
"""Get detailed information about a specific execution"""
try:
execution = get_object_or_404(
AgentExecution,
id=execution_id,
user=request.user
)
return Response({
'id': str(execution.id),
'agent': {
'name': execution.agent.name,
'slug': execution.agent.slug,
'description': execution.agent.description
},
'status': execution.status,
'price_paid': float(execution.price_paid),
'input_data': execution.input_data,
'output_data': execution.output_data,
'error_message': execution.error_message,
'created_at': execution.created_at.isoformat(),
'updated_at': execution.updated_at.isoformat(),
'completed_at': execution.completed_at.isoformat() if execution.completed_at else None
})
except Exception as e:
return Response(
{'error': f'Failed to fetch execution details: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def list_agents(request):
"""List all available agents with filtering"""
try:
agents = Agent.objects.filter(is_active=True)
# Filter by category
category_slug = request.GET.get('category')
if category_slug:
agents = agents.filter(category__slug=category_slug)
# Search
search = request.GET.get('search', '').strip()
if search:
agents = agents.filter(
name__icontains=search
) | agents.filter(
short_description__icontains=search
)
agents_data = []
for agent in agents:
agents_data.append({
'id': str(agent.id),
'name': agent.name,
'slug': agent.slug,
'short_description': agent.short_description,
'price': float(agent.price),
'category': {
'name': agent.category.name,
'slug': agent.category.slug
},
'usage_count': agent.usage_count,
'is_featured': agent.is_featured,
'icon': agent.icon
})
return Response({'agents': agents_data})
except Exception as e:
return Response(
{'error': f'Failed to fetch agents: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_agent_detail(request, slug):
"""Get detailed information about a specific agent"""
try:
agent = get_object_or_404(Agent, slug=slug, is_active=True)
return Response({
'id': str(agent.id),
'name': agent.name,
'slug': agent.slug,
'description': agent.description,
'short_description': agent.short_description,
'price': float(agent.price),
'category': {
'name': agent.category.name,
'slug': agent.category.slug
},
'form_schema': agent.form_schema,
'usage_count': agent.usage_count,
'is_featured': agent.is_featured,
'icon': agent.icon,
'created_at': agent.created_at.isoformat()
})
except Exception as e:
return Response(
{'error': f'Failed to fetch agent details: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)

0
frontend/__init__.py Normal file
View File

3
frontend/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
frontend/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class FrontendConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'frontend'

View File

3
frontend/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
frontend/tests.py Normal file
View File

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

14
frontend/urls.py Normal file
View File

@ -0,0 +1,14 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.landing, name='landing'),
path('login/', views.login_view, name='login'),
path('register/', views.register_view, name='register'),
path('dashboard/', views.dashboard, name='dashboard'),
path('wallet/', views.wallet, name='wallet'),
path('workflows/', views.workflows, name='workflows'),
path('agents/', views.agents_marketplace, name='agents'),
path('agents/<slug:slug>/', views.agent_detail, name='agent_detail'),
path('logout/', views.logout_view, name='logout'),
]

90
frontend/views.py Normal file
View File

@ -0,0 +1,90 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from agents.models import Agent, AgentCategory
import json
def landing(request):
if request.user.is_authenticated:
return redirect('dashboard')
return render(request, 'pages/landing.html')
def login_view(request):
if request.user.is_authenticated:
return redirect('dashboard')
return render(request, 'pages/login.html')
def register_view(request):
if request.user.is_authenticated:
return redirect('dashboard')
return render(request, 'pages/register.html')
@login_required(login_url='/login/')
def dashboard(request):
return render(request, 'pages/dashboard.html')
@login_required(login_url='/login/')
def wallet(request):
return render(request, 'pages/wallet.html')
@login_required(login_url='/login/')
def workflows(request):
return render(request, 'pages/workflows.html')
@login_required(login_url='/login/')
def agents_marketplace(request):
"""Dynamic agent marketplace showing all available agents"""
category_slug = request.GET.get('category')
search_query = request.GET.get('search', '').strip()
# Get all active agents
agents = Agent.objects.filter(is_active=True)
# Filter by category if specified
if category_slug:
agents = agents.filter(category__slug=category_slug)
# Filter by search query if specified
if search_query:
agents = agents.filter(
name__icontains=search_query
) | agents.filter(
short_description__icontains=search_query
) | agents.filter(
description__icontains=search_query
)
# Get all categories for filter menu
categories = AgentCategory.objects.filter(is_active=True)
context = {
'agents': agents,
'categories': categories,
'current_category': category_slug,
'search_query': search_query,
}
return render(request, 'pages/agents.html', context)
@login_required(login_url='/login/')
def agent_detail(request, slug):
"""Dynamic agent detail page with form rendering"""
agent = get_object_or_404(Agent, slug=slug, is_active=True)
context = {
'agent': agent,
'agent_form_schema_json': json.dumps(agent.form_schema),
'user_wallet_balance': float(request.user.wallet_balance),
}
return render(request, 'pages/agent_detail.html', context)
def logout_view(request):
return redirect('/')

47
manage_users.py Normal file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env python
"""
Simple script to manage test users for development
"""
import os
import sys
import django
# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_ai_agent.settings.development')
django.setup()
from users.models import User
def list_users():
print("Current users:")
for user in User.objects.all():
print(f"- {user.email} (Balance: ${user.wallet_balance}, Created: {user.created_at.strftime('%Y-%m-%d')})")
def delete_user(email):
try:
user = User.objects.get(email=email)
user.delete()
print(f"Deleted user: {email}")
except User.DoesNotExist:
print(f"User not found: {email}")
def add_balance(email, amount):
try:
user = User.objects.get(email=email)
user.add_balance(amount)
print(f"Added ${amount} to {email}. New balance: ${user.wallet_balance}")
except User.DoesNotExist:
print(f"User not found: {email}")
if __name__ == '__main__':
if len(sys.argv) == 1:
list_users()
elif sys.argv[1] == 'delete' and len(sys.argv) == 3:
delete_user(sys.argv[2])
elif sys.argv[1] == 'balance' and len(sys.argv) == 4:
add_balance(sys.argv[2], float(sys.argv[3]))
else:
print("Usage:")
print(" python manage_users.py # List all users")
print(" python manage_users.py delete <email> # Delete user")
print(" python manage_users.py balance <email> <amount> # Add balance")

View File

@ -13,10 +13,13 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
'users',
'wallet',
'workflows',
'agents',
'frontend',
]
MIDDLEWARE = [
@ -36,7 +39,7 @@ ROOT_URLCONF = 'netcop_ai_agent.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -73,6 +76,9 @@ USE_TZ = True
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@ -16,12 +16,12 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import path, include
from . import views
urlpatterns = [
path('', views.api_root, name='api_root'),
path('', include('frontend.urls')),
path('admin/', admin.site.urls),
path('api/auth/', include('users.urls')),
path('api/wallet/', include('wallet.urls')),
path('api/workflows/', include('workflows.urls')),
path('api/agents/', include('agents.urls')),
]

382
static/css/main.css Normal file
View File

@ -0,0 +1,382 @@
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--black: #000000;
--white: #ffffff;
--gray-50: #fafafa;
--gray-100: #f5f5f5;
--gray-200: #e5e5e5;
--gray-300: #d4d4d4;
--gray-400: #a3a3a3;
--gray-500: #737373;
--gray-600: #525252;
--gray-700: #404040;
--gray-800: #262626;
--gray-900: #171717;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: var(--gray-900);
background-color: var(--white);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.2;
margin-bottom: 1rem;
}
h1 { font-size: 2.25rem; }
h2 { font-size: 1.875rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
p { margin-bottom: 1rem; }
/* Navigation */
.navbar {
background: var(--white);
border-bottom: 1px solid var(--gray-200);
padding: 0 1rem;
position: sticky;
top: 0;
z-index: 100;
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
height: 4rem;
}
.nav-logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--black);
text-decoration: none;
}
.nav-logo:hover {
color: var(--gray-700);
}
.nav-links {
display: flex;
align-items: center;
gap: 2rem;
}
.nav-link {
color: var(--gray-600);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.nav-link:hover {
color: var(--black);
}
.nav-button {
background: var(--black);
color: var(--white);
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: background-color 0.2s;
}
.nav-button:hover {
background: var(--gray-800);
}
/* Main Content */
.main-content {
flex: 1;
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
width: 100%;
}
/* Messages */
.messages {
margin-bottom: 2rem;
}
.alert {
padding: 0.75rem 1rem;
border-radius: 4px;
margin-bottom: 1rem;
border-left: 4px solid;
}
.alert-success {
background: var(--gray-50);
border-left-color: var(--gray-400);
color: var(--gray-800);
}
.alert-error {
background: var(--gray-100);
border-left-color: var(--black);
color: var(--black);
}
/* Cards */
.card {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: 8px;
padding: 1.5rem;
box-shadow: var(--shadow-sm);
margin-bottom: 1.5rem;
}
.card-header {
border-bottom: 1px solid var(--gray-200);
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0;
}
/* Forms */
.form {
max-width: 400px;
margin: 0 auto;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--gray-700);
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--gray-300);
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--black);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 120px;
}
/* Buttons */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-weight: 500;
text-align: center;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
font-size: 1rem;
}
.btn-primary {
background: var(--black);
color: var(--white);
}
.btn-primary:hover {
background: var(--gray-800);
}
.btn-secondary {
background: var(--white);
color: var(--black);
border: 1px solid var(--gray-300);
}
.btn-secondary:hover {
background: var(--gray-50);
}
.btn-full {
width: 100%;
}
/* Tables */
.table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--gray-200);
}
.table th {
font-weight: 600;
background: var(--gray-50);
}
/* Stats */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: 8px;
padding: 1.5rem;
text-align: center;
box-shadow: var(--shadow-sm);
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--black);
margin-bottom: 0.5rem;
}
.stat-label {
color: var(--gray-600);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Loading */
.loading {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid var(--gray-300);
border-top: 2px solid var(--black);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Footer */
.footer {
background: var(--gray-50);
border-top: 1px solid var(--gray-200);
padding: 2rem 1rem;
margin-top: auto;
}
.footer-container {
max-width: 1200px;
margin: 0 auto;
text-align: center;
color: var(--gray-600);
font-size: 0.875rem;
}
/* Hero Section */
.hero {
text-align: center;
padding: 4rem 0;
background: var(--gray-50);
margin: -2rem -1rem 2rem -1rem;
}
.hero-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--black);
}
.hero-subtitle {
font-size: 1.25rem;
color: var(--gray-600);
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.hero-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
/* Responsive */
@media (max-width: 768px) {
.nav-links {
gap: 1rem;
}
.hero-title {
font-size: 2rem;
}
.hero-subtitle {
font-size: 1rem;
}
.hero-buttons {
flex-direction: column;
align-items: center;
}
.btn {
min-width: 200px;
}
.stats-grid {
grid-template-columns: 1fr;
}
}

283
static/js/main.js Normal file
View File

@ -0,0 +1,283 @@
// API Configuration
const API_BASE = '/api';
const TOKEN_KEY = 'auth_token';
// API Client
class APIClient {
constructor() {
this.token = localStorage.getItem(TOKEN_KEY);
}
async request(endpoint, options = {}) {
const url = `${API_BASE}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
if (this.token) {
config.headers['Authorization'] = `Token ${this.token}`;
}
// Add CSRF token if available
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
if (csrfToken) {
config.headers['X-CSRFToken'] = csrfToken;
}
try {
const response = await fetch(url, config);
let data;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = { error: await response.text() };
}
if (!response.ok) {
// Handle validation errors
if (data && typeof data === 'object') {
const errorMessages = [];
for (const [field, errors] of Object.entries(data)) {
if (Array.isArray(errors)) {
errorMessages.push(`${field}: ${errors.join(', ')}`);
} else {
errorMessages.push(`${field}: ${errors}`);
}
}
if (errorMessages.length > 0) {
throw new Error(errorMessages.join('\n'));
}
}
throw new Error(data.error || data.detail || data.message || 'Request failed');
}
return data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
async get(endpoint) {
return this.request(endpoint);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
setToken(token) {
this.token = token;
localStorage.setItem(TOKEN_KEY, token);
}
clearToken() {
this.token = null;
localStorage.removeItem(TOKEN_KEY);
}
}
// Global API client
const api = new APIClient();
// Authentication
async function login(email, password) {
try {
const data = await api.post('/auth/login/', { email, password });
api.setToken(data.token);
return data;
} catch (error) {
throw error;
}
}
async function register(email, password, passwordConfirm) {
try {
const data = await api.post('/auth/register/', {
email,
password,
password_confirm: passwordConfirm,
});
api.setToken(data.token);
return data;
} catch (error) {
throw error;
}
}
function logout() {
api.clearToken();
window.location.href = '/';
}
// UI Helpers - Minimal Toast System
function showMessage(message, type = 'success') {
// Only show critical messages as toasts
if (type === 'error' || message.toLowerCase().includes('insufficient') || message.toLowerCase().includes('unavailable') || message.toLowerCase().includes('failed') || message.toLowerCase().includes('unable to connect')) {
showToast(message, type);
}
// Success messages are handled by visual feedback (balance animation, etc.)
}
function showToast(message, type = 'error') {
// Create toast container if it doesn't exist
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
pointer-events: none;
`;
document.body.appendChild(toastContainer);
}
// Create toast
const toast = document.createElement('div');
const isError = type === 'error';
toast.style.cssText = `
background: ${isError ? '#ef4444' : '#10b981'};
color: white;
padding: 14px 18px;
border-radius: 10px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateX(400px);
transition: transform 0.3s ease;
pointer-events: auto;
max-width: 380px;
word-wrap: break-word;
white-space: normal;
hyphens: auto;
`;
toast.textContent = message;
toastContainer.appendChild(toast);
// Slide in
setTimeout(() => {
toast.style.transform = 'translateX(0)';
}, 100);
// Auto-remove after 4 seconds
setTimeout(() => {
toast.style.transform = 'translateX(400px)';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 4000);
}
function showLoading(element) {
const original = element.innerHTML;
element.innerHTML = '<span class="loading"></span> Loading...';
element.disabled = true;
return () => {
element.innerHTML = original;
element.disabled = false;
};
}
// Form handling
function handleFormSubmit(formSelector, submitHandler) {
const form = document.querySelector(formSelector);
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = form.querySelector('button[type="submit"]');
const hideLoading = showLoading(submitBtn);
try {
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// Remove CSRF token from API data
delete data.csrfmiddlewaretoken;
await submitHandler(data);
} catch (error) {
showMessage(error.message, 'error');
} finally {
hideLoading();
}
});
}
// Format currency
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
}
// Format date
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
// Check authentication
function isAuthenticated() {
return !!localStorage.getItem(TOKEN_KEY);
}
function requireAuth() {
if (!isAuthenticated()) {
window.location.href = '/login/';
return false;
}
return true;
}
// Agent execution functions
async function executeAgent(agentSlug, inputData) {
try {
const response = await api.post('/agents/execute/', {
agent_slug: agentSlug,
input_data: inputData
});
return response;
} catch (error) {
throw error;
}
}
async function getAgentExecutions(page = 1) {
try {
const response = await api.get(`/agents/executions/?page=${page}`);
return response;
} catch (error) {
throw error;
}
}
async function getExecutionDetail(executionId) {
try {
const response = await api.get(`/agents/executions/${executionId}/`);
return response;
} catch (error) {
throw error;
}
}

53
templates/base.html Normal file
View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}NetCop AI Agent{% endblock %}</title>
{% load static %}
<link rel="stylesheet" href="{% static 'css/main.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar">
<div class="nav-container">
<a href="/" class="nav-logo">NetCop AI</a>
<div class="nav-links">
{% if user.is_authenticated %}
<a href="/dashboard/" class="nav-link">Dashboard</a>
<a href="/agents/" class="nav-link">Agents</a>
<a href="/wallet/" class="nav-link">Wallet</a>
<a href="/workflows/" class="nav-link">Workflows</a>
<button onclick="logout()" class="nav-button">Logout</button>
{% else %}
<a href="/login/" class="nav-link">Login</a>
<a href="/register/" class="nav-button">Register</a>
{% endif %}
</div>
</div>
</nav>
<main class="main-content">
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% block content %}{% endblock %}
</main>
<footer class="footer">
<div class="footer-container">
<p>&copy; 2025 NetCop AI Agent. Built with Django & minimal design.</p>
</div>
</footer>
<script src="{% static 'js/main.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

File diff suppressed because it is too large Load Diff

311
templates/pages/agents.html Normal file
View File

@ -0,0 +1,311 @@
{% extends 'base.html' %}
{% block title %}AI Agents Marketplace{% endblock %}
{% block content %}
<div class="page-header">
<h1>AI Agents Marketplace</h1>
<p>Discover and use powerful AI agents to automate your tasks</p>
</div>
<!-- Search and Filter Section -->
<div class="search-filter-section">
<form method="GET" class="search-form">
<div class="search-group">
<input type="text" name="search" placeholder="Search agents..."
value="{{ search_query }}" class="search-input">
<button type="submit" class="search-btn">Search</button>
</div>
</form>
<div class="category-filters">
<a href="{% url 'agents' %}"
class="filter-btn {% if not current_category %}active{% endif %}">
All Categories
</a>
{% for category in categories %}
<a href="{% url 'agents' %}?category={{ category.slug }}"
class="filter-btn {% if current_category == category.slug %}active{% endif %}">
{% if category.icon %}{{ category.icon }}{% endif %}
{{ category.name }}
</a>
{% endfor %}
</div>
</div>
<!-- Results Count -->
<div class="results-info">
<p>{{ agents.count }} agent{{ agents.count|pluralize }} found</p>
</div>
<!-- Agents Grid -->
<div class="agents-grid">
{% for agent in agents %}
<div class="agent-card">
<div class="agent-header">
{% if agent.icon %}
<div class="agent-icon">{{ agent.icon }}</div>
{% endif %}
<h3 class="agent-name">{{ agent.name }}</h3>
{% if agent.is_featured %}
<span class="featured-badge">Featured</span>
{% endif %}
</div>
<div class="agent-content">
<p class="agent-description">{{ agent.short_description }}</p>
<div class="agent-meta">
<span class="agent-category">{{ agent.category.name }}</span>
<span class="agent-usage">{{ agent.usage_count }} uses</span>
</div>
</div>
<div class="agent-footer">
<div class="agent-price">{{ agent.price }} AED</div>
<a href="{% url 'agent_detail' agent.slug %}" class="btn btn-primary">
Use Agent
</a>
</div>
</div>
{% empty %}
<div class="empty-state">
<h3>No agents found</h3>
<p>Try adjusting your search criteria or browse all categories.</p>
<a href="{% url 'agents' %}" class="btn btn-secondary">View All Agents</a>
</div>
{% endfor %}
</div>
<!-- Load agents-specific styles and scripts -->
<style>
.page-header {
text-align: center;
margin-bottom: 2rem;
}
.page-header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.page-header p {
color: var(--text-muted);
font-size: 1.1rem;
}
.search-filter-section {
background: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
border: 1px solid var(--border-color);
}
.search-form {
margin-bottom: 1.5rem;
}
.search-group {
display: flex;
gap: 0.5rem;
max-width: 500px;
margin: 0 auto;
}
.search-input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid var(--border-color);
border-radius: 6px;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: var(--primary-color);
}
.search-btn {
padding: 0.75rem 1.5rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}
.search-btn:hover {
background: var(--primary-hover);
}
.category-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 2px solid var(--border-color);
border-radius: 20px;
text-decoration: none;
color: var(--text-color);
background: white;
transition: all 0.2s;
font-size: 0.9rem;
}
.filter-btn:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
.filter-btn.active {
background: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.results-info {
margin-bottom: 1.5rem;
text-align: center;
color: var(--text-muted);
}
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.agent-card {
background: white;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.agent-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.agent-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
position: relative;
}
.agent-icon {
font-size: 2rem;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: var(--light-gray);
border-radius: 8px;
}
.agent-name {
flex: 1;
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.featured-badge {
position: absolute;
top: -8px;
right: -8px;
background: var(--accent-color);
color: white;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: 500;
}
.agent-content {
margin-bottom: 1.5rem;
}
.agent-description {
color: var(--text-muted);
line-height: 1.5;
margin-bottom: 1rem;
}
.agent-meta {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: var(--text-muted);
}
.agent-category {
background: var(--light-gray);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.agent-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.agent-price {
font-size: 1.25rem;
font-weight: 600;
color: var(--primary-color);
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 3rem;
background: white;
border: 1px solid var(--border-color);
border-radius: 8px;
}
.empty-state h3 {
margin-bottom: 0.5rem;
color: var(--text-muted);
}
.empty-state p {
color: var(--text-muted);
margin-bottom: 1.5rem;
}
@media (max-width: 768px) {
.agents-grid {
grid-template-columns: 1fr;
}
.search-group {
flex-direction: column;
}
.category-filters {
justify-content: flex-start;
}
.agent-footer {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
}
</style>
{% endblock %}

View File

@ -0,0 +1,167 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Dashboard - NetCop AI Agent{% endblock %}
{% block content %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<h1>Dashboard</h1>
<div style="display: flex; gap: 1rem;">
<a href="/wallet/" class="btn btn-secondary">Manage Wallet</a>
<a href="/workflows/" class="btn btn-primary">Trigger Workflow</a>
</div>
</div>
<!-- User Profile & Stats -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="walletBalance">$0.00</div>
<div class="stat-label">Wallet Balance</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalWorkflows">0</div>
<div class="stat-label">Workflows Executed</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalSpent">$0.00</div>
<div class="stat-label">Total Spent</div>
</div>
<div class="stat-card">
<div class="stat-value" id="userEmail">Loading...</div>
<div class="stat-label">Account Email</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Quick Actions</h3>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
<a href="/wallet/" class="btn btn-secondary" style="text-align: center; padding: 1.5rem;">
💳<br>Top Up Wallet
</a>
<a href="/workflows/" class="btn btn-secondary" style="text-align: center; padding: 1.5rem;">
<br>Trigger Workflow
</a>
<a href="/wallet/" class="btn btn-secondary" style="text-align: center; padding: 1.5rem;">
📊<br>View Transactions
</a>
<a href="/workflows/" class="btn btn-secondary" style="text-align: center; padding: 1.5rem;">
📝<br>Workflow History
</a>
</div>
</div>
<!-- Recent Activity -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 2rem;">
<div class="card">
<div class="card-header">
<h3 class="card-title">Recent Transactions</h3>
</div>
<div id="recentTransactions">
<p style="text-align: center; color: var(--gray-600); padding: 2rem;">Loading transactions...</p>
</div>
<div style="text-align: center; margin-top: 1rem;">
<a href="/wallet/" class="btn btn-secondary">View All</a>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Recent Workflows</h3>
</div>
<div id="recentWorkflows">
<p style="text-align: center; color: var(--gray-600); padding: 2rem;">Loading workflow history...</p>
</div>
<div style="text-align: center; margin-top: 1rem;">
<a href="/workflows/" class="btn btn-secondary">View All</a>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!requireAuth()) return;
try {
// Load user profile
const profile = await api.get('/auth/profile/');
document.getElementById('userEmail').textContent = profile.email;
document.getElementById('walletBalance').textContent = formatCurrency(profile.wallet_balance);
// Load recent transactions
const transactions = await api.get('/wallet/transactions/');
displayRecentTransactions(transactions.slice(0, 5));
// Load recent workflows
const workflows = await api.get('/workflows/history/');
displayRecentWorkflows(workflows.slice(0, 5));
// Calculate stats
const totalWorkflows = workflows.length;
const totalSpent = workflows.reduce((sum, w) => sum + parseFloat(w.fee_charged), 0);
document.getElementById('totalWorkflows').textContent = totalWorkflows;
document.getElementById('totalSpent').textContent = formatCurrency(totalSpent);
} catch (error) {
showMessage('Failed to load dashboard data: ' + error.message, 'error');
}
});
function displayRecentTransactions(transactions) {
const container = document.getElementById('recentTransactions');
if (transactions.length === 0) {
container.innerHTML = '<p style="text-align: center; color: var(--gray-600); padding: 2rem;">No transactions yet</p>';
return;
}
container.innerHTML = transactions.map(transaction => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; border-bottom: 1px solid var(--gray-200);">
<div>
<div style="font-weight: 500; text-transform: capitalize;">${transaction.transaction_type}</div>
<div style="font-size: 0.875rem; color: var(--gray-600);">${formatDate(transaction.created_at)}</div>
</div>
<div style="font-weight: 600; color: ${transaction.transaction_type === 'deposit' ? 'var(--gray-700)' : 'var(--gray-500)'};">
${transaction.transaction_type === 'deposit' ? '+' : '-'}${formatCurrency(transaction.amount)}
</div>
</div>
`).join('');
}
function displayRecentWorkflows(workflows) {
const container = document.getElementById('recentWorkflows');
if (workflows.length === 0) {
container.innerHTML = '<p style="text-align: center; color: var(--gray-600); padding: 2rem;">No workflows executed yet</p>';
return;
}
container.innerHTML = workflows.map(workflow => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; border-bottom: 1px solid var(--gray-200);">
<div>
<div style="font-weight: 500;">${workflow.workflow_name}</div>
<div style="font-size: 0.875rem; color: var(--gray-600);">${formatDate(workflow.created_at)}</div>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 12px; background: ${getStatusColor(workflow.status)};">
${workflow.status}
</span>
<span style="font-weight: 600;">${formatCurrency(workflow.fee_charged)}</span>
</div>
</div>
`).join('');
}
function getStatusColor(status) {
switch (status) {
case 'success': return 'var(--gray-100)';
case 'failed': return 'var(--gray-200)';
case 'pending': return 'var(--gray-50)';
default: return 'var(--gray-100)';
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}NetCop AI Agent - Secure Workflow Automation{% endblock %}
{% block content %}
<section class="hero">
<h1 class="hero-title">NetCop AI Agent</h1>
<p class="hero-subtitle">
Secure, pay-per-use workflow automation platform.
Top up your wallet, trigger powerful n8n workflows, and monitor your usage—all with a sleek, minimal interface.
</p>
<div class="hero-buttons">
<a href="/register/" class="btn btn-primary">Get Started</a>
<a href="/login/" class="btn btn-secondary">Sign In</a>
</div>
</section>
<section class="features">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">$0.10</div>
<div class="stat-label">Per Workflow</div>
</div>
<div class="stat-card">
<div class="stat-value">100%</div>
<div class="stat-label">Secure</div>
</div>
<div class="stat-card">
<div class="stat-value">24/7</div>
<div class="stat-label">Available</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">How it Works</h3>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem;">
<div>
<h4>1. Create Account</h4>
<p>Sign up with your email and create a secure account in seconds.</p>
</div>
<div>
<h4>2. Top Up Wallet</h4>
<p>Add funds to your wallet using our secure Stripe integration.</p>
</div>
<div>
<h4>3. Trigger Workflows</h4>
<p>Execute powerful n8n workflows with custom data. Pay only $0.10 per execution.</p>
</div>
<div>
<h4>4. Monitor Usage</h4>
<p>Track all your transactions and workflow executions in real-time.</p>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Features</h3>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem;">
<div>
<h4>🔐 Secure Authentication</h4>
<p>Token-based authentication with email verification and secure password handling.</p>
</div>
<div>
<h4>💳 Stripe Integration</h4>
<p>Secure payment processing with Stripe Checkout for wallet top-ups.</p>
</div>
<div>
<h4>⚡ n8n Workflows</h4>
<p>Trigger powerful automation workflows with custom JSON data.</p>
</div>
<div>
<h4>📊 Usage Tracking</h4>
<p>Comprehensive logging of all transactions and workflow executions.</p>
</div>
<div>
<h4>🎛️ Admin Dashboard</h4>
<p>Full administrative interface for monitoring users and system health.</p>
</div>
<div>
<h4>📱 Responsive Design</h4>
<p>Clean, minimal interface that works perfectly on all devices.</p>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Login - NetCop AI Agent{% endblock %}
{% block content %}
<div class="card" style="max-width: 400px; margin: 2rem auto;">
<div class="card-header">
<h2 class="card-title">Sign In</h2>
</div>
<form id="loginForm" class="form">
{% csrf_token %}
<div class="form-group">
<label for="email" class="form-label">Email</label>
<input
type="email"
id="email"
name="email"
class="form-input"
required
placeholder="your@email.com"
>
</div>
<div class="form-group">
<label for="password" class="form-label">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
required
placeholder="Enter your password"
>
</div>
<button type="submit" class="btn btn-primary btn-full">
Sign In
</button>
</form>
<div style="text-align: center; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--gray-200);">
<p style="color: var(--gray-600); margin-bottom: 0.5rem;">Don't have an account?</p>
<a href="/register/" class="btn btn-secondary">Create Account</a>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
handleFormSubmit('#loginForm', async (data) => {
const result = await login(data.email, data.password);
showMessage('Login successful! Redirecting...', 'success');
setTimeout(() => {
window.location.href = '/dashboard/';
}, 1000);
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,78 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Register - NetCop AI Agent{% endblock %}
{% block content %}
<div class="card" style="max-width: 400px; margin: 2rem auto;">
<div class="card-header">
<h2 class="card-title">Create Account</h2>
<p style="color: var(--gray-600); margin: 0;">Join NetCop AI Agent and start automating your workflows.</p>
</div>
<form id="registerForm" class="form">
{% csrf_token %}
<div class="form-group">
<label for="email" class="form-label">Email</label>
<input
type="email"
id="email"
name="email"
class="form-input"
required
placeholder="your@email.com"
>
</div>
<div class="form-group">
<label for="password" class="form-label">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
required
minlength="8"
placeholder="At least 8 characters"
>
</div>
<div class="form-group">
<label for="password_confirm" class="form-label">Confirm Password</label>
<input
type="password"
id="password_confirm"
name="password_confirm"
class="form-input"
required
placeholder="Re-enter your password"
>
</div>
<button type="submit" class="btn btn-primary btn-full">
Create Account
</button>
</form>
<div style="text-align: center; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--gray-200);">
<p style="color: var(--gray-600); margin-bottom: 0.5rem;">Already have an account?</p>
<a href="/login/" class="btn btn-secondary">Sign In</a>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
handleFormSubmit('#registerForm', async (data) => {
if (data.password !== data.password_confirm) {
throw new Error('Passwords do not match');
}
const result = await register(data.email, data.password, data.password_confirm);
showMessage('Account created successfully! Redirecting...', 'success');
setTimeout(() => {
window.location.href = '/dashboard/';
}, 1000);
});
});
</script>
{% endblock %}

194
templates/pages/wallet.html Normal file
View File

@ -0,0 +1,194 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Wallet - NetCop AI Agent{% endblock %}
{% block content %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<h1>Wallet</h1>
<a href="/dashboard/" class="btn btn-secondary">← Back to Dashboard</a>
</div>
<!-- Wallet Balance -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Current Balance</h3>
</div>
<div style="text-align: center; padding: 2rem 0;">
<div style="font-size: 3rem; font-weight: 700; color: var(--black); margin-bottom: 1rem;" id="walletBalance">
0.00 AED
</div>
<button onclick="showTopUpForm()" class="btn btn-primary">
Top Up Wallet
</button>
</div>
</div>
<!-- Top Up Form (Hidden by default) -->
<div id="topUpForm" class="card" style="display: none;">
<div class="card-header">
<h3 class="card-title">Top Up Wallet</h3>
<p style="color: var(--gray-600); margin: 0;">Add funds to your wallet using Stripe Checkout</p>
</div>
<form id="topUpFormElement">
<div class="form-group">
<label for="amount" class="form-label">Amount (AED)</label>
<input
type="number"
id="amount"
name="amount"
class="form-input"
required
min="1"
step="0.01"
placeholder="10.00"
>
</div>
<div style="display: flex; gap: 1rem;">
<button type="submit" class="btn btn-primary">
Proceed to Checkout
</button>
<button type="button" onclick="hideTopUpForm()" class="btn btn-secondary">
Cancel
</button>
</div>
</form>
</div>
<!-- Transaction History -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Transaction History</h3>
</div>
<div id="transactionHistory">
<p style="text-align: center; color: var(--gray-600); padding: 2rem;">Loading transactions...</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!requireAuth()) return;
await loadWalletData();
// Set up top-up form
handleFormSubmit('#topUpFormElement', async (data) => {
const response = await api.post('/wallet/top-up/', { amount: parseFloat(data.amount) });
if (response.checkout_url) {
showMessage('Redirecting to Stripe Checkout...', 'success');
window.location.href = response.checkout_url;
} else {
throw new Error('Failed to create checkout session');
}
});
});
async function loadWalletData() {
try {
// Load user profile for balance
const profile = await api.get('/auth/profile/');
document.getElementById('walletBalance').textContent = profile.wallet_balance.toFixed(2) + ' AED';
// Load transaction history
const transactions = await api.get('/wallet/transactions/');
displayTransactionHistory(transactions);
} catch (error) {
showMessage('Failed to load wallet data: ' + error.message, 'error');
}
}
function displayTransactionHistory(transactions) {
const container = document.getElementById('transactionHistory');
if (transactions.length === 0) {
container.innerHTML = '<p style="text-align: center; color: var(--gray-600); padding: 2rem;">No transactions yet</p>';
return;
}
const tableHTML = `
<table class="table">
<thead>
<tr>
<th>Type</th>
<th>Amount</th>
<th>Status</th>
<th>Description</th>
<th>Date</th>
</tr>
</thead>
<tbody>
${transactions.map(transaction => `
<tr>
<td style="text-transform: capitalize; font-weight: 500;">
${getTransactionIcon(transaction.transaction_type)} ${transaction.transaction_type}
</td>
<td style="font-weight: 600; color: ${getAmountColor(transaction.transaction_type)};">
${transaction.transaction_type === 'deposit' ? '+' : '-'}${parseFloat(transaction.amount).toFixed(2)} AED
</td>
<td>
<span style="font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 12px; background: ${getStatusBg(transaction.status)}; text-transform: capitalize;">
${transaction.status}
</span>
</td>
<td style="color: var(--gray-600);">
${transaction.description || '-'}
</td>
<td style="color: var(--gray-600); font-size: 0.875rem;">
${formatDate(transaction.created_at)}
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = tableHTML;
}
function getTransactionIcon(type) {
switch (type) {
case 'deposit': return '💳';
case 'withdrawal': return '💸';
case 'fee': return '⚡';
default: return '📝';
}
}
function getAmountColor(type) {
return type === 'deposit' ? 'var(--gray-700)' : 'var(--gray-500)';
}
function getStatusBg(status) {
switch (status) {
case 'completed': return 'var(--gray-100)';
case 'failed': return 'var(--gray-200)';
case 'pending': return 'var(--gray-50)';
default: return 'var(--gray-100)';
}
}
function showTopUpForm() {
document.getElementById('topUpForm').style.display = 'block';
document.getElementById('amount').focus();
}
function hideTopUpForm() {
document.getElementById('topUpForm').style.display = 'none';
document.getElementById('topUpFormElement').reset();
}
// Handle returning from Stripe (success/cancel)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('success') === 'true') {
// Visual feedback: wallet balance will show updated amount
loadWalletData(); // Refresh data
} else if (urlParams.get('canceled') === 'true') {
showMessage('Payment was canceled', 'error');
}
</script>
{% endblock %}

View File

@ -0,0 +1,280 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Workflows - NetCop AI Agent{% endblock %}
{% block content %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<h1>Workflows</h1>
<a href="/dashboard/" class="btn btn-secondary">← Back to Dashboard</a>
</div>
<!-- Workflow Trigger Form -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Trigger Workflow</h3>
<p style="color: var(--gray-600); margin: 0;">
Execute n8n workflows with custom data. Each execution costs $0.10.
</p>
</div>
<div style="background: var(--gray-50); padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 500;">Current Balance:</span>
<span style="font-size: 1.25rem; font-weight: 600;" id="walletBalance">$0.00</span>
</div>
<div style="font-size: 0.875rem; color: var(--gray-600); margin-top: 0.5rem;">
Sufficient for <span id="executionsRemaining">0</span> workflow executions
</div>
</div>
<form id="workflowForm">
<div class="form-group">
<label for="workflow_name" class="form-label">Workflow Name</label>
<input
type="text"
id="workflow_name"
name="workflow_name"
class="form-input"
required
placeholder="my-workflow"
pattern="[a-zA-Z0-9-_]+"
title="Only letters, numbers, hyphens, and underscores allowed"
>
<div style="font-size: 0.875rem; color: var(--gray-600); margin-top: 0.25rem;">
This should match your n8n webhook endpoint name
</div>
</div>
<div class="form-group">
<label for="workflow_data" class="form-label">Workflow Data (JSON)</label>
<textarea
id="workflow_data"
name="workflow_data"
class="form-input form-textarea"
placeholder='{"key": "value", "message": "Hello World"}'
style="font-family: 'Monaco', 'Consolas', monospace; font-size: 0.875rem;"
></textarea>
<div style="font-size: 0.875rem; color: var(--gray-600); margin-top: 0.25rem;">
Optional: Custom data to pass to your workflow (must be valid JSON)
</div>
</div>
<div style="display: flex; gap: 1rem; align-items: center;">
<button type="submit" class="btn btn-primary">
Execute Workflow ($0.10)
</button>
<button type="button" onclick="validateJSON()" class="btn btn-secondary">
Validate JSON
</button>
</div>
</form>
</div>
<!-- Workflow History -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Execution History</h3>
</div>
<div id="workflowHistory">
<p style="text-align: center; color: var(--gray-600); padding: 2rem;">Loading workflow history...</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!requireAuth()) return;
await loadWorkflowData();
// Set up workflow form
handleFormSubmit('#workflowForm', async (data) => {
// Validate JSON if provided
let workflowData = {};
if (data.workflow_data.trim()) {
try {
workflowData = JSON.parse(data.workflow_data);
} catch (error) {
throw new Error('Invalid JSON data. Please check your syntax.');
}
}
const response = await api.post(`/workflows/trigger/${data.workflow_name}/`, {
data: workflowData
});
if (response.success) {
showMessage(`Workflow "${data.workflow_name}" executed successfully!`, 'success');
} else {
showMessage(`Workflow failed: ${response.error || 'Unknown error'}`, 'error');
}
// Reset form and reload data
document.getElementById('workflowForm').reset();
await loadWorkflowData();
});
});
async function loadWorkflowData() {
try {
// Load user profile for balance
const profile = await api.get('/auth/profile/');
const balance = parseFloat(profile.wallet_balance);
const executionsRemaining = Math.floor(balance / 0.10);
document.getElementById('walletBalance').textContent = formatCurrency(balance);
document.getElementById('executionsRemaining').textContent = executionsRemaining;
// Load workflow history
const workflows = await api.get('/workflows/history/');
displayWorkflowHistory(workflows);
} catch (error) {
showMessage('Failed to load workflow data: ' + error.message, 'error');
}
}
function displayWorkflowHistory(workflows) {
const container = document.getElementById('workflowHistory');
if (workflows.length === 0) {
container.innerHTML = '<p style="text-align: center; color: var(--gray-600); padding: 2rem;">No workflows executed yet</p>';
return;
}
const tableHTML = `
<table class="table">
<thead>
<tr>
<th>Workflow</th>
<th>Status</th>
<th>Fee</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${workflows.map(workflow => `
<tr>
<td style="font-weight: 500;">${workflow.workflow_name}</td>
<td>
<span style="font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 12px; background: ${getStatusBg(workflow.status)}; text-transform: capitalize;">
${getStatusIcon(workflow.status)} ${workflow.status}
</span>
</td>
<td style="font-weight: 600;">${formatCurrency(workflow.fee_charged)}</td>
<td style="color: var(--gray-600); font-size: 0.875rem;">
${formatDate(workflow.created_at)}
</td>
<td>
<button onclick="showWorkflowDetails('${workflow.id}')" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;">
Details
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = tableHTML;
}
function getStatusIcon(status) {
switch (status) {
case 'success': return '✅';
case 'failed': return '❌';
case 'pending': return '⏳';
default: return '📝';
}
}
function getStatusBg(status) {
switch (status) {
case 'success': return 'var(--gray-100)';
case 'failed': return 'var(--gray-200)';
case 'pending': return 'var(--gray-50)';
default: return 'var(--gray-100)';
}
}
function validateJSON() {
const textarea = document.getElementById('workflow_data');
const data = textarea.value.trim();
if (!data) {
showMessage('JSON data is empty - this is valid for workflows that don\'t need input data.', 'success');
return;
}
try {
JSON.parse(data);
showMessage('JSON is valid!', 'success');
} catch (error) {
showMessage('Invalid JSON: ' + error.message, 'error');
textarea.focus();
}
}
async function showWorkflowDetails(workflowId) {
try {
const workflows = await api.get('/workflows/history/');
const workflow = workflows.find(w => w.id === workflowId);
if (!workflow) {
showMessage('Workflow not found', 'error');
return;
}
const details = `
<div style="background: var(--white); border: 1px solid var(--gray-300); border-radius: 8px; padding: 1.5rem; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; z-index: 1000; box-shadow: var(--shadow-lg);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; border-bottom: 1px solid var(--gray-200); padding-bottom: 1rem;">
<h3 style="margin: 0;">Workflow Details</h3>
<button onclick="closeModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">&times;</button>
</div>
<div style="margin-bottom: 1rem;">
<strong>Name:</strong> ${workflow.workflow_name}<br>
<strong>Status:</strong> ${workflow.status}<br>
<strong>Fee:</strong> ${formatCurrency(workflow.fee_charged)}<br>
<strong>Date:</strong> ${formatDate(workflow.created_at)}
</div>
${workflow.request_data ? `
<div style="margin-bottom: 1rem;">
<strong>Request Data:</strong>
<pre style="background: var(--gray-50); padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.875rem;">${JSON.stringify(workflow.request_data, null, 2)}</pre>
</div>
` : ''}
${workflow.response_data ? `
<div style="margin-bottom: 1rem;">
<strong>Response Data:</strong>
<pre style="background: var(--gray-50); padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.875rem;">${JSON.stringify(workflow.response_data, null, 2)}</pre>
</div>
` : ''}
${workflow.error_message ? `
<div style="margin-bottom: 1rem;">
<strong>Error:</strong>
<div style="background: var(--gray-100); padding: 1rem; border-radius: 4px; color: var(--gray-800);">${workflow.error_message}</div>
</div>
` : ''}
</div>
<div onclick="closeModal()" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 999;"></div>
`;
document.body.insertAdjacentHTML('beforeend', details);
} catch (error) {
showMessage('Failed to load workflow details: ' + error.message, 'error');
}
}
function closeModal() {
const modals = document.querySelectorAll('[style*="z-index: 1000"], [style*="z-index: 999"]');
modals.forEach(modal => modal.remove());
}
</script>
{% endblock %}

View File

View File

View File

@ -5,4 +5,5 @@ urlpatterns = [
path('register/', views.register, name='register'),
path('login/', views.login_view, name='login'),
path('profile/', views.profile, name='profile'),
path('user/', views.profile, name='user'), # Alias for profile
]

View File

@ -4,9 +4,11 @@ from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from django.contrib.auth import login
from django.views.decorators.csrf import csrf_exempt
from .serializers import UserRegistrationSerializer, UserLoginSerializer, UserSerializer
@csrf_exempt
@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
@ -21,6 +23,7 @@ def register(request):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@csrf_exempt
@api_view(['POST'])
@permission_classes([AllowAny])
def login_view(request):
@ -37,6 +40,7 @@ def login_view(request):
@api_view(['GET'])
@permission_classes([]) # Use default authentication from settings
def profile(request):
serializer = UserSerializer(request.user)
return Response(serializer.data)

View File

@ -1,7 +1,8 @@
import stripe
from django.conf import settings
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from decimal import Decimal
from .models import WalletTransaction
@ -11,6 +12,7 @@ stripe.api_key = settings.STRIPE_SECRET_KEY
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def create_checkout_session(request):
serializer = TopUpSerializer(data=request.data)
if serializer.is_valid():
@ -21,7 +23,7 @@ def create_checkout_session(request):
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': 'usd',
'currency': 'aed',
'product_data': {
'name': 'Wallet Top-up',
},
@ -58,6 +60,7 @@ def create_checkout_session(request):
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def transaction_history(request):
transactions = WalletTransaction.objects.filter(user=request.user)
serializer = WalletTransactionSerializer(transactions, many=True)