mirror of
https://github.com/thecyberlearn/netcop-ai.git
synced 2026-08-18 08:53:01 +00:00
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:
parent
060c3a9c78
commit
213486946e
0
agents/__init__.py
Normal file
0
agents/__init__.py
Normal file
112
agents/admin.py
Normal file
112
agents/admin.py
Normal 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
6
agents/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AgentsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'agents'
|
||||||
0
agents/management/__init__.py
Normal file
0
agents/management/__init__.py
Normal file
0
agents/management/commands/__init__.py
Normal file
0
agents/management/commands/__init__.py
Normal file
373
agents/management/commands/create_sample_agents.py
Normal file
373
agents/management/commands/create_sample_agents.py
Normal 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'
|
||||||
|
)
|
||||||
|
)
|
||||||
79
agents/migrations/0001_initial.py
Normal file
79
agents/migrations/0001_initial.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
agents/migrations/__init__.py
Normal file
0
agents/migrations/__init__.py
Normal file
113
agents/models.py
Normal file
113
agents/models.py
Normal 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
3
agents/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
13
agents/urls.py
Normal file
13
agents/urls.py
Normal 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
367
agents/views.py
Normal 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
0
frontend/__init__.py
Normal file
3
frontend/admin.py
Normal file
3
frontend/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
frontend/apps.py
Normal file
6
frontend/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FrontendConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'frontend'
|
||||||
0
frontend/migrations/__init__.py
Normal file
0
frontend/migrations/__init__.py
Normal file
3
frontend/models.py
Normal file
3
frontend/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
3
frontend/tests.py
Normal file
3
frontend/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
14
frontend/urls.py
Normal file
14
frontend/urls.py
Normal 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
90
frontend/views.py
Normal 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
47
manage_users.py
Normal 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")
|
||||||
@ -13,10 +13,13 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
'rest_framework.authtoken',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
'users',
|
'users',
|
||||||
'wallet',
|
'wallet',
|
||||||
'workflows',
|
'workflows',
|
||||||
|
'agents',
|
||||||
|
'frontend',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -36,7 +39,7 @@ ROOT_URLCONF = 'netcop_ai_agent.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [BASE_DIR / 'templates'],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@ -73,6 +76,9 @@ USE_TZ = True
|
|||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / 'static',
|
||||||
|
]
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
|||||||
@ -16,12 +16,12 @@ Including another URLconf
|
|||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.api_root, name='api_root'),
|
path('', include('frontend.urls')),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('api/auth/', include('users.urls')),
|
path('api/auth/', include('users.urls')),
|
||||||
path('api/wallet/', include('wallet.urls')),
|
path('api/wallet/', include('wallet.urls')),
|
||||||
path('api/workflows/', include('workflows.urls')),
|
path('api/workflows/', include('workflows.urls')),
|
||||||
|
path('api/agents/', include('agents.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
382
static/css/main.css
Normal file
382
static/css/main.css
Normal 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
283
static/js/main.js
Normal 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
53
templates/base.html
Normal 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>© 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>
|
||||||
1177
templates/pages/agent_detail.html
Normal file
1177
templates/pages/agent_detail.html
Normal file
File diff suppressed because it is too large
Load Diff
311
templates/pages/agents.html
Normal file
311
templates/pages/agents.html
Normal 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 %}
|
||||||
167
templates/pages/dashboard.html
Normal file
167
templates/pages/dashboard.html
Normal 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 %}
|
||||||
91
templates/pages/landing.html
Normal file
91
templates/pages/landing.html
Normal 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 %}
|
||||||
60
templates/pages/login.html
Normal file
60
templates/pages/login.html
Normal 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 %}
|
||||||
78
templates/pages/register.html
Normal file
78
templates/pages/register.html
Normal 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
194
templates/pages/wallet.html
Normal 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 %}
|
||||||
280
templates/pages/workflows.html
Normal file
280
templates/pages/workflows.html
Normal 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;">×</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 %}
|
||||||
0
users/management/__init__.py
Normal file
0
users/management/__init__.py
Normal file
0
users/management/commands/__init__.py
Normal file
0
users/management/commands/__init__.py
Normal file
@ -5,4 +5,5 @@ urlpatterns = [
|
|||||||
path('register/', views.register, name='register'),
|
path('register/', views.register, name='register'),
|
||||||
path('login/', views.login_view, name='login'),
|
path('login/', views.login_view, name='login'),
|
||||||
path('profile/', views.profile, name='profile'),
|
path('profile/', views.profile, name='profile'),
|
||||||
|
path('user/', views.profile, name='user'), # Alias for profile
|
||||||
]
|
]
|
||||||
@ -4,9 +4,11 @@ from rest_framework.permissions import AllowAny
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from .serializers import UserRegistrationSerializer, UserLoginSerializer, UserSerializer
|
from .serializers import UserRegistrationSerializer, UserLoginSerializer, UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([AllowAny])
|
@permission_classes([AllowAny])
|
||||||
def register(request):
|
def register(request):
|
||||||
@ -21,6 +23,7 @@ def register(request):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([AllowAny])
|
@permission_classes([AllowAny])
|
||||||
def login_view(request):
|
def login_view(request):
|
||||||
@ -37,6 +40,7 @@ def login_view(request):
|
|||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
|
@permission_classes([]) # Use default authentication from settings
|
||||||
def profile(request):
|
def profile(request):
|
||||||
serializer = UserSerializer(request.user)
|
serializer = UserSerializer(request.user)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import stripe
|
import stripe
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework import status
|
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 rest_framework.response import Response
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from .models import WalletTransaction
|
from .models import WalletTransaction
|
||||||
@ -11,6 +12,7 @@ stripe.api_key = settings.STRIPE_SECRET_KEY
|
|||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
def create_checkout_session(request):
|
def create_checkout_session(request):
|
||||||
serializer = TopUpSerializer(data=request.data)
|
serializer = TopUpSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
@ -21,7 +23,7 @@ def create_checkout_session(request):
|
|||||||
payment_method_types=['card'],
|
payment_method_types=['card'],
|
||||||
line_items=[{
|
line_items=[{
|
||||||
'price_data': {
|
'price_data': {
|
||||||
'currency': 'usd',
|
'currency': 'aed',
|
||||||
'product_data': {
|
'product_data': {
|
||||||
'name': 'Wallet Top-up',
|
'name': 'Wallet Top-up',
|
||||||
},
|
},
|
||||||
@ -58,6 +60,7 @@ def create_checkout_session(request):
|
|||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
def transaction_history(request):
|
def transaction_history(request):
|
||||||
transactions = WalletTransaction.objects.filter(user=request.user)
|
transactions = WalletTransaction.objects.filter(user=request.user)
|
||||||
serializer = WalletTransactionSerializer(transactions, many=True)
|
serializer = WalletTransactionSerializer(transactions, many=True)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user