mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 08:52:58 +00:00
first working
This commit is contained in:
parent
3fc5f01309
commit
67ba2de335
1535
Complete Next.js-Supabase Development Guide 2025
Normal file
1535
Complete Next.js-Supabase Development Guide 2025
Normal file
File diff suppressed because it is too large
Load Diff
119
Simplified Hardcoded Agent Pages Plan
Normal file
119
Simplified Hardcoded Agent Pages Plan
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
│ Simplified Hardcoded Agent Pages Plan │
|
||||||
|
│ │
|
||||||
|
│ Overview │
|
||||||
|
│ │
|
||||||
|
│ Create individual hardcoded pages for each agent with specific integrations to n8n workflows. Each agent │
|
||||||
|
│ gets its own custom page with tailored UI and webhook connections. │
|
||||||
|
│ │
|
||||||
|
│ Implementation Approach │
|
||||||
|
│ │
|
||||||
|
│ 1. Route Structure │
|
||||||
|
│ │
|
||||||
|
│ - /agent/pdf-processor - PDF processing agent page │
|
||||||
|
│ - /agent/weather-report - Weather report agent page │
|
||||||
|
│ - /agent/content-writer - Content writing agent page │
|
||||||
|
│ - /agent/data-analyzer - Data analysis agent page │
|
||||||
|
│ - /agent/email-automation - Email automation agent page │
|
||||||
|
│ - /agent/task-automation - Task automation agent page │
|
||||||
|
│ │
|
||||||
|
│ 2. Update Current Flow │
|
||||||
|
│ │
|
||||||
|
│ Modify useAgent function in page.tsx: │
|
||||||
|
│ - Instead of showing modal with mock results │
|
||||||
|
│ - Redirect to specific agent page: router.push(/agent/${agent.slug}) │
|
||||||
|
│ - Pass agent info and user data via URL params or state │
|
||||||
|
│ │
|
||||||
|
│ 3. Example Implementation: PDF Processor Agent │
|
||||||
|
│ │
|
||||||
|
│ Page: /app/agent/pdf-processor/page.tsx │
|
||||||
|
│ - File upload component for PDF │
|
||||||
|
│ - Form fields for processing options │
|
||||||
|
│ - Connect to specific n8n webhook │
|
||||||
|
│ - Display results from Google Sheets API │
|
||||||
|
│ - Deduct credits on successful processing │
|
||||||
|
│ │
|
||||||
|
│ Workflow: │
|
||||||
|
│ 1. User uploads PDF file │
|
||||||
|
│ 2. Send file + options to n8n webhook │
|
||||||
|
│ 3. n8n processes PDF and saves results to Google Sheet │
|
||||||
|
│ 4. Page fetches results from Google Sheets │
|
||||||
|
│ 5. Display formatted results to user │
|
||||||
|
│ 6. Deduct credits from user account │
|
||||||
|
│ │
|
||||||
|
│ 4. Agent Page Components │
|
||||||
|
│ │
|
||||||
|
│ Each agent page will have: │
|
||||||
|
│ - Custom input form (file upload, text fields, dropdowns) │
|
||||||
|
│ - Processing status indicator │
|
||||||
|
│ - Results display section │
|
||||||
|
│ - Credit cost display │
|
||||||
|
│ - Back to agents list button │
|
||||||
|
│ │
|
||||||
|
│ 5. N8N Integration Pattern │
|
||||||
|
│ │
|
||||||
|
│ For each agent: │
|
||||||
|
│ - Unique n8n webhook URL │
|
||||||
|
│ - Hardcoded request format │
|
||||||
|
│ - Specific response handling │
|
||||||
|
│ - Google Sheets integration for data storage/retrieval │
|
||||||
|
│ │
|
||||||
|
│ 6. Example Agent Pages Structure │
|
||||||
|
│ │
|
||||||
|
│ PDF Processor: │
|
||||||
|
│ - Input: PDF file, processing type (extract text/data/images) │
|
||||||
|
│ - N8N: PDF processing workflow │
|
||||||
|
│ - Output: Extracted data displayed in tables/lists │
|
||||||
|
│ │
|
||||||
|
│ Weather Report: │
|
||||||
|
│ - Input: Location, date range │
|
||||||
|
│ - N8N: Weather API workflow │
|
||||||
|
│ - Output: Weather forecast with charts │
|
||||||
|
│ │
|
||||||
|
│ Content Writer: │
|
||||||
|
│ - Input: Topic, content type, length │
|
||||||
|
│ - N8N: OpenAI integration workflow │
|
||||||
|
│ - Output: Generated content with formatting │
|
||||||
|
│ │
|
||||||
|
│ Data Analyzer: │
|
||||||
|
│ - Input: CSV/Excel file upload │
|
||||||
|
│ - N8N: Data processing workflow │
|
||||||
|
│ - Output: Analysis results, charts, insights │
|
||||||
|
│ │
|
||||||
|
│ 7. File Structure │
|
||||||
|
│ │
|
||||||
|
│ src/app/agent/ │
|
||||||
|
│ ├── pdf-processor/ │
|
||||||
|
│ │ └── page.tsx │
|
||||||
|
│ ├── weather-report/ │
|
||||||
|
│ │ └── page.tsx │
|
||||||
|
│ ├── content-writer/ │
|
||||||
|
│ │ └── page.tsx │
|
||||||
|
│ ├── data-analyzer/ │
|
||||||
|
│ │ └── page.tsx │
|
||||||
|
│ ├── email-automation/ │
|
||||||
|
│ │ └── page.tsx │
|
||||||
|
│ └── task-automation/ │
|
||||||
|
│ └── page.tsx │
|
||||||
|
│ │
|
||||||
|
│ 8. Shared Components │
|
||||||
|
│ │
|
||||||
|
│ src/components/agent-shared/ │
|
||||||
|
│ ├── AgentLayout.tsx # Common layout for all agent pages │
|
||||||
|
│ ├── FileUpload.tsx # File upload component │
|
||||||
|
│ ├── ProcessingStatus.tsx # Status indicator │
|
||||||
|
│ ├── ResultsDisplay.tsx # Results formatting │
|
||||||
|
│ └── CreditCounter.tsx # Credit cost display │
|
||||||
|
│ │
|
||||||
|
│ 9. Implementation Steps │
|
||||||
|
│ │
|
||||||
|
│ 1. Create agent slug mapping in database │
|
||||||
|
│ 2. Update main page to redirect instead of showing modal │
|
||||||
|
│ 3. Create first agent page (PDF processor) as template │
|
||||||
|
│ 4. Add n8n webhook integration │
|
||||||
|
│ 5. Implement Google Sheets result fetching │
|
||||||
|
│ 6. Add credit deduction on success │
|
||||||
|
│ 7. Replicate pattern for other agents │
|
||||||
|
│ │
|
||||||
|
│ This approach keeps each agent simple and hardcoded while providing real functionality through n8n │
|
||||||
|
│ workflows and external integrations. │
|
||||||
|
╰──────────────────────────────────────
|
||||||
26
agents/admin.py
Normal file
26
agents/admin.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Agent
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Agent)
|
||||||
|
class AgentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'slug', 'category', 'price', 'is_active', 'rating', 'review_count')
|
||||||
|
list_filter = ('category', 'is_active', 'created_at')
|
||||||
|
search_fields = ('name', 'slug', 'description')
|
||||||
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
list_editable = ('price', 'is_active')
|
||||||
|
ordering = ('-created_at',)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Basic Information', {
|
||||||
|
'fields': ('name', 'slug', 'description', 'category', 'icon')
|
||||||
|
}),
|
||||||
|
('Pricing & Rating', {
|
||||||
|
'fields': ('price', 'rating', 'review_count')
|
||||||
|
}),
|
||||||
|
('Configuration', {
|
||||||
|
'fields': ('is_active', 'n8n_webhook_url')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = ('created_at',)
|
||||||
@ -5,6 +5,7 @@ from django.core.files.base import ContentFile
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class AgentProcessor:
|
class AgentProcessor:
|
||||||
def __init__(self, agent_slug):
|
def __init__(self, agent_slug):
|
||||||
self.agent_slug = agent_slug
|
self.agent_slug = agent_slug
|
||||||
@ -128,4 +129,4 @@ class AgentProcessor:
|
|||||||
if not processor:
|
if not processor:
|
||||||
raise ValueError(f"No processor found for agent: {self.agent_slug}")
|
raise ValueError(f"No processor found for agent: {self.agent_slug}")
|
||||||
|
|
||||||
return processor(**kwargs)
|
return processor(**kwargs)
|
||||||
91
agents/management/commands/populate_agents.py
Normal file
91
agents/management/commands/populate_agents.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from agents.models import Agent
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Populate the database with sample agent data'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# Clear existing agents
|
||||||
|
Agent.objects.all().delete()
|
||||||
|
|
||||||
|
# Create sample agents
|
||||||
|
agents_data = [
|
||||||
|
{
|
||||||
|
'name': 'Data Analyzer',
|
||||||
|
'slug': 'data-analyzer',
|
||||||
|
'description': 'Upload your data files and get comprehensive analysis with insights, trends, and visualizations.',
|
||||||
|
'category': 'analytics',
|
||||||
|
'price': Decimal('15.00'),
|
||||||
|
'icon': '📊',
|
||||||
|
'rating': Decimal('4.7'),
|
||||||
|
'review_count': 324,
|
||||||
|
'n8n_webhook_url': 'https://n8n.example.com/webhook/data-analyzer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Weather Reporter',
|
||||||
|
'slug': 'weather-reporter',
|
||||||
|
'description': 'Get current weather conditions, forecasts, and detailed meteorological data for any location.',
|
||||||
|
'category': 'utilities',
|
||||||
|
'price': Decimal('5.00'),
|
||||||
|
'icon': '🌤️',
|
||||||
|
'rating': Decimal('4.5'),
|
||||||
|
'review_count': 892,
|
||||||
|
'n8n_webhook_url': '', # Uses OpenWeather API directly
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': '5 Whys Analysis',
|
||||||
|
'slug': 'five-whys',
|
||||||
|
'description': 'Perform root cause analysis using the 5 Whys technique to identify the underlying cause of problems.',
|
||||||
|
'category': 'analytics',
|
||||||
|
'price': Decimal('12.00'),
|
||||||
|
'icon': '❓',
|
||||||
|
'rating': Decimal('4.6'),
|
||||||
|
'review_count': 156,
|
||||||
|
'n8n_webhook_url': 'https://n8n.example.com/webhook/five-whys',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'FAQ Generator',
|
||||||
|
'slug': 'faq-generator',
|
||||||
|
'description': 'Generate comprehensive FAQ sections from your content, documentation, or product information.',
|
||||||
|
'category': 'content',
|
||||||
|
'price': Decimal('10.00'),
|
||||||
|
'icon': '❔',
|
||||||
|
'rating': Decimal('4.4'),
|
||||||
|
'review_count': 287,
|
||||||
|
'n8n_webhook_url': 'https://n8n.example.com/webhook/faq-generator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Social Ads Generator',
|
||||||
|
'slug': 'social-ads-generator',
|
||||||
|
'description': 'Create compelling social media advertisements with copy, targeting suggestions, and campaign ideas.',
|
||||||
|
'category': 'marketing',
|
||||||
|
'price': Decimal('20.00'),
|
||||||
|
'icon': '📢',
|
||||||
|
'rating': Decimal('4.8'),
|
||||||
|
'review_count': 543,
|
||||||
|
'n8n_webhook_url': 'https://n8n.example.com/webhook/social-ads',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Job Posting Generator',
|
||||||
|
'slug': 'job-posting-generator',
|
||||||
|
'description': 'Generate professional job postings with requirements, responsibilities, and compelling descriptions.',
|
||||||
|
'category': 'content',
|
||||||
|
'price': Decimal('8.00'),
|
||||||
|
'icon': '💼',
|
||||||
|
'rating': Decimal('4.3'),
|
||||||
|
'review_count': 198,
|
||||||
|
'n8n_webhook_url': 'https://n8n.example.com/webhook/job-posting',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for agent_data in agents_data:
|
||||||
|
agent = Agent.objects.create(**agent_data)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'Successfully created agent: {agent.name}')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'Successfully populated {len(agents_data)} agents')
|
||||||
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.4 on 2025-07-08 08:17
|
# Generated by Django 5.2.4 on 2025-07-08 15:00
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
# Create your models here.
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
class Agent(models.Model):
|
class Agent(models.Model):
|
||||||
CATEGORIES = [
|
CATEGORIES = [
|
||||||
('analytics', 'Analytics'),
|
('analytics', 'Analytics'),
|
||||||
@ -39,4 +38,4 @@ class Agent(models.Model):
|
|||||||
'marketing': 'from-pink-500 to-rose-600',
|
'marketing': 'from-pink-500 to-rose-600',
|
||||||
'customer-service': 'from-blue-500 to-blue-600',
|
'customer-service': 'from-blue-500 to-blue-600',
|
||||||
}
|
}
|
||||||
return gradient_map.get(self.category, 'from-gray-500 to-gray-600')
|
return gradient_map.get(self.category, 'from-gray-500 to-gray-600')
|
||||||
@ -1,80 +0,0 @@
|
|||||||
from django.core.management.base import BaseCommand
|
|
||||||
from agents.models import Agent
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Populate database with default agents'
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
agents = [
|
|
||||||
{
|
|
||||||
'name': '5 Whys Analysis Agent',
|
|
||||||
'slug': 'five-whys',
|
|
||||||
'description': 'Systematic root cause analysis using the proven 5 Whys methodology to identify and solve business problems effectively.',
|
|
||||||
'category': 'analytics',
|
|
||||||
'price': Decimal('8.00'),
|
|
||||||
'icon': '🔍',
|
|
||||||
'rating': Decimal('4.8'),
|
|
||||||
'review_count': 850,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Data Analysis Agent',
|
|
||||||
'slug': 'data-analyzer',
|
|
||||||
'description': 'Processes complex datasets and generates actionable insights with automated reporting and visualization capabilities.',
|
|
||||||
'category': 'analytics',
|
|
||||||
'price': Decimal('5.00'),
|
|
||||||
'icon': '📊',
|
|
||||||
'rating': Decimal('4.8'),
|
|
||||||
'review_count': 1800,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Weather Reporter Agent',
|
|
||||||
'slug': 'weather-reporter',
|
|
||||||
'description': 'Get detailed weather reports for any location worldwide with current conditions, forecasts, and weather alerts.',
|
|
||||||
'category': 'utilities',
|
|
||||||
'price': Decimal('2.00'),
|
|
||||||
'icon': '🌤️',
|
|
||||||
'rating': Decimal('4.9'),
|
|
||||||
'review_count': 1650,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Job Posting Generator Agent',
|
|
||||||
'slug': 'job-posting-generator',
|
|
||||||
'description': 'Create compelling, professional job postings with AI-powered content generation.',
|
|
||||||
'category': 'content',
|
|
||||||
'price': Decimal('3.00'),
|
|
||||||
'icon': '📝',
|
|
||||||
'rating': Decimal('4.7'),
|
|
||||||
'review_count': 1200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Social Ads Generator Agent',
|
|
||||||
'slug': 'social-ads-generator',
|
|
||||||
'description': 'Create engaging social media advertisements optimized for different platforms.',
|
|
||||||
'category': 'marketing',
|
|
||||||
'price': Decimal('4.00'),
|
|
||||||
'icon': '📱',
|
|
||||||
'rating': Decimal('4.8'),
|
|
||||||
'review_count': 950,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'FAQ Generator Agent',
|
|
||||||
'slug': 'faq-generator',
|
|
||||||
'description': 'Generate comprehensive FAQs from uploaded files or website URLs.',
|
|
||||||
'category': 'content',
|
|
||||||
'price': Decimal('3.00'),
|
|
||||||
'icon': '❓',
|
|
||||||
'rating': Decimal('4.7'),
|
|
||||||
'review_count': 750,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
for agent_data in agents:
|
|
||||||
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:
|
|
||||||
self.stdout.write(f'Agent already exists: {agent.name}')
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
# Agent management URLs would go here
|
|
||||||
# For now, agents are handled by core app
|
|
||||||
]
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from django.contrib.auth import views as auth_views
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('login/', auth_views.LoginView.as_view(template_name='auth/login.html'), name='login'),
|
|
||||||
path('logout/', auth_views.LogoutView.as_view(next_page='homepage'), name='logout'),
|
|
||||||
path('register/', views.register, name='register'),
|
|
||||||
path('password-reset/', auth_views.PasswordResetView.as_view(
|
|
||||||
template_name='auth/password_reset.html',
|
|
||||||
email_template_name='auth/password_reset_email.html',
|
|
||||||
success_url='/auth/password-reset/done/'
|
|
||||||
), name='password_reset'),
|
|
||||||
path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(
|
|
||||||
template_name='auth/password_reset_done.html'
|
|
||||||
), name='password_reset_done'),
|
|
||||||
path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(
|
|
||||||
template_name='auth/password_reset_confirm.html',
|
|
||||||
success_url='/auth/reset/done/'
|
|
||||||
), name='password_reset_confirm'),
|
|
||||||
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(
|
|
||||||
template_name='auth/password_reset_complete.html'
|
|
||||||
), name='password_reset_complete'),
|
|
||||||
]
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
from django.shortcuts import render, redirect
|
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.contrib.auth.forms import UserCreationForm
|
|
||||||
from django.contrib import messages
|
|
||||||
|
|
||||||
def register(request):
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = UserCreationForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
user = form.save()
|
|
||||||
login(request, user)
|
|
||||||
messages.success(request, 'Registration successful!')
|
|
||||||
return redirect('homepage')
|
|
||||||
else:
|
|
||||||
form = UserCreationForm()
|
|
||||||
return render(request, 'auth/register.html', {'form': form})
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('', views.homepage, name='homepage'),
|
|
||||||
path('marketplace/', views.marketplace, name='marketplace'),
|
|
||||||
path('pricing/', views.pricing, name='pricing'),
|
|
||||||
path('debug/', views.debug_page, name='debug'),
|
|
||||||
path('reset-password/', views.reset_password, name='reset_password'),
|
|
||||||
path('agent/<slug:slug>/', views.agent_detail, name='agent_detail'),
|
|
||||||
path('agent/<slug:slug>/process/', views.process_agent, name='process_agent'),
|
|
||||||
path('profile/', views.profile, name='profile'),
|
|
||||||
|
|
||||||
# API endpoints
|
|
||||||
path('api/wallet/balance/', views.check_wallet_balance, name='api_wallet_balance'),
|
|
||||||
path('api/chat/<slug:slug>/', views.chat_message, name='api_chat_message'),
|
|
||||||
]
|
|
||||||
@ -1,452 +0,0 @@
|
|||||||
# Create your views here.
|
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.utils import timezone
|
|
||||||
from agents.models import Agent
|
|
||||||
from agents.agent_processors import AgentProcessor
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
def homepage(request):
|
|
||||||
"""Enhanced homepage with all features from Next.js version"""
|
|
||||||
# Handle contact form submission
|
|
||||||
if request.method == 'POST':
|
|
||||||
name = request.POST.get('name')
|
|
||||||
email = request.POST.get('email')
|
|
||||||
company = request.POST.get('company', '')
|
|
||||||
message = request.POST.get('message')
|
|
||||||
|
|
||||||
if name and email and message:
|
|
||||||
# Here you can save to database or send email
|
|
||||||
# For now, just show success message
|
|
||||||
messages.success(request, f'Thank you {name}! Your message has been sent. We will get back to you soon.')
|
|
||||||
return redirect('homepage')
|
|
||||||
else:
|
|
||||||
messages.error(request, 'Please fill in all required fields.')
|
|
||||||
|
|
||||||
# Get featured agents for preview
|
|
||||||
featured_agents = Agent.objects.filter(is_active=True)[:3]
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'featured_agents': featured_agents,
|
|
||||||
'total_agents': Agent.objects.filter(is_active=True).count(),
|
|
||||||
'total_users': User.objects.count(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, 'homepage.html', context)
|
|
||||||
|
|
||||||
def marketplace(request):
|
|
||||||
"""Display all available agents with enhanced filtering"""
|
|
||||||
category_filter = request.GET.get('category')
|
|
||||||
search_query = request.GET.get('search')
|
|
||||||
|
|
||||||
agents = Agent.objects.filter(is_active=True)
|
|
||||||
|
|
||||||
if category_filter:
|
|
||||||
agents = agents.filter(category=category_filter)
|
|
||||||
|
|
||||||
if search_query:
|
|
||||||
from django.db import models
|
|
||||||
agents = agents.filter(
|
|
||||||
models.Q(name__icontains=search_query) |
|
|
||||||
models.Q(description__icontains=search_query)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get unique categories for filter dropdown
|
|
||||||
categories = Agent.objects.filter(is_active=True).values_list('category', flat=True).distinct()
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'agents': agents.order_by('category', 'name'),
|
|
||||||
'categories': categories,
|
|
||||||
'current_category': category_filter,
|
|
||||||
'search_query': search_query,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, 'marketplace.html', context)
|
|
||||||
|
|
||||||
def pricing(request):
|
|
||||||
"""Enhanced pricing page with payment status handling"""
|
|
||||||
packages = [
|
|
||||||
{
|
|
||||||
'id': 'basic',
|
|
||||||
'amount': 10,
|
|
||||||
'price': 9.99,
|
|
||||||
'label': 'Basic',
|
|
||||||
'description': 'Perfect for trying out AI agents',
|
|
||||||
'features': ['2-4 agent uses', 'Basic support', 'Email notifications'],
|
|
||||||
'icon': '💰',
|
|
||||||
'gradient': 'from-blue-500 to-purple-600'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'popular',
|
|
||||||
'amount': 50,
|
|
||||||
'price': 49.99,
|
|
||||||
'label': 'Popular',
|
|
||||||
'description': 'Most popular choice for regular users',
|
|
||||||
'features': ['10-25 agent uses', 'Priority support', 'Advanced analytics', 'Export options'],
|
|
||||||
'icon': '⭐',
|
|
||||||
'gradient': 'from-purple-500 to-pink-600',
|
|
||||||
'popular': True
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'premium',
|
|
||||||
'amount': 100,
|
|
||||||
'price': 99.99,
|
|
||||||
'label': 'Premium',
|
|
||||||
'description': 'For power users and small teams',
|
|
||||||
'features': ['50+ agent uses', '24/7 support', 'Custom integrations', 'Team collaboration'],
|
|
||||||
'icon': '🚀',
|
|
||||||
'gradient': 'from-green-500 to-teal-600'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'enterprise',
|
|
||||||
'amount': 500,
|
|
||||||
'price': 499.99,
|
|
||||||
'label': 'Enterprise',
|
|
||||||
'description': 'For large teams and businesses',
|
|
||||||
'features': ['Unlimited uses', 'Dedicated support', 'Custom development', 'SLA guarantee'],
|
|
||||||
'icon': '👑',
|
|
||||||
'gradient': 'from-yellow-500 to-red-600'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Handle payment status messages (prevent duplicate messages)
|
|
||||||
payment_status = request.GET.get('payment')
|
|
||||||
session_id = request.GET.get('session_id')
|
|
||||||
|
|
||||||
# Create session key to prevent duplicate messages
|
|
||||||
if payment_status:
|
|
||||||
session_key = f"payment_message_{payment_status}_{session_id or 'cancelled'}"
|
|
||||||
if not request.session.get(session_key):
|
|
||||||
request.session[session_key] = True
|
|
||||||
|
|
||||||
if payment_status == 'success':
|
|
||||||
messages.success(request, '✅ Payment successful! Your wallet has been topped up.')
|
|
||||||
elif payment_status == 'cancelled':
|
|
||||||
messages.error(request, '❌ Payment was cancelled. No charges were made.')
|
|
||||||
|
|
||||||
# FAQ data
|
|
||||||
faqs = [
|
|
||||||
{
|
|
||||||
'question': 'How does the pay-per-use pricing work?',
|
|
||||||
'answer': 'You add money to your wallet and pay for each AI agent use. Prices range from 2.00 to 8.00 AED per use.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'question': 'Do wallet funds expire?',
|
|
||||||
'answer': 'No, your wallet balance never expires. Use it whenever you need AI assistance.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'question': 'Can I get a refund?',
|
|
||||||
'answer': 'Yes, unused wallet balance can be refunded within 30 days of purchase.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'question': 'Is my payment information secure?',
|
|
||||||
'answer': 'Absolutely. We use Stripe for secure payment processing and never store your payment details.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'packages': packages,
|
|
||||||
'faqs': faqs,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, 'pricing.html', context)
|
|
||||||
|
|
||||||
def debug_page(request):
|
|
||||||
"""Debug page for development environment checking"""
|
|
||||||
if not settings.DEBUG:
|
|
||||||
context = {'debug_mode': False}
|
|
||||||
return render(request, 'debug.html', context)
|
|
||||||
|
|
||||||
# Environment status check
|
|
||||||
env_status = {
|
|
||||||
'DATABASE_URL': bool(os.getenv('DATABASE_URL')),
|
|
||||||
'STRIPE_SECRET_KEY': bool(settings.STRIPE_SECRET_KEY),
|
|
||||||
'N8N_WEBHOOK_DATA_ANALYZER': bool(settings.N8N_WEBHOOK_DATA_ANALYZER),
|
|
||||||
'N8N_WEBHOOK_FIVE_WHYS': bool(settings.N8N_WEBHOOK_FIVE_WHYS),
|
|
||||||
'OPENWEATHER_API_KEY': bool(settings.OPENWEATHER_API_KEY),
|
|
||||||
'DEBUG': settings.DEBUG,
|
|
||||||
'ALLOWED_HOSTS': settings.ALLOWED_HOSTS,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Database connection test
|
|
||||||
try:
|
|
||||||
user_count = User.objects.count()
|
|
||||||
agent_count = Agent.objects.count()
|
|
||||||
db_status = {'status': 'Connected', 'color': 'green'}
|
|
||||||
except Exception as e:
|
|
||||||
user_count = 0
|
|
||||||
agent_count = 0
|
|
||||||
db_status = {'status': f'Error: {str(e)}', 'color': 'red'}
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'debug_mode': True,
|
|
||||||
'env_status': json.dumps(env_status, indent=2),
|
|
||||||
'db_status': db_status,
|
|
||||||
'user_count': user_count,
|
|
||||||
'agent_count': agent_count,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, 'debug.html', context)
|
|
||||||
|
|
||||||
def reset_password(request):
|
|
||||||
"""Password reset functionality"""
|
|
||||||
if request.method == 'POST':
|
|
||||||
password = request.POST.get('password')
|
|
||||||
confirm_password = request.POST.get('confirm_password')
|
|
||||||
|
|
||||||
if not password or not confirm_password:
|
|
||||||
context = {'error': 'Both password fields are required'}
|
|
||||||
return render(request, 'reset_password.html', context)
|
|
||||||
|
|
||||||
if password != confirm_password:
|
|
||||||
context = {'error': 'Passwords do not match'}
|
|
||||||
return render(request, 'reset_password.html', context)
|
|
||||||
|
|
||||||
if len(password) < 8:
|
|
||||||
context = {'error': 'Password must be at least 8 characters long'}
|
|
||||||
return render(request, 'reset_password.html', context)
|
|
||||||
|
|
||||||
# In a real implementation, you would:
|
|
||||||
# 1. Verify the reset token from the URL
|
|
||||||
# 2. Update the user's password
|
|
||||||
# 3. Redirect to login with success message
|
|
||||||
|
|
||||||
messages.success(request, 'Password updated successfully! Please log in with your new password.')
|
|
||||||
return redirect('homepage')
|
|
||||||
|
|
||||||
# Check if we have a valid reset token (simplified version)
|
|
||||||
token = request.GET.get('token')
|
|
||||||
if not token:
|
|
||||||
context = {'error': 'Invalid or expired reset link. Please request a new password reset.'}
|
|
||||||
return render(request, 'reset_password.html', context)
|
|
||||||
|
|
||||||
return render(request, 'reset_password.html')
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def agent_detail(request, slug):
|
|
||||||
"""Enhanced agent detail page with wallet balance checking"""
|
|
||||||
agent = get_object_or_404(Agent, slug=slug, is_active=True)
|
|
||||||
|
|
||||||
# Calculate wallet status
|
|
||||||
user_balance = request.user.wallet_balance
|
|
||||||
has_sufficient_balance = user_balance >= agent.price
|
|
||||||
|
|
||||||
# Calculate usage count
|
|
||||||
if has_sufficient_balance:
|
|
||||||
possible_uses = int(user_balance / agent.price)
|
|
||||||
else:
|
|
||||||
possible_uses = 0
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'agent': agent,
|
|
||||||
'user_balance': user_balance,
|
|
||||||
'has_sufficient_balance': has_sufficient_balance,
|
|
||||||
'possible_uses': possible_uses,
|
|
||||||
'balance_after_use': user_balance - agent.price if has_sufficient_balance else user_balance,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, 'agent_detail.html', context)
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_http_methods(["POST"])
|
|
||||||
def process_agent(request, slug):
|
|
||||||
"""Enhanced agent processing with comprehensive error handling"""
|
|
||||||
agent = get_object_or_404(Agent, slug=slug, is_active=True)
|
|
||||||
|
|
||||||
# Check wallet balance
|
|
||||||
if not request.user.has_sufficient_balance(agent.price):
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': f'Insufficient balance. Required: {agent.price_display}, Available: {request.user.wallet_balance:.2f} AED'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Process based on agent type
|
|
||||||
processor = AgentProcessor(agent.slug)
|
|
||||||
|
|
||||||
if agent.slug == 'data-analyzer':
|
|
||||||
file_obj = request.FILES.get('file')
|
|
||||||
if not file_obj:
|
|
||||||
return JsonResponse({'success': False, 'error': 'File is required'}, status=400)
|
|
||||||
|
|
||||||
# Validate file size (10MB limit)
|
|
||||||
if file_obj.size > 10 * 1024 * 1024:
|
|
||||||
return JsonResponse({'success': False, 'error': 'File size must be less than 10MB'}, status=400)
|
|
||||||
|
|
||||||
# Validate file type
|
|
||||||
allowed_extensions = ['.csv', '.xlsx', '.xls', '.json']
|
|
||||||
file_extension = os.path.splitext(file_obj.name)[1].lower()
|
|
||||||
if file_extension not in allowed_extensions:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Invalid file type. Allowed: CSV, Excel, JSON'}, status=400)
|
|
||||||
|
|
||||||
result = processor.process_agent(file_obj=file_obj, user_id=str(request.user.id))
|
|
||||||
|
|
||||||
elif agent.slug == 'five-whys':
|
|
||||||
problem = request.POST.get('problem')
|
|
||||||
if not problem or len(problem.strip()) < 10:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Problem description must be at least 10 characters'}, status=400)
|
|
||||||
result = processor.process_agent(problem_description=problem, user_id=str(request.user.id))
|
|
||||||
|
|
||||||
elif agent.slug == 'weather-reporter':
|
|
||||||
location = request.POST.get('location')
|
|
||||||
if not location or len(location.strip()) < 2:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Location must be at least 2 characters'}, status=400)
|
|
||||||
result = processor.process_agent(location=location)
|
|
||||||
|
|
||||||
elif agent.slug == 'job-posting-generator':
|
|
||||||
required_fields = ['title', 'company', 'description', 'requirements']
|
|
||||||
job_details = {}
|
|
||||||
|
|
||||||
for field in required_fields:
|
|
||||||
value = request.POST.get(field, '').strip()
|
|
||||||
if not value:
|
|
||||||
return JsonResponse({'success': False, 'error': f'{field.title()} is required'}, status=400)
|
|
||||||
if len(value) < 5:
|
|
||||||
return JsonResponse({'success': False, 'error': f'{field.title()} must be at least 5 characters'}, status=400)
|
|
||||||
job_details[field] = value
|
|
||||||
|
|
||||||
result = processor.process_agent(job_details=job_details, user_id=str(request.user.id))
|
|
||||||
|
|
||||||
elif agent.slug == 'social-ads-generator':
|
|
||||||
required_fields = ['product', 'platform', 'target_audience', 'tone']
|
|
||||||
ad_requirements = {}
|
|
||||||
|
|
||||||
for field in required_fields:
|
|
||||||
value = request.POST.get(field, '').strip()
|
|
||||||
if not value:
|
|
||||||
return JsonResponse({'success': False, 'error': f'{field.replace("_", " ").title()} is required'}, status=400)
|
|
||||||
ad_requirements[field] = value
|
|
||||||
|
|
||||||
# Validate platform
|
|
||||||
valid_platforms = ['facebook', 'instagram', 'twitter', 'linkedin']
|
|
||||||
if ad_requirements['platform'] not in valid_platforms:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Invalid platform selected'}, status=400)
|
|
||||||
|
|
||||||
# Validate tone
|
|
||||||
valid_tones = ['professional', 'casual', 'humorous', 'urgent']
|
|
||||||
if ad_requirements['tone'] not in valid_tones:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Invalid tone selected'}, status=400)
|
|
||||||
|
|
||||||
result = processor.process_agent(ad_requirements=ad_requirements, user_id=str(request.user.id))
|
|
||||||
|
|
||||||
elif agent.slug == 'faq-generator':
|
|
||||||
content_source = request.POST.get('content_source', '').strip()
|
|
||||||
if not content_source:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Content source is required'}, status=400)
|
|
||||||
if len(content_source) < 50:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Content source must be at least 50 characters'}, status=400)
|
|
||||||
|
|
||||||
result = processor.process_agent(content_source=content_source, user_id=str(request.user.id))
|
|
||||||
|
|
||||||
else:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Agent not supported'}, status=400)
|
|
||||||
|
|
||||||
# Deduct balance on successful processing
|
|
||||||
if request.user.deduct_balance(
|
|
||||||
agent.price,
|
|
||||||
f"Used {agent.name}",
|
|
||||||
agent.slug
|
|
||||||
):
|
|
||||||
return JsonResponse({
|
|
||||||
'success': True,
|
|
||||||
'result': result,
|
|
||||||
'new_balance': float(request.user.wallet_balance),
|
|
||||||
'agent_used': agent.name,
|
|
||||||
'cost': float(agent.price)
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Failed to process payment. Please try again.'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Log the error in production
|
|
||||||
if not settings.DEBUG:
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.error(f"Agent processing error: {str(e)}", exc_info=True)
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': 'An error occurred while processing your request. Please try again later.'
|
|
||||||
}, status=500)
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def profile(request):
|
|
||||||
"""Enhanced user profile with transaction history and wallet management"""
|
|
||||||
# Get recent transactions
|
|
||||||
transactions = request.user.wallet_transactions.all()[:50] # Last 50 transactions
|
|
||||||
|
|
||||||
# Calculate usage statistics
|
|
||||||
total_spent = sum(abs(t.amount) for t in transactions if t.type == 'agent_usage')
|
|
||||||
total_topped_up = sum(t.amount for t in transactions if t.type == 'top_up')
|
|
||||||
total_agents_used = transactions.filter(type='agent_usage').count()
|
|
||||||
|
|
||||||
# Get most used agents
|
|
||||||
from django.db.models import Count
|
|
||||||
popular_agents = (transactions.filter(type='agent_usage')
|
|
||||||
.values('agent_slug')
|
|
||||||
.annotate(count=Count('agent_slug'))
|
|
||||||
.order_by('-count')[:5])
|
|
||||||
|
|
||||||
# Wallet status
|
|
||||||
balance = request.user.wallet_balance
|
|
||||||
if balance < 5:
|
|
||||||
wallet_status = {'status': 'low', 'color': 'red', 'message': 'Low balance - Add money to continue using agents'}
|
|
||||||
elif balance < 20:
|
|
||||||
wallet_status = {'status': 'medium', 'color': 'orange', 'message': 'Consider adding more funds'}
|
|
||||||
else:
|
|
||||||
wallet_status = {'status': 'high', 'color': 'green', 'message': 'Good balance'}
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'transactions': transactions,
|
|
||||||
'total_spent': total_spent,
|
|
||||||
'total_topped_up': total_topped_up,
|
|
||||||
'total_agents_used': total_agents_used,
|
|
||||||
'popular_agents': popular_agents,
|
|
||||||
'wallet_status': wallet_status,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, 'profile.html', context)
|
|
||||||
|
|
||||||
# API endpoints for AJAX functionality
|
|
||||||
@login_required
|
|
||||||
@require_http_methods(["GET"])
|
|
||||||
def check_wallet_balance(request):
|
|
||||||
"""API endpoint to check current wallet balance"""
|
|
||||||
return JsonResponse({
|
|
||||||
'balance': float(request.user.wallet_balance),
|
|
||||||
'formatted_balance': f"{request.user.wallet_balance:.2f} AED"
|
|
||||||
})
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_http_methods(["POST"])
|
|
||||||
def chat_message(request, slug):
|
|
||||||
"""Handle chat messages for interactive agents like 5 Whys"""
|
|
||||||
if slug != 'five-whys':
|
|
||||||
return JsonResponse({'success': False, 'error': 'Chat not available for this agent'}, status=400)
|
|
||||||
|
|
||||||
message = request.POST.get('message', '').strip()
|
|
||||||
if not message:
|
|
||||||
return JsonResponse({'success': False, 'error': 'Message is required'}, status=400)
|
|
||||||
|
|
||||||
# Here you would integrate with your 5 Whys processing logic
|
|
||||||
# For now, return a simple response
|
|
||||||
response_message = f"Thank you for: {message}. Let me ask you the next Why question..."
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
'success': True,
|
|
||||||
'response': response_message,
|
|
||||||
'timestamp': timezone.now().isoformat()
|
|
||||||
})
|
|
||||||
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import stripe
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
|
||||||
|
|
||||||
class StripeHandler:
|
|
||||||
"""Handle all Stripe-related operations"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.packages = {
|
|
||||||
'basic': {'amount': 999, 'currency': 'aed', 'name': 'Basic Package - 10 AED', 'wallet_amount': 10},
|
|
||||||
'popular': {'amount': 4999, 'currency': 'aed', 'name': 'Popular Package - 50 AED', 'wallet_amount': 50},
|
|
||||||
'premium': {'amount': 9999, 'currency': 'aed', 'name': 'Premium Package - 100 AED', 'wallet_amount': 100},
|
|
||||||
'enterprise': {'amount': 49999, 'currency': 'aed', 'name': 'Enterprise Package - 500 AED', 'wallet_amount': 500},
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_checkout_session(self, amount, user_id, package_id):
|
|
||||||
"""Create Stripe checkout session"""
|
|
||||||
package = self.packages.get(package_id)
|
|
||||||
if not package:
|
|
||||||
raise ValueError('Invalid package')
|
|
||||||
|
|
||||||
session = stripe.checkout.Session.create(
|
|
||||||
payment_method_types=['card'],
|
|
||||||
line_items=[{
|
|
||||||
'price_data': {
|
|
||||||
'currency': package['currency'],
|
|
||||||
'product_data': {
|
|
||||||
'name': package['name'],
|
|
||||||
},
|
|
||||||
'unit_amount': package['amount'],
|
|
||||||
},
|
|
||||||
'quantity': 1,
|
|
||||||
}],
|
|
||||||
mode='payment',
|
|
||||||
success_url='https://yoursite.com/wallet/success/?session_id={CHECKOUT_SESSION_ID}',
|
|
||||||
cancel_url='https://yoursite.com/wallet/cancel/',
|
|
||||||
client_reference_id=str(user_id),
|
|
||||||
metadata={
|
|
||||||
'package_id': package_id,
|
|
||||||
'user_id': str(user_id),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return session
|
|
||||||
|
|
||||||
def handle_webhook_event(self, event):
|
|
||||||
"""Handle Stripe webhook events"""
|
|
||||||
if event['type'] == 'checkout.session.completed':
|
|
||||||
session = event['data']['object']
|
|
||||||
|
|
||||||
# Get user and package info
|
|
||||||
user_id = session['client_reference_id']
|
|
||||||
package_id = session['metadata']['package_id']
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = User.objects.get(id=user_id)
|
|
||||||
package = self.packages.get(package_id)
|
|
||||||
|
|
||||||
if package:
|
|
||||||
amount = package['wallet_amount']
|
|
||||||
user.add_balance(
|
|
||||||
amount,
|
|
||||||
f"Wallet top-up: {amount} AED",
|
|
||||||
session['id']
|
|
||||||
)
|
|
||||||
|
|
||||||
except User.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('create-checkout/', views.create_checkout_session, name='create_checkout_session'),
|
|
||||||
path('webhook/', views.stripe_webhook, name='stripe_webhook'),
|
|
||||||
path('success/', views.payment_success, name='payment_success'),
|
|
||||||
path('cancel/', views.payment_cancel, name='payment_cancel'),
|
|
||||||
]
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
from django.shortcuts import render, redirect
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
|
||||||
from django.http import JsonResponse, HttpResponse
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from .stripe_handler import StripeHandler
|
|
||||||
import json
|
|
||||||
import stripe
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_http_methods(["POST"])
|
|
||||||
def create_checkout_session(request):
|
|
||||||
"""Create Stripe checkout session for wallet top-up"""
|
|
||||||
try:
|
|
||||||
data = json.loads(request.body)
|
|
||||||
amount = data.get('amount')
|
|
||||||
package_id = data.get('package_id')
|
|
||||||
|
|
||||||
if not amount or amount <= 0:
|
|
||||||
return JsonResponse({'error': 'Invalid amount'}, status=400)
|
|
||||||
|
|
||||||
# Create Stripe checkout session
|
|
||||||
stripe_handler = StripeHandler()
|
|
||||||
session = stripe_handler.create_checkout_session(
|
|
||||||
amount=amount,
|
|
||||||
user_id=request.user.id,
|
|
||||||
package_id=package_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return JsonResponse({'checkout_url': session.url})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return JsonResponse({'error': str(e)}, status=500)
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["POST"])
|
|
||||||
def stripe_webhook(request):
|
|
||||||
"""Handle Stripe webhook events"""
|
|
||||||
payload = request.body
|
|
||||||
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
|
|
||||||
|
|
||||||
try:
|
|
||||||
event = stripe.Webhook.construct_event(
|
|
||||||
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
|
|
||||||
)
|
|
||||||
|
|
||||||
stripe_handler = StripeHandler()
|
|
||||||
stripe_handler.handle_webhook_event(event)
|
|
||||||
|
|
||||||
return HttpResponse(status=200)
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
except stripe.error.SignatureVerificationError:
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
except Exception as e:
|
|
||||||
return HttpResponse(status=500)
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def payment_success(request):
|
|
||||||
"""Handle successful payment redirect"""
|
|
||||||
session_id = request.GET.get('session_id')
|
|
||||||
if session_id:
|
|
||||||
messages.success(request, '✅ Payment successful! Your wallet has been topped up.')
|
|
||||||
return redirect('pricing')
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def payment_cancel(request):
|
|
||||||
"""Handle cancelled payment redirect"""
|
|
||||||
messages.error(request, '❌ Payment was cancelled. No charges were made.')
|
|
||||||
return redirect('pricing')
|
|
||||||
22
authentication/admin.py
Normal file
22
authentication/admin.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
list_display = ('username', 'email', 'wallet_balance', 'is_staff', 'is_active', 'date_joined')
|
||||||
|
list_filter = ('is_staff', 'is_active', 'date_joined')
|
||||||
|
search_fields = ('username', 'email')
|
||||||
|
ordering = ('-date_joined',)
|
||||||
|
|
||||||
|
fieldsets = UserAdmin.fieldsets + (
|
||||||
|
('Wallet Information', {'fields': ('wallet_balance',)}),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = ('date_joined', 'last_login')
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
if obj: # editing an existing object
|
||||||
|
return self.readonly_fields + ('username',)
|
||||||
|
return self.readonly_fields
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.4 on 2025-07-08 08:17
|
# Generated by Django 5.2.4 on 2025-07-08 15:00
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
@ -1,8 +1,8 @@
|
|||||||
# Create your models here.
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
email = models.EmailField(unique=True)
|
email = models.EmailField(unique=True)
|
||||||
wallet_balance = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
wallet_balance = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||||
9
authentication/urls.py
Normal file
9
authentication/urls.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('login/', views.login_view, name='login'),
|
||||||
|
path('register/', views.register_view, name='register'),
|
||||||
|
path('logout/', views.logout_view, name='logout'),
|
||||||
|
path('profile/', views.profile_view, name='profile'),
|
||||||
|
]
|
||||||
103
authentication/views.py
Normal file
103
authentication/views.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.contrib.auth import login, authenticate, logout
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
def login_view(request):
|
||||||
|
"""User login view"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.POST.get('email')
|
||||||
|
password = request.POST.get('password')
|
||||||
|
|
||||||
|
user = authenticate(request, username=email, password=password)
|
||||||
|
if user is not None:
|
||||||
|
login(request, user)
|
||||||
|
return redirect('homepage')
|
||||||
|
else:
|
||||||
|
messages.error(request, 'Invalid email or password')
|
||||||
|
|
||||||
|
return render(request, 'authentication/login.html')
|
||||||
|
|
||||||
|
|
||||||
|
def register_view(request):
|
||||||
|
"""User registration view"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.POST.get('username')
|
||||||
|
email = request.POST.get('email')
|
||||||
|
password1 = request.POST.get('password1')
|
||||||
|
password2 = request.POST.get('password2')
|
||||||
|
|
||||||
|
if password1 != password2:
|
||||||
|
messages.error(request, 'Passwords do not match')
|
||||||
|
return render(request, 'authentication/register.html')
|
||||||
|
|
||||||
|
if User.objects.filter(email=email).exists():
|
||||||
|
messages.error(request, 'Email already exists')
|
||||||
|
return render(request, 'authentication/register.html')
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
password=password1
|
||||||
|
)
|
||||||
|
login(request, user)
|
||||||
|
messages.success(request, 'Account created successfully!')
|
||||||
|
return redirect('homepage')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, 'Error creating account')
|
||||||
|
|
||||||
|
return render(request, 'authentication/register.html')
|
||||||
|
|
||||||
|
|
||||||
|
def logout_view(request):
|
||||||
|
"""User logout view"""
|
||||||
|
logout(request)
|
||||||
|
messages.success(request, 'You have been logged out successfully')
|
||||||
|
return redirect('homepage')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile_view(request):
|
||||||
|
"""User profile view"""
|
||||||
|
# Get all transactions first (not sliced)
|
||||||
|
all_transactions = request.user.wallet_transactions.all()
|
||||||
|
|
||||||
|
# Get recent transactions (sliced for display)
|
||||||
|
transactions = all_transactions[:50]
|
||||||
|
|
||||||
|
# Calculate usage statistics using all transactions
|
||||||
|
total_spent = sum(abs(t.amount) for t in all_transactions if t.type == 'agent_usage')
|
||||||
|
total_topped_up = sum(t.amount for t in all_transactions if t.type == 'top_up')
|
||||||
|
total_agents_used = all_transactions.filter(type='agent_usage').count()
|
||||||
|
|
||||||
|
# Get most used agents
|
||||||
|
from django.db.models import Count
|
||||||
|
popular_agents = (all_transactions.filter(type='agent_usage')
|
||||||
|
.values('agent_slug')
|
||||||
|
.annotate(count=Count('agent_slug'))
|
||||||
|
.order_by('-count')[:5])
|
||||||
|
|
||||||
|
# Wallet status
|
||||||
|
balance = request.user.wallet_balance
|
||||||
|
if balance < 5:
|
||||||
|
wallet_status = {'status': 'low', 'color': 'red', 'message': 'Low balance - Add money to continue using agents'}
|
||||||
|
elif balance < 20:
|
||||||
|
wallet_status = {'status': 'medium', 'color': 'orange', 'message': 'Consider adding more funds'}
|
||||||
|
else:
|
||||||
|
wallet_status = {'status': 'high', 'color': 'green', 'message': 'Good balance'}
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'transactions': transactions,
|
||||||
|
'total_spent': total_spent,
|
||||||
|
'total_topped_up': total_topped_up,
|
||||||
|
'total_agents_used': total_agents_used,
|
||||||
|
'popular_agents': popular_agents,
|
||||||
|
'wallet_status': wallet_status,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'authentication/profile.html', context)
|
||||||
12
core/urls.py
Normal file
12
core/urls.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.homepage_view, name='homepage'),
|
||||||
|
path('agents/<slug:agent_slug>/', views.agent_detail_view, name='agent_detail'),
|
||||||
|
path('agents/<slug:agent_slug>/use/', views.use_agent_view, name='use_agent'),
|
||||||
|
path('wallet/', views.wallet_view, name='wallet'),
|
||||||
|
path('wallet/topup/', views.wallet_topup_view, name='wallet_topup'),
|
||||||
|
path('stripe/webhook/', views.stripe_webhook_view, name='stripe_webhook'),
|
||||||
|
path('api/agents/', views.agents_api_view, name='agents_api'),
|
||||||
|
]
|
||||||
229
core/views.py
Normal file
229
core/views.py
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.views.decorators.http import require_http_methods
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from agents.models import Agent
|
||||||
|
from agents.agent_processors import AgentProcessor
|
||||||
|
from wallet.stripe_handler import StripePaymentHandler
|
||||||
|
from wallet.models import WalletTransaction
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def homepage_view(request):
|
||||||
|
"""Homepage view showing all available agents"""
|
||||||
|
agents = Agent.objects.filter(is_active=True)
|
||||||
|
|
||||||
|
# Group agents by category
|
||||||
|
categories = {}
|
||||||
|
for agent in agents:
|
||||||
|
category = agent.get_category_display()
|
||||||
|
if category not in categories:
|
||||||
|
categories[category] = []
|
||||||
|
categories[category].append(agent)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'agents': agents,
|
||||||
|
'categories': categories,
|
||||||
|
'user_balance': request.user.wallet_balance if request.user.is_authenticated else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'core/homepage.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def agent_detail_view(request, agent_slug):
|
||||||
|
"""Individual agent detail page"""
|
||||||
|
agent = get_object_or_404(Agent, slug=agent_slug, is_active=True)
|
||||||
|
|
||||||
|
# Check if user has sufficient balance
|
||||||
|
can_use_agent = request.user.has_sufficient_balance(agent.price)
|
||||||
|
|
||||||
|
# Get recent usage by this user
|
||||||
|
recent_usage = WalletTransaction.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
agent_slug=agent_slug,
|
||||||
|
type='agent_usage'
|
||||||
|
)[:5]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'agent': agent,
|
||||||
|
'can_use_agent': can_use_agent,
|
||||||
|
'recent_usage': recent_usage,
|
||||||
|
'user_balance': request.user.wallet_balance,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'core/agent_detail.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def use_agent_view(request, agent_slug):
|
||||||
|
"""Process agent usage"""
|
||||||
|
agent = get_object_or_404(Agent, slug=agent_slug, is_active=True)
|
||||||
|
|
||||||
|
# Check balance
|
||||||
|
if not request.user.has_sufficient_balance(agent.price):
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Insufficient balance'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get input data based on agent type
|
||||||
|
if agent_slug == 'data-analyzer':
|
||||||
|
file_obj = request.FILES.get('file')
|
||||||
|
if not file_obj:
|
||||||
|
return JsonResponse({'success': False, 'error': 'File required'}, status=400)
|
||||||
|
|
||||||
|
processor = AgentProcessor(agent_slug)
|
||||||
|
result = processor.process_agent(file_obj=file_obj, user_id=request.user.id)
|
||||||
|
|
||||||
|
elif agent_slug == 'five-whys':
|
||||||
|
problem = request.POST.get('problem')
|
||||||
|
if not problem:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Problem description required'}, status=400)
|
||||||
|
|
||||||
|
processor = AgentProcessor(agent_slug)
|
||||||
|
result = processor.process_agent(problem_description=problem, user_id=request.user.id)
|
||||||
|
|
||||||
|
elif agent_slug == 'weather-reporter':
|
||||||
|
location = request.POST.get('location')
|
||||||
|
if not location:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Location required'}, status=400)
|
||||||
|
|
||||||
|
processor = AgentProcessor(agent_slug)
|
||||||
|
result = processor.process_agent(location=location)
|
||||||
|
|
||||||
|
elif agent_slug == 'job-posting-generator':
|
||||||
|
job_details = request.POST.get('job_details')
|
||||||
|
if not job_details:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Job details required'}, status=400)
|
||||||
|
|
||||||
|
processor = AgentProcessor(agent_slug)
|
||||||
|
result = processor.process_agent(job_details=job_details, user_id=request.user.id)
|
||||||
|
|
||||||
|
elif agent_slug == 'social-ads-generator':
|
||||||
|
ad_requirements = request.POST.get('ad_requirements')
|
||||||
|
if not ad_requirements:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Ad requirements required'}, status=400)
|
||||||
|
|
||||||
|
processor = AgentProcessor(agent_slug)
|
||||||
|
result = processor.process_agent(ad_requirements=ad_requirements, user_id=request.user.id)
|
||||||
|
|
||||||
|
elif agent_slug == 'faq-generator':
|
||||||
|
content_source = request.POST.get('content_source')
|
||||||
|
if not content_source:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Content source required'}, status=400)
|
||||||
|
|
||||||
|
processor = AgentProcessor(agent_slug)
|
||||||
|
result = processor.process_agent(content_source=content_source, user_id=request.user.id)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Invalid agent'}, status=400)
|
||||||
|
|
||||||
|
# Deduct balance and record transaction
|
||||||
|
request.user.deduct_balance(
|
||||||
|
amount=agent.price,
|
||||||
|
description=f"Used {agent.name}",
|
||||||
|
agent_slug=agent_slug
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'result': result,
|
||||||
|
'remaining_balance': float(request.user.wallet_balance)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def wallet_view(request):
|
||||||
|
"""Wallet management page"""
|
||||||
|
transactions = request.user.wallet_transactions.all()[:50]
|
||||||
|
|
||||||
|
# Calculate statistics
|
||||||
|
total_spent = sum(abs(t.amount) for t in transactions if t.type == 'agent_usage')
|
||||||
|
total_topped_up = sum(t.amount for t in transactions if t.type == 'top_up')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'transactions': transactions,
|
||||||
|
'total_spent': total_spent,
|
||||||
|
'total_topped_up': total_topped_up,
|
||||||
|
'current_balance': request.user.wallet_balance,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'core/wallet.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def wallet_topup_view(request):
|
||||||
|
"""Wallet top-up page"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
amount = request.POST.get('amount')
|
||||||
|
|
||||||
|
try:
|
||||||
|
amount = float(amount)
|
||||||
|
if amount not in [10, 50, 100, 500]:
|
||||||
|
messages.error(request, 'Invalid amount selected')
|
||||||
|
return redirect('wallet_topup')
|
||||||
|
|
||||||
|
# Create Stripe checkout session
|
||||||
|
stripe_handler = StripePaymentHandler()
|
||||||
|
session_data = stripe_handler.create_checkout_session(request.user, amount)
|
||||||
|
|
||||||
|
return redirect(session_data['payment_url'])
|
||||||
|
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
messages.error(request, 'Invalid amount')
|
||||||
|
return redirect('wallet_topup')
|
||||||
|
|
||||||
|
return render(request, 'core/wallet_topup.html')
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def stripe_webhook_view(request):
|
||||||
|
"""Handle Stripe webhook events"""
|
||||||
|
payload = request.body
|
||||||
|
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
|
||||||
|
|
||||||
|
stripe_handler = StripePaymentHandler()
|
||||||
|
result = stripe_handler.handle_webhook(payload, sig_header)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
return JsonResponse({'status': 'success'})
|
||||||
|
else:
|
||||||
|
return JsonResponse({'status': 'error', 'message': result['error']}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
def agents_api_view(request):
|
||||||
|
"""API endpoint for agents list"""
|
||||||
|
agents = Agent.objects.filter(is_active=True)
|
||||||
|
|
||||||
|
agents_data = []
|
||||||
|
for agent in agents:
|
||||||
|
agents_data.append({
|
||||||
|
'id': agent.id,
|
||||||
|
'name': agent.name,
|
||||||
|
'slug': agent.slug,
|
||||||
|
'description': agent.description,
|
||||||
|
'category': agent.category,
|
||||||
|
'price': float(agent.price),
|
||||||
|
'icon': agent.icon,
|
||||||
|
'rating': float(agent.rating),
|
||||||
|
'review_count': agent.review_count,
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'agents': agents_data,
|
||||||
|
'total_count': len(agents_data)
|
||||||
|
})
|
||||||
48
future agent creation
Normal file
48
future agent creation
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
● Future Agent Creation - What You Need to Tell Me
|
||||||
|
|
||||||
|
🎯 Minimum Required Information
|
||||||
|
|
||||||
|
1. Agent Basic Info
|
||||||
|
|
||||||
|
- Agent Name: (e.g., "PDF Document Processor")
|
||||||
|
- Description: (what it does)
|
||||||
|
- Icon/Emoji: (e.g., 📄)
|
||||||
|
- Cost: (credits per use)
|
||||||
|
|
||||||
|
2. Input Requirements
|
||||||
|
|
||||||
|
- What inputs does the user provide?
|
||||||
|
• File upload? (what file types?)
|
||||||
|
• Text input? (what kind?)
|
||||||
|
• Form fields? (which ones?)
|
||||||
|
• Options/settings? (what choices?)
|
||||||
|
|
||||||
|
3. Processing Method
|
||||||
|
|
||||||
|
- How should it work?
|
||||||
|
• API integration? (which service?)
|
||||||
|
• Mock/simulation? (what response?)
|
||||||
|
• External webhook? (URL/endpoint?)
|
||||||
|
|
||||||
|
💡 Example Request
|
||||||
|
|
||||||
|
"Create a PDF Document Processor agent:
|
||||||
|
- Name: PDF Document Processor
|
||||||
|
- Description: Extract text and summarize PDF documents
|
||||||
|
- Icon: 📄
|
||||||
|
- Cost: 35 credits
|
||||||
|
- Input: PDF file upload (max 10MB)
|
||||||
|
- Processing: OpenAI API for text extraction and summarization
|
||||||
|
- Output: Text summary + key points"
|
||||||
|
|
||||||
|
🚀 What I'll Handle Automatically
|
||||||
|
|
||||||
|
- ✅ Suspense wrappers
|
||||||
|
- ✅ File structure (/agent/pdf-processor/page.tsx)
|
||||||
|
- ✅ Slug mapping in agentUtils.ts
|
||||||
|
- ✅ Complete component structure
|
||||||
|
- ✅ Error handling
|
||||||
|
- ✅ Credit system integration
|
||||||
|
- ✅ UI consistency with existing agents
|
||||||
|
|
||||||
|
Just give me the basics above and I'll build the complete agent for you!
|
||||||
@ -1,18 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Django settings for netcop_hub project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.2.4.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from decouple import config
|
from decouple import config
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# Add apps directory to Python path
|
# Add apps directory to Python path
|
||||||
sys.path.insert(0, str(BASE_DIR / 'apps'))
|
sys.path.insert(0, str(BASE_DIR / 'apps'))
|
||||||
|
|
||||||
# Security
|
|
||||||
SECRET_KEY = config('SECRET_KEY', default='your-secret-key-here')
|
# Quick-start development settings - unsuitable for production
|
||||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = config('SECRET_KEY', default='django-insecure-thdd^re4==p$4geq^$52w7%egd0xxrj#fpgk1c+$xt-jrr5d7%')
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True # Force DEBUG=True for development
|
||||||
|
|
||||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')
|
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
@ -21,10 +43,10 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'core',
|
|
||||||
'authentication',
|
'authentication',
|
||||||
'agents',
|
'agents',
|
||||||
'wallet',
|
'wallet',
|
||||||
|
'core',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -39,7 +61,6 @@ MIDDLEWARE = [
|
|||||||
|
|
||||||
ROOT_URLCONF = 'netcop_hub.urls'
|
ROOT_URLCONF = 'netcop_hub.urls'
|
||||||
|
|
||||||
# Templates
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
@ -56,7 +77,12 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'netcop_hub.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
@ -64,21 +90,55 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
# Custom user model
|
# Custom user model
|
||||||
AUTH_USER_MODEL = 'authentication.User'
|
AUTH_USER_MODEL = 'authentication.User'
|
||||||
|
|
||||||
# Static files
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
# STATICFILES_DIRS removed for development - Django will use app static files
|
||||||
|
|
||||||
# Media files
|
# Media files
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
MEDIA_ROOT = BASE_DIR / 'media'
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
# Stripe
|
# Stripe
|
||||||
STRIPE_SECRET_KEY = config('STRIPE_SECRET_KEY')
|
STRIPE_SECRET_KEY = config('STRIPE_SECRET_KEY', default='')
|
||||||
STRIPE_WEBHOOK_SECRET = config('STRIPE_WEBHOOK_SECRET')
|
STRIPE_WEBHOOK_SECRET = config('STRIPE_WEBHOOK_SECRET', default='')
|
||||||
|
|
||||||
# N8N Webhooks
|
# N8N Webhooks
|
||||||
N8N_WEBHOOK_DATA_ANALYZER = config('N8N_WEBHOOK_DATA_ANALYZER', default='')
|
N8N_WEBHOOK_DATA_ANALYZER = config('N8N_WEBHOOK_DATA_ANALYZER', default='')
|
||||||
@ -93,3 +153,13 @@ OPENWEATHER_API_KEY = config('OPENWEATHER_API_KEY', default='')
|
|||||||
# Security settings
|
# Security settings
|
||||||
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default='').split(',')
|
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default='').split(',')
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# Authentication URLs
|
||||||
|
LOGIN_URL = '/auth/login/'
|
||||||
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
LOGOUT_REDIRECT_URL = '/'
|
||||||
|
|||||||
@ -1,3 +1,19 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for netcop_hub project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -7,9 +23,10 @@ urlpatterns = [
|
|||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('', include('core.urls')),
|
path('', include('core.urls')),
|
||||||
path('auth/', include('authentication.urls')),
|
path('auth/', include('authentication.urls')),
|
||||||
path('agents/', include('agents.urls')),
|
|
||||||
path('wallet/', include('wallet.urls')),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Serve static files during development
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
# Also serve media files
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
Django==4.2.7
|
|
||||||
djangorestframework==3.14.0
|
|
||||||
stripe==7.8.0
|
|
||||||
python-decouple==3.8
|
|
||||||
requests==2.31.0
|
|
||||||
Pillow==10.1.0
|
|
||||||
psycopg2-binary==2.9.9
|
|
||||||
gunicorn==21.2.0
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,485 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}{{ agent.name }} - NetCop AI Hub{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="lg:col-span-2">
|
|
||||||
<div class="bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<div class="flex items-center mb-6">
|
|
||||||
<div class="w-16 h-16 bg-gradient-to-r {{ agent.get_gradient_class }} rounded-lg flex items-center justify-center text-white text-2xl mr-4">
|
|
||||||
{{ agent.icon }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-bold text-gray-900">{{ agent.name }}</h1>
|
|
||||||
<p class="text-gray-600">{{ agent.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Agent-specific interfaces -->
|
|
||||||
{% if agent.slug == 'five-whys' %}
|
|
||||||
<!-- Advanced Chat Interface for 5 Whys -->
|
|
||||||
<div id="chatContainer" class="h-96 overflow-y-auto border border-gray-200 rounded-lg p-4 mb-4" style="background: linear-gradient(to bottom, #f9fafb, #ffffff);">
|
|
||||||
<div id="chatMessages">
|
|
||||||
<div class="mb-4 flex">
|
|
||||||
<div class="w-8 h-8 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full flex items-center justify-center text-white text-sm mr-3 flex-shrink-0">
|
|
||||||
🤖
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-100 rounded-lg p-3 max-w-md">
|
|
||||||
<div class="markdown-content">
|
|
||||||
<p>👋 Welcome to the 5 Whys Root Cause Analysis!</p>
|
|
||||||
<p>I'll help you systematically analyze your problem using the proven 5 Whys methodology.</p>
|
|
||||||
<p><strong>To get started, please describe the problem you're experiencing.</strong></p>
|
|
||||||
<p>For example:</p>
|
|
||||||
<ul>
|
|
||||||
<li>"Our customer complaints increased by 40% this month"</li>
|
|
||||||
<li>"Production quality has decreased recently"</li>
|
|
||||||
<li>"Website performance is slower than usual"</li>
|
|
||||||
</ul>
|
|
||||||
<p>What problem would you like to analyze?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<input type="text" id="chatInput" placeholder="Describe your problem..." class="flex-1 p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
||||||
<button id="sendButton" class="px-6 py-3 bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-lg hover:from-purple-700 hover:to-blue-700 transition-all duration-300">
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Report Display -->
|
|
||||||
<div id="reportSection" class="mt-6 hidden">
|
|
||||||
<h3 class="text-lg font-semibold mb-4">5 Whys Analysis Report</h3>
|
|
||||||
<div id="reportContent" class="bg-gray-50 p-4 rounded-lg"></div>
|
|
||||||
<div class="mt-4 flex gap-2">
|
|
||||||
<button onclick="copyReport()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Copy Report</button>
|
|
||||||
<button onclick="downloadReport()" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Download PDF</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% elif agent.slug == 'data-analyzer' %}
|
|
||||||
<!-- File Upload Interface -->
|
|
||||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center" id="fileDropZone">
|
|
||||||
<div class="mb-4">
|
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
|
||||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p class="text-lg text-gray-600 mb-2">Drop your file here or click to browse</p>
|
|
||||||
<p class="text-sm text-gray-500">Supports CSV, Excel, JSON files up to 10MB</p>
|
|
||||||
<input type="file" id="fileInput" accept=".csv,.xlsx,.xls,.json" class="hidden">
|
|
||||||
<button onclick="document.getElementById('fileInput').click()" class="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
|
||||||
Choose File
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="filePreview" class="mt-4 hidden">
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
|
||||||
<p class="font-medium">Selected File:</p>
|
|
||||||
<p id="fileName" class="text-gray-600"></p>
|
|
||||||
<p id="fileSize" class="text-sm text-gray-500"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% elif agent.slug == 'weather-reporter' %}
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Location</label>
|
|
||||||
<input type="text" name="location" id="locationInput" required class="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500" placeholder="Enter city name (e.g., Dubai, UAE)">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% elif agent.slug == 'job-posting-generator' %}
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Job Title</label>
|
|
||||||
<input type="text" name="title" required class="w-full p-3 border border-gray-300 rounded-md">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Company</label>
|
|
||||||
<input type="text" name="company" required class="w-full p-3 border border-gray-300 rounded-md">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Job Description</label>
|
|
||||||
<textarea name="description" rows="4" required class="w-full p-3 border border-gray-300 rounded-md"></textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Requirements</label>
|
|
||||||
<textarea name="requirements" rows="4" required class="w-full p-3 border border-gray-300 rounded-md"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% elif agent.slug == 'social-ads-generator' %}
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Product/Service</label>
|
|
||||||
<input type="text" name="product" required class="w-full p-3 border border-gray-300 rounded-md">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Platform</label>
|
|
||||||
<select name="platform" required class="w-full p-3 border border-gray-300 rounded-md">
|
|
||||||
<option value="facebook">Facebook</option>
|
|
||||||
<option value="instagram">Instagram</option>
|
|
||||||
<option value="twitter">Twitter</option>
|
|
||||||
<option value="linkedin">LinkedIn</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Target Audience</label>
|
|
||||||
<input type="text" name="target_audience" required class="w-full p-3 border border-gray-300 rounded-md">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Tone</label>
|
|
||||||
<select name="tone" required class="w-full p-3 border border-gray-300 rounded-md">
|
|
||||||
<option value="professional">Professional</option>
|
|
||||||
<option value="casual">Casual</option>
|
|
||||||
<option value="humorous">Humorous</option>
|
|
||||||
<option value="urgent">Urgent</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% elif agent.slug == 'faq-generator' %}
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Content Source</label>
|
|
||||||
<textarea name="content_source" rows="6" required class="w-full p-3 border border-gray-300 rounded-md" placeholder="Paste your content or URL here..."></textarea>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Processing Status -->
|
|
||||||
<div id="processingStatus" class="mt-6 hidden">
|
|
||||||
<div class="flex items-center justify-center p-6">
|
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mr-3"></div>
|
|
||||||
<span class="text-gray-600">Processing your request...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results Display -->
|
|
||||||
<div id="results" class="mt-6 hidden">
|
|
||||||
<h3 class="text-lg font-semibold mb-4">Results</h3>
|
|
||||||
<div id="resultsContent" class="bg-gray-50 p-4 rounded-md">
|
|
||||||
<!-- Results will be inserted here -->
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 flex gap-2">
|
|
||||||
<button onclick="copyResults()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Copy Results</button>
|
|
||||||
<button onclick="downloadResults()" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Download</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div class="lg:col-span-1">
|
|
||||||
<!-- Wallet Balance Component -->
|
|
||||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6">
|
|
||||||
<h3 class="text-lg font-semibold mb-4">Cost</h3>
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<span class="text-gray-600">Current Balance:</span>
|
|
||||||
<span class="font-semibold">{{ user.wallet_balance }} AED</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<span class="text-gray-600">Cost:</span>
|
|
||||||
<span class="font-semibold text-red-600">-{{ agent.price_display }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="border-t pt-2">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="font-semibold">After Processing:</span>
|
|
||||||
<span class="font-semibold {% if has_sufficient_balance %}text-green-600{% else %}text-red-600{% endif %}">
|
|
||||||
{% if has_sufficient_balance %}
|
|
||||||
{{ user.wallet_balance|floatformat:2 }} AED
|
|
||||||
{% else %}
|
|
||||||
Insufficient Balance
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="processBtn" type="button"
|
|
||||||
class="w-full py-3 px-4 rounded-md font-semibold transition-colors
|
|
||||||
{% if has_sufficient_balance %}
|
|
||||||
bg-blue-600 text-white hover:bg-blue-700
|
|
||||||
{% else %}
|
|
||||||
bg-gray-300 text-gray-500 cursor-not-allowed
|
|
||||||
{% endif %}"
|
|
||||||
{% if not has_sufficient_balance %}disabled{% endif %}>
|
|
||||||
{% if has_sufficient_balance %}
|
|
||||||
Process Agent ({{ agent.price_display }})
|
|
||||||
{% else %}
|
|
||||||
Insufficient Balance
|
|
||||||
{% endif %}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{% if not has_sufficient_balance %}
|
|
||||||
<p class="text-center mt-4 text-sm">
|
|
||||||
<a href="{% url 'pricing' %}" class="text-blue-600 hover:text-blue-800 underline">
|
|
||||||
Top up wallet
|
|
||||||
</a> to use this agent
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Agent Stats -->
|
|
||||||
<div class="bg-white rounded-lg shadow-lg p-6">
|
|
||||||
<h3 class="text-lg font-semibold mb-4">Agent Statistics</h3>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-gray-600">Rating:</span>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-yellow-400">★</span>
|
|
||||||
<span class="ml-1">{{ agent.rating }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-gray-600">Reviews:</span>
|
|
||||||
<span>{{ agent.review_count }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-gray-600">Category:</span>
|
|
||||||
<span class="capitalize">{{ agent.get_category_display }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Agent processing logic
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const agentSlug = '{{ agent.slug }}';
|
|
||||||
const processBtn = document.getElementById('processBtn');
|
|
||||||
const processingStatus = document.getElementById('processingStatus');
|
|
||||||
const results = document.getElementById('results');
|
|
||||||
const resultsContent = document.getElementById('resultsContent');
|
|
||||||
|
|
||||||
// File upload for data analyzer
|
|
||||||
if (agentSlug === 'data-analyzer') {
|
|
||||||
const fileInput = document.getElementById('fileInput');
|
|
||||||
const fileDropZone = document.getElementById('fileDropZone');
|
|
||||||
const filePreview = document.getElementById('filePreview');
|
|
||||||
const fileName = document.getElementById('fileName');
|
|
||||||
const fileSize = document.getElementById('fileSize');
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', function(e) {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
fileName.textContent = file.name;
|
|
||||||
fileSize.textContent = `${(file.size / 1024 / 1024).toFixed(2)} MB`;
|
|
||||||
filePreview.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drag and drop functionality
|
|
||||||
fileDropZone.addEventListener('dragover', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
fileDropZone.classList.add('border-blue-400', 'bg-blue-50');
|
|
||||||
});
|
|
||||||
|
|
||||||
fileDropZone.addEventListener('dragleave', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
fileDropZone.classList.remove('border-blue-400', 'bg-blue-50');
|
|
||||||
});
|
|
||||||
|
|
||||||
fileDropZone.addEventListener('drop', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
fileDropZone.classList.remove('border-blue-400', 'bg-blue-50');
|
|
||||||
const files = e.dataTransfer.files;
|
|
||||||
if (files.length > 0) {
|
|
||||||
fileInput.files = files;
|
|
||||||
fileInput.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chat functionality for 5 Whys
|
|
||||||
if (agentSlug === 'five-whys') {
|
|
||||||
const chatInput = document.getElementById('chatInput');
|
|
||||||
const sendButton = document.getElementById('sendButton');
|
|
||||||
const chatMessages = document.getElementById('chatMessages');
|
|
||||||
const chatContainer = document.getElementById('chatContainer');
|
|
||||||
|
|
||||||
function addMessage(content, sender = 'user') {
|
|
||||||
const messageDiv = document.createElement('div');
|
|
||||||
messageDiv.className = 'mb-4 flex';
|
|
||||||
|
|
||||||
if (sender === 'user') {
|
|
||||||
messageDiv.innerHTML = `
|
|
||||||
<div class="ml-auto flex">
|
|
||||||
<div class="bg-blue-600 text-white rounded-lg p-3 max-w-md">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white text-sm ml-3 flex-shrink-0">
|
|
||||||
👤
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
messageDiv.innerHTML = `
|
|
||||||
<div class="w-8 h-8 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full flex items-center justify-center text-white text-sm mr-3 flex-shrink-0">
|
|
||||||
🤖
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-100 rounded-lg p-3 max-w-md">
|
|
||||||
<div class="markdown-content">${content}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
chatMessages.appendChild(messageDiv);
|
|
||||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMessage() {
|
|
||||||
const message = chatInput.value.trim();
|
|
||||||
if (!message) return;
|
|
||||||
|
|
||||||
addMessage(message, 'user');
|
|
||||||
chatInput.value = '';
|
|
||||||
|
|
||||||
// Here you would send the message to your Django backend
|
|
||||||
// For now, we'll simulate a response
|
|
||||||
setTimeout(() => {
|
|
||||||
addMessage('Thank you for sharing that problem. Let me ask you the first "Why" question...', 'bot');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendButton.addEventListener('click', sendMessage);
|
|
||||||
chatInput.addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
sendMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main process button functionality
|
|
||||||
processBtn.addEventListener('click', async function() {
|
|
||||||
if (processBtn.disabled) return;
|
|
||||||
|
|
||||||
processBtn.disabled = true;
|
|
||||||
processBtn.textContent = 'Processing...';
|
|
||||||
processingStatus.classList.remove('hidden');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
// Collect form data based on agent type
|
|
||||||
if (agentSlug === 'data-analyzer') {
|
|
||||||
const fileInput = document.getElementById('fileInput');
|
|
||||||
if (fileInput.files[0]) {
|
|
||||||
formData.append('file', fileInput.files[0]);
|
|
||||||
} else {
|
|
||||||
throw new Error('Please select a file');
|
|
||||||
}
|
|
||||||
} else if (agentSlug === 'weather-reporter') {
|
|
||||||
const location = document.getElementById('locationInput').value;
|
|
||||||
if (!location) throw new Error('Please enter a location');
|
|
||||||
formData.append('location', location);
|
|
||||||
} else {
|
|
||||||
// For other agents, collect all form inputs
|
|
||||||
const inputs = document.querySelectorAll('input[name], textarea[name], select[name]');
|
|
||||||
inputs.forEach(input => {
|
|
||||||
if (input.value) {
|
|
||||||
formData.append(input.name, input.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`{% url 'process_agent' agent.slug %}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
// Display results
|
|
||||||
resultsContent.innerHTML = `<pre>${JSON.stringify(data.result, null, 2)}</pre>`;
|
|
||||||
results.classList.remove('hidden');
|
|
||||||
|
|
||||||
// Update balance display (you might want to refresh the page or update via AJAX)
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert('Error: ' + data.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
alert('Error: ' + error.message);
|
|
||||||
} finally {
|
|
||||||
processBtn.disabled = false;
|
|
||||||
processBtn.textContent = 'Process Agent ({{ agent.price_display }})';
|
|
||||||
processingStatus.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Utility functions
|
|
||||||
function copyResults() {
|
|
||||||
const content = document.getElementById('resultsContent').textContent;
|
|
||||||
navigator.clipboard.writeText(content).then(() => {
|
|
||||||
alert('Results copied to clipboard!');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadResults() {
|
|
||||||
const content = document.getElementById('resultsContent').textContent;
|
|
||||||
const blob = new Blob([content], { type: 'text/plain' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = '{{ agent.slug }}_results.txt';
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyReport() {
|
|
||||||
const content = document.getElementById('reportContent').textContent;
|
|
||||||
navigator.clipboard.writeText(content).then(() => {
|
|
||||||
alert('Report copied to clipboard!');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadReport() {
|
|
||||||
const content = document.getElementById('reportContent').textContent;
|
|
||||||
const blob = new Blob([content], { type: 'text/plain' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'five_whys_analysis_report.txt';
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.markdown-content ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
margin-left: 20px;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content li {
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content p {
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content strong {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-spin {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
60
templates/authentication/login.html
Normal file
60
templates/authentication/login.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - NetCop Hub</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 400px; margin: 50px auto; }
|
||||||
|
.login-form { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.form-group { margin-bottom: 20px; }
|
||||||
|
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||||
|
.form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
|
||||||
|
.btn { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; }
|
||||||
|
.btn:hover { background: #0056b3; }
|
||||||
|
.messages { margin-bottom: 20px; }
|
||||||
|
.message { padding: 10px; border-radius: 4px; margin-bottom: 10px; }
|
||||||
|
.message.error { background: #ffe6e6; color: #cc0000; }
|
||||||
|
.message.success { background: #e6ffe6; color: #006600; }
|
||||||
|
.auth-links { text-align: center; margin-top: 20px; }
|
||||||
|
.auth-links a { color: #007bff; text-decoration: none; }
|
||||||
|
.auth-links a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="login-form">
|
||||||
|
<h2>Login to NetCop Hub</h2>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="messages">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="message {{ message.tags }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email:</label>
|
||||||
|
<input type="email" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<p>Don't have an account? <a href="{% url 'register' %}">Register here</a></p>
|
||||||
|
<p><a href="{% url 'homepage' %}">Back to Homepage</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
97
templates/authentication/profile.html
Normal file
97
templates/authentication/profile.html
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Profile - NetCop Hub</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; }
|
||||||
|
.profile-header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.wallet-status { padding: 15px; border-radius: 8px; margin: 20px 0; }
|
||||||
|
.wallet-status.high { background: #d4edda; color: #155724; }
|
||||||
|
.wallet-status.medium { background: #fff3cd; color: #856404; }
|
||||||
|
.wallet-status.low { background: #f8d7da; color: #721c24; }
|
||||||
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px; }
|
||||||
|
.stat-card { background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.stat-value { font-size: 1.5em; font-weight: bold; color: #007bff; }
|
||||||
|
.popular-agents { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
||||||
|
.agent-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
|
||||||
|
.agent-item:last-child { border-bottom: none; }
|
||||||
|
.transactions { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.transaction-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
|
||||||
|
.transaction-item:last-child { border-bottom: none; }
|
||||||
|
.transaction-amount { font-weight: bold; }
|
||||||
|
.transaction-amount.positive { color: #28a745; }
|
||||||
|
.transaction-amount.negative { color: #dc3545; }
|
||||||
|
.btn { padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; display: inline-block; margin-right: 10px; }
|
||||||
|
.btn:hover { background: #0056b3; }
|
||||||
|
.back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
|
||||||
|
.back-link:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="{% url 'homepage' %}" class="back-link">← Back to Homepage</a>
|
||||||
|
|
||||||
|
<div class="profile-header">
|
||||||
|
<h1>Profile - {{ user.username }}</h1>
|
||||||
|
<p><strong>Email:</strong> {{ user.email }}</p>
|
||||||
|
<p><strong>Balance:</strong> ${{ user.wallet_balance }}</p>
|
||||||
|
|
||||||
|
<div class="wallet-status {{ wallet_status.status }}">
|
||||||
|
<strong>{{ wallet_status.message }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'wallet' %}" class="btn">Manage Wallet</a>
|
||||||
|
<a href="{% url 'wallet_topup' %}" class="btn">Top Up</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">${{ total_spent }}</div>
|
||||||
|
<div>Total Spent</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">${{ total_topped_up }}</div>
|
||||||
|
<div>Total Topped Up</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">{{ total_agents_used }}</div>
|
||||||
|
<div>Agents Used</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if popular_agents %}
|
||||||
|
<div class="popular-agents">
|
||||||
|
<h2>Your Most Used Agents</h2>
|
||||||
|
{% for agent in popular_agents %}
|
||||||
|
<div class="agent-item">
|
||||||
|
<span>{{ agent.agent_slug }}</span>
|
||||||
|
<span><strong>{{ agent.count }}</strong> times</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="transactions">
|
||||||
|
<h2>Recent Transactions</h2>
|
||||||
|
{% if transactions %}
|
||||||
|
{% for transaction in transactions %}
|
||||||
|
<div class="transaction-item">
|
||||||
|
<div>
|
||||||
|
<strong>{{ transaction.description }}</strong>
|
||||||
|
<small>{{ transaction.created_at|date:"M d, Y H:i" }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="transaction-amount {% if transaction.type == 'top_up' %}positive{% else %}negative{% endif %}">
|
||||||
|
{% if transaction.type == 'top_up' %}+{% else %}-{% endif %}${{ transaction.amount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>No transactions yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
70
templates/authentication/register.html
Normal file
70
templates/authentication/register.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Register - NetCop Hub</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 400px; margin: 50px auto; }
|
||||||
|
.register-form { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.form-group { margin-bottom: 20px; }
|
||||||
|
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||||
|
.form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
|
||||||
|
.btn { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; }
|
||||||
|
.btn:hover { background: #0056b3; }
|
||||||
|
.messages { margin-bottom: 20px; }
|
||||||
|
.message { padding: 10px; border-radius: 4px; margin-bottom: 10px; }
|
||||||
|
.message.error { background: #ffe6e6; color: #cc0000; }
|
||||||
|
.message.success { background: #e6ffe6; color: #006600; }
|
||||||
|
.auth-links { text-align: center; margin-top: 20px; }
|
||||||
|
.auth-links a { color: #007bff; text-decoration: none; }
|
||||||
|
.auth-links a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="register-form">
|
||||||
|
<h2>Register for NetCop Hub</h2>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="messages">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="message {{ message.tags }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email:</label>
|
||||||
|
<input type="email" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password1">Password:</label>
|
||||||
|
<input type="password" id="password1" name="password1" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password2">Confirm Password:</label>
|
||||||
|
<input type="password" id="password2" name="password2" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn">Register</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<p>Already have an account? <a href="{% url 'login' %}">Login here</a></p>
|
||||||
|
<p><a href="{% url 'homepage' %}">Back to Homepage</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,74 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!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 Hub{% endblock %}</title>
|
|
||||||
<link rel="icon" type="image/png" href="{% static 'favicon.png' %}">
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
body { font-family: 'Inter', sans-serif; }
|
|
||||||
.glass { backdrop-filter: blur(10px); background: rgba(255, 255, 255, 0.1); }
|
|
||||||
.gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50">
|
|
||||||
<!-- Navigation -->
|
|
||||||
<nav class="bg-white shadow-sm border-b">
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="flex justify-between items-center h-16">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<a href="{% url 'marketplace' %}" class="text-xl font-bold text-gray-900">
|
|
||||||
NetCop AI Hub
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
{% if user.is_authenticated %}
|
|
||||||
<div class="glass px-3 py-1 rounded-full">
|
|
||||||
<span class="text-sm font-medium">💰 {{ user.wallet_balance }} AED</span>
|
|
||||||
</div>
|
|
||||||
<a href="{% url 'pricing' %}" class="text-blue-600 hover:text-blue-800">Top Up</a>
|
|
||||||
<a href="{% url 'profile' %}" class="text-gray-600 hover:text-gray-800">Profile</a>
|
|
||||||
<a href="{% url 'logout' %}" class="text-red-600 hover:text-red-800">Logout</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'login' %}" class="text-blue-600 hover:text-blue-800">Login</a>
|
|
||||||
<a href="{% url 'register' %}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">Sign Up</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Messages -->
|
|
||||||
{% if messages %}
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
|
||||||
{% for message in messages %}
|
|
||||||
{% if message.tags == 'error' %}
|
|
||||||
<div class="alert alert-error bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-success bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
||||||
{% endif %}
|
|
||||||
{{ message }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="bg-gray-800 text-white mt-20">
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
<div class="text-center">
|
|
||||||
<p>© 2024 NetCop AI Hub. All rights reserved.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
118
templates/core/agent_detail.html
Normal file
118
templates/core/agent_detail.html
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ agent.name }} - NetCop Hub</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; }
|
||||||
|
.agent-detail { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.agent-header { display: flex; align-items: center; margin-bottom: 20px; }
|
||||||
|
.agent-icon { font-size: 3em; margin-right: 20px; }
|
||||||
|
.agent-info h1 { margin: 0; }
|
||||||
|
.agent-price { color: #007bff; font-size: 1.5em; font-weight: bold; margin: 10px 0; }
|
||||||
|
.agent-rating { color: #ffa500; font-size: 1.1em; }
|
||||||
|
.user-balance { background: #e8f5e8; padding: 10px; border-radius: 5px; margin: 20px 0; }
|
||||||
|
.insufficient-balance { background: #ffe6e6; padding: 10px; border-radius: 5px; margin: 20px 0; color: #cc0000; }
|
||||||
|
.agent-form { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||||
|
.form-group { margin-bottom: 15px; }
|
||||||
|
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||||
|
.form-group input, .form-group textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
|
||||||
|
.form-group textarea { height: 100px; resize: vertical; }
|
||||||
|
.btn { padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; }
|
||||||
|
.btn:hover { background: #0056b3; }
|
||||||
|
.btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||||
|
.back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
|
||||||
|
.back-link:hover { text-decoration: underline; }
|
||||||
|
.recent-usage { margin-top: 30px; }
|
||||||
|
.usage-item { background: #f8f9fa; padding: 10px; margin: 5px 0; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="{% url 'homepage' %}" class="back-link">← Back to Homepage</a>
|
||||||
|
|
||||||
|
<div class="agent-detail">
|
||||||
|
<div class="agent-header">
|
||||||
|
<div class="agent-icon">{{ agent.icon }}</div>
|
||||||
|
<div class="agent-info">
|
||||||
|
<h1>{{ agent.name }}</h1>
|
||||||
|
<div class="agent-price">${{ agent.price }}</div>
|
||||||
|
<div class="agent-rating">★ {{ agent.rating }} ({{ agent.review_count }} reviews)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>{{ agent.description }}</p>
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="user-balance">
|
||||||
|
Your Balance: ${{ user_balance }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if can_use_agent %}
|
||||||
|
<div class="agent-form">
|
||||||
|
<h3>Use {{ agent.name }}</h3>
|
||||||
|
<form method="post" action="{% url 'use_agent' agent.slug %}" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if agent.slug == 'data-analyzer' %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="file">Upload Data File:</label>
|
||||||
|
<input type="file" id="file" name="file" accept=".csv,.xlsx,.json" required>
|
||||||
|
</div>
|
||||||
|
{% elif agent.slug == 'five-whys' %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="problem">Problem Description:</label>
|
||||||
|
<textarea id="problem" name="problem" placeholder="Describe the problem you want to analyze..." required></textarea>
|
||||||
|
</div>
|
||||||
|
{% elif agent.slug == 'weather-reporter' %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location">Location:</label>
|
||||||
|
<input type="text" id="location" name="location" placeholder="Enter city name or coordinates" required>
|
||||||
|
</div>
|
||||||
|
{% elif agent.slug == 'job-posting-generator' %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="job_details">Job Details:</label>
|
||||||
|
<textarea id="job_details" name="job_details" placeholder="Enter job title, requirements, company info..." required></textarea>
|
||||||
|
</div>
|
||||||
|
{% elif agent.slug == 'social-ads-generator' %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ad_requirements">Ad Requirements:</label>
|
||||||
|
<textarea id="ad_requirements" name="ad_requirements" placeholder="Describe your product/service, target audience, goals..." required></textarea>
|
||||||
|
</div>
|
||||||
|
{% elif agent.slug == 'faq-generator' %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="content_source">Content Source:</label>
|
||||||
|
<textarea id="content_source" name="content_source" placeholder="Paste your content, documentation, or product information..." required></textarea>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button type="submit" class="btn">Use Agent (${{ agent.price }})</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="insufficient-balance">
|
||||||
|
<strong>Insufficient Balance!</strong> You need ${{ agent.price }} to use this agent.
|
||||||
|
<a href="{% url 'wallet_topup' %}" style="color: #007bff;">Top up your wallet</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if recent_usage %}
|
||||||
|
<div class="recent-usage">
|
||||||
|
<h3>Your Recent Usage</h3>
|
||||||
|
{% for usage in recent_usage %}
|
||||||
|
<div class="usage-item">
|
||||||
|
<strong>${{ usage.amount }}</strong> - {{ usage.description }}
|
||||||
|
<small>({{ usage.created_at|date:"M d, Y H:i" }})</small>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p><a href="{% url 'login' %}">Login</a> to use this agent.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
85
templates/core/homepage.html
Normal file
85
templates/core/homepage.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NetCop Hub - AI Agent Marketplace</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
.header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.user-info { float: right; }
|
||||||
|
.balance { background: #e8f5e8; padding: 5px 15px; border-radius: 20px; color: #2d5a2d; }
|
||||||
|
.agents-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
|
||||||
|
.agent-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.agent-header { display: flex; align-items: center; margin-bottom: 15px; }
|
||||||
|
.agent-icon { font-size: 2em; margin-right: 15px; }
|
||||||
|
.agent-name { font-size: 1.3em; font-weight: bold; margin: 0; }
|
||||||
|
.agent-price { color: #007bff; font-weight: bold; }
|
||||||
|
.agent-rating { color: #ffa500; }
|
||||||
|
.category-section { margin-bottom: 30px; }
|
||||||
|
.category-title { font-size: 1.5em; margin-bottom: 15px; padding: 10px; background: #007bff; color: white; border-radius: 5px; }
|
||||||
|
.btn { padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; display: inline-block; margin-top: 10px; }
|
||||||
|
.btn:hover { background: #0056b3; }
|
||||||
|
.auth-links { margin-top: 10px; }
|
||||||
|
.auth-links a { margin-right: 15px; text-decoration: none; color: #007bff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>NetCop Hub - AI Agent Marketplace</h1>
|
||||||
|
<div class="user-info">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<p>Welcome, {{ user.username }}!</p>
|
||||||
|
<div class="balance">Balance: ${{ user_balance }}</div>
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{% url 'profile' %}">Profile</a>
|
||||||
|
<a href="{% url 'wallet' %}">Wallet</a>
|
||||||
|
<a href="{% url 'logout' %}">Logout</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{% url 'login' %}">Login</a>
|
||||||
|
<a href="{% url 'register' %}">Register</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if categories %}
|
||||||
|
{% for category_name, category_agents in categories.items %}
|
||||||
|
<div class="category-section">
|
||||||
|
<div class="category-title">{{ category_name }}</div>
|
||||||
|
<div class="agents-grid">
|
||||||
|
{% for agent in category_agents %}
|
||||||
|
<div class="agent-card">
|
||||||
|
<div class="agent-header">
|
||||||
|
<div class="agent-icon">{{ agent.icon }}</div>
|
||||||
|
<div>
|
||||||
|
<div class="agent-name">{{ agent.name }}</div>
|
||||||
|
<div class="agent-price">${{ agent.price }}</div>
|
||||||
|
<div class="agent-rating">
|
||||||
|
★ {{ agent.rating }} ({{ agent.review_count }} reviews)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>{{ agent.description }}</p>
|
||||||
|
<a href="{% url 'agent_detail' agent.slug %}" class="btn">Use Agent</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="agents-grid">
|
||||||
|
<div class="agent-card">
|
||||||
|
<h3>No agents available</h3>
|
||||||
|
<p>Please check back later or contact the administrator.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
68
templates/core/wallet.html
Normal file
68
templates/core/wallet.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Wallet - NetCop Hub</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; }
|
||||||
|
.wallet-header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.balance { font-size: 2em; color: #007bff; font-weight: bold; }
|
||||||
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px; }
|
||||||
|
.stat-card { background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.stat-value { font-size: 1.5em; font-weight: bold; color: #007bff; }
|
||||||
|
.transactions { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.transaction-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
|
||||||
|
.transaction-item:last-child { border-bottom: none; }
|
||||||
|
.transaction-amount { font-weight: bold; }
|
||||||
|
.transaction-amount.positive { color: #28a745; }
|
||||||
|
.transaction-amount.negative { color: #dc3545; }
|
||||||
|
.btn { padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; display: inline-block; margin-bottom: 20px; }
|
||||||
|
.btn:hover { background: #0056b3; }
|
||||||
|
.back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
|
||||||
|
.back-link:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="{% url 'homepage' %}" class="back-link">← Back to Homepage</a>
|
||||||
|
|
||||||
|
<div class="wallet-header">
|
||||||
|
<h1>Your Wallet</h1>
|
||||||
|
<div class="balance">${{ current_balance }}</div>
|
||||||
|
<a href="{% url 'wallet_topup' %}" class="btn">Top Up Wallet</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">${{ total_spent }}</div>
|
||||||
|
<div>Total Spent</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">${{ total_topped_up }}</div>
|
||||||
|
<div>Total Topped Up</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="transactions">
|
||||||
|
<h2>Recent Transactions</h2>
|
||||||
|
{% if transactions %}
|
||||||
|
{% for transaction in transactions %}
|
||||||
|
<div class="transaction-item">
|
||||||
|
<div>
|
||||||
|
<strong>{{ transaction.description }}</strong>
|
||||||
|
<small>{{ transaction.created_at|date:"M d, Y H:i" }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="transaction-amount {% if transaction.type == 'top_up' %}positive{% else %}negative{% endif %}">
|
||||||
|
{% if transaction.type == 'top_up' %}+{% else %}-{% endif %}${{ transaction.amount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>No transactions yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
97
templates/core/wallet_topup.html
Normal file
97
templates/core/wallet_topup.html
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Top Up Wallet - NetCop Hub</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; }
|
||||||
|
.topup-form { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.amount-options { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; margin: 20px 0; }
|
||||||
|
.amount-option { padding: 20px; border: 2px solid #ddd; border-radius: 8px; text-align: center; cursor: pointer; transition: all 0.3s; }
|
||||||
|
.amount-option:hover { border-color: #007bff; background: #f8f9fa; }
|
||||||
|
.amount-option.selected { border-color: #007bff; background: #e7f3ff; }
|
||||||
|
.amount-value { font-size: 1.5em; font-weight: bold; color: #007bff; }
|
||||||
|
.btn { width: 100%; padding: 15px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 1.1em; margin-top: 20px; }
|
||||||
|
.btn:hover { background: #0056b3; }
|
||||||
|
.btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||||
|
.back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
|
||||||
|
.back-link:hover { text-decoration: underline; }
|
||||||
|
.messages { margin-bottom: 20px; }
|
||||||
|
.message { padding: 10px; border-radius: 4px; margin-bottom: 10px; }
|
||||||
|
.message.error { background: #ffe6e6; color: #cc0000; }
|
||||||
|
.message.success { background: #e6ffe6; color: #006600; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="{% url 'wallet' %}" class="back-link">← Back to Wallet</a>
|
||||||
|
|
||||||
|
<div class="topup-form">
|
||||||
|
<h1>Top Up Your Wallet</h1>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="messages">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="message {{ message.tags }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" id="topup-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>Select an amount to add to your wallet:</p>
|
||||||
|
|
||||||
|
<div class="amount-options">
|
||||||
|
<div class="amount-option" data-amount="10">
|
||||||
|
<div class="amount-value">$10</div>
|
||||||
|
<div>Basic</div>
|
||||||
|
</div>
|
||||||
|
<div class="amount-option" data-amount="50">
|
||||||
|
<div class="amount-value">$50</div>
|
||||||
|
<div>Popular</div>
|
||||||
|
</div>
|
||||||
|
<div class="amount-option" data-amount="100">
|
||||||
|
<div class="amount-value">$100</div>
|
||||||
|
<div>Best Value</div>
|
||||||
|
</div>
|
||||||
|
<div class="amount-option" data-amount="500">
|
||||||
|
<div class="amount-value">$500</div>
|
||||||
|
<div>Premium</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="amount" id="selected-amount" value="">
|
||||||
|
<button type="submit" class="btn" id="topup-btn" disabled>Select an amount</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const amountOptions = document.querySelectorAll('.amount-option');
|
||||||
|
const selectedAmountInput = document.getElementById('selected-amount');
|
||||||
|
const topupBtn = document.getElementById('topup-btn');
|
||||||
|
|
||||||
|
amountOptions.forEach(option => {
|
||||||
|
option.addEventListener('click', function() {
|
||||||
|
// Remove selected class from all options
|
||||||
|
amountOptions.forEach(opt => opt.classList.remove('selected'));
|
||||||
|
|
||||||
|
// Add selected class to clicked option
|
||||||
|
this.classList.add('selected');
|
||||||
|
|
||||||
|
// Set the selected amount
|
||||||
|
const amount = this.getAttribute('data-amount');
|
||||||
|
selectedAmountInput.value = amount;
|
||||||
|
|
||||||
|
// Enable submit button
|
||||||
|
topupBtn.disabled = false;
|
||||||
|
topupBtn.textContent = `Top Up $${amount}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,36 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Debug - Environment Status{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div style="padding: 20px; font-family: monospace; max-width: 800px; margin: 0 auto;">
|
|
||||||
{% if not debug_mode %}
|
|
||||||
<h1>🚫 Debug page disabled in production</h1>
|
|
||||||
<p>This debug page is only available in development mode.</p>
|
|
||||||
{% else %}
|
|
||||||
<h1>🔧 Environment Debug Page</h1>
|
|
||||||
|
|
||||||
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
|
||||||
<h2>Environment Variables Status:</h2>
|
|
||||||
<pre>{{ env_status|safe }}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: #e8f4f8; padding: 15px; border-radius: 8px;">
|
|
||||||
<h3>💡 Troubleshooting Tips:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Make sure .env file exists in your project root</li>
|
|
||||||
<li>Restart your Django server after changing environment variables</li>
|
|
||||||
<li>In production, set environment variables in your hosting platform dashboard</li>
|
|
||||||
<li>Check that sensitive variables are properly configured</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; margin-top: 20px;">
|
|
||||||
<h3>🔍 Database Status:</h3>
|
|
||||||
<p>Database Connection: <strong style="color: {{ db_status.color }};">{{ db_status.status }}</strong></p>
|
|
||||||
<p>User Count: <strong>{{ user_count }}</strong></p>
|
|
||||||
<p>Agent Count: <strong>{{ agent_count }}</strong></p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,45 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}AI Agent Marketplace - NetCop AI Hub{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="text-center mb-12">
|
|
||||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">AI Agent Marketplace</h1>
|
|
||||||
<p class="text-xl text-gray-600">Choose from our collection of powerful AI agents</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{% for agent in agents %}
|
|
||||||
<div class="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300">
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<div class="w-12 h-12 bg-gradient-to-r {{ agent.get_gradient_class }} rounded-lg flex items-center justify-center text-white font-bold">
|
|
||||||
{{ agent.icon }}
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<div class="text-lg font-bold text-gray-900">{{ agent.price_display }}</div>
|
|
||||||
<div class="text-sm text-gray-500">per use</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{ agent.name }}</h3>
|
|
||||||
<p class="text-gray-600 mb-4">{{ agent.description }}</p>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-yellow-400">★</span>
|
|
||||||
<span class="ml-1 text-sm text-gray-600">{{ agent.rating }} ({{ agent.review_count }})</span>
|
|
||||||
</div>
|
|
||||||
<span class="bg-{{ agent.category == 'analytics' and 'blue' or 'green' }}-100 text-{{ agent.category == 'analytics' and 'blue' or 'green' }}-800 px-2 py-1 rounded-full text-xs">
|
|
||||||
{{ agent.get_category_display }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="{% url 'agent_detail' agent.slug %}" class="block w-full bg-blue-600 text-white text-center py-2 rounded-md hover:bg-blue-700 transition-colors">
|
|
||||||
Use Agent
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Reset Password - NetCop AI Hub{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div style="min-height: 100vh; background: linear-gradient(135deg, #f6f8ff 0%, #e8f0fe 50%, #f0f7ff 100%); display: flex; align-items: center; justify-content: center; padding: 20px;">
|
|
||||||
|
|
||||||
<div style="background: white; border-radius: 20px; padding: 40px; width: 100%; max-width: 400px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);">
|
|
||||||
<div style="text-align: center; margin-bottom: 32px;">
|
|
||||||
<h1 style="font-size: 28px; font-weight: bold; color: #1f2937; margin-bottom: 8px;">
|
|
||||||
Reset Your Password
|
|
||||||
</h1>
|
|
||||||
<p style="color: #6b7280;">
|
|
||||||
Enter your new password below
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<div style="background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 16px; border-radius: 12px; text-align: center; margin-bottom: 20px;">
|
|
||||||
<div style="font-size: 48px; margin-bottom: 16px;">❌</div>
|
|
||||||
<h3 style="font-weight: bold; margin-bottom: 8px;">Error</h3>
|
|
||||||
<p style="margin-bottom: 16px;">{{ error }}</p>
|
|
||||||
<a href="{% url 'homepage' %}" style="background: #dc2626; color: white; padding: 8px 16px; border-radius: 8px; text-decoration: none; font-size: 14px;">
|
|
||||||
Go to Homepage
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<form method="post" style="display: flex; flex-direction: column; gap: 20px;">
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">
|
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
<input type="password" name="password" required style="width: 100%; padding: 12px 16px; border: 2px solid #e5e7eb; border-radius: 12px; font-size: 16px; transition: border-color 0.2s;" placeholder="Enter new password">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">
|
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<input type="password" name="confirm_password" required style="width: 100%; padding: 12px 16px; border: 2px solid #e5e7eb; border-radius: 12px; font-size: 16px; transition: border-color 0.2s;" placeholder="Confirm new password">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" style="background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: white; padding: 14px 24px; border-radius: 12px; font-size: 16px; font-weight: 600; border: none; cursor: pointer; transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.02)'" onmouseout="this.style.transform='scale(1)'">
|
|
||||||
Update Password
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 24px; padding-top: 24px; border-top: 1px solid #e5e7eb;">
|
|
||||||
<a href="{% url 'homepage' %}" style="background: none; border: none; color: #3b82f6; font-weight: 500; cursor: pointer; text-decoration: underline;">
|
|
||||||
Back to Homepage
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
34
test_views.py
Normal file
34
test_views.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# Setup Django
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_hub.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.test import RequestFactory
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from core.views import homepage_view
|
||||||
|
|
||||||
|
# Create a test request
|
||||||
|
factory = RequestFactory()
|
||||||
|
request = factory.get('/')
|
||||||
|
|
||||||
|
# Create a mock user
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test homepage view
|
||||||
|
response = homepage_view(request)
|
||||||
|
print(f"Homepage view status: {response.status_code}")
|
||||||
|
print("Homepage view working correctly!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in homepage view: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
0
wallet/__init__.py
Normal file
0
wallet/__init__.py
Normal file
31
wallet/admin.py
Normal file
31
wallet/admin.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import WalletTransaction
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(WalletTransaction)
|
||||||
|
class WalletTransactionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'type', 'amount', 'agent_slug', 'created_at', 'id')
|
||||||
|
list_filter = ('type', 'created_at', 'agent_slug')
|
||||||
|
search_fields = ('user__username', 'user__email', 'description', 'agent_slug')
|
||||||
|
readonly_fields = ('id', 'created_at')
|
||||||
|
ordering = ('-created_at',)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Transaction Details', {
|
||||||
|
'fields': ('user', 'type', 'amount', 'description')
|
||||||
|
}),
|
||||||
|
('Agent Information', {
|
||||||
|
'fields': ('agent_slug',)
|
||||||
|
}),
|
||||||
|
('Payment Information', {
|
||||||
|
'fields': ('stripe_session_id',)
|
||||||
|
}),
|
||||||
|
('Metadata', {
|
||||||
|
'fields': ('id', 'created_at')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
if obj: # editing an existing object
|
||||||
|
return self.readonly_fields + ('user', 'type', 'amount')
|
||||||
|
return self.readonly_fields
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.4 on 2025-07-08 08:17
|
# Generated by Django 5.2.4 on 2025-07-08 15:00
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import uuid
|
import uuid
|
||||||
0
wallet/migrations/__init__.py
Normal file
0
wallet/migrations/__init__.py
Normal file
@ -4,6 +4,7 @@ import uuid
|
|||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class WalletTransaction(models.Model):
|
class WalletTransaction(models.Model):
|
||||||
TRANSACTION_TYPES = [
|
TRANSACTION_TYPES = [
|
||||||
('top_up', 'Top Up'),
|
('top_up', 'Top Up'),
|
||||||
@ -25,4 +26,3 @@ class WalletTransaction(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.email} - {self.amount} AED ({self.type})"
|
return f"{self.user.email} - {self.amount} AED ({self.type})"
|
||||||
|
|
||||||
106
wallet/stripe_handler.py
Normal file
106
wallet/stripe_handler.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import stripe
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from decimal import Decimal
|
||||||
|
import json
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||||
|
|
||||||
|
|
||||||
|
class StripePaymentHandler:
|
||||||
|
def __init__(self):
|
||||||
|
self.payment_links = {
|
||||||
|
10: 'https://buy.stripe.com/test_28EbJ16AA7ly3ic7vh2VG0a',
|
||||||
|
50: 'https://buy.stripe.com/test_4gM00jbUUgW83ic3f12VG0b',
|
||||||
|
100: 'https://buy.stripe.com/test_aFadR99MM35ibOI6rd2VG0c',
|
||||||
|
500: 'https://buy.stripe.com/test_14AbJ12kk7lyf0U16T2VG0d'
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_checkout_session(self, user, amount):
|
||||||
|
"""Create a Stripe checkout session for wallet top-up"""
|
||||||
|
if amount not in self.payment_links:
|
||||||
|
raise ValueError(f"Invalid amount: {amount}")
|
||||||
|
|
||||||
|
payment_link = self.payment_links[amount]
|
||||||
|
|
||||||
|
# Return the payment link URL with user reference
|
||||||
|
return {
|
||||||
|
'payment_url': f"{payment_link}?client_reference_id={user.id}&prefilled_email={user.email}",
|
||||||
|
'session_id': None # Payment links don't have session IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
def verify_payment(self, session_id):
|
||||||
|
"""Verify payment from Stripe webhook"""
|
||||||
|
try:
|
||||||
|
session = stripe.checkout.Session.retrieve(session_id)
|
||||||
|
|
||||||
|
if session.payment_status == 'paid':
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'amount': session.amount_total / 100, # Convert from cents
|
||||||
|
'customer_email': session.customer_details.email,
|
||||||
|
'client_reference_id': session.client_reference_id
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {'success': False, 'error': 'Payment not completed'}
|
||||||
|
|
||||||
|
except stripe.error.StripeError as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def handle_webhook(self, payload, signature):
|
||||||
|
"""Handle Stripe webhook events"""
|
||||||
|
try:
|
||||||
|
event = stripe.Webhook.construct_event(
|
||||||
|
payload, signature, settings.STRIPE_WEBHOOK_SECRET
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return {'success': False, 'error': 'Invalid payload'}
|
||||||
|
except stripe.error.SignatureVerificationError:
|
||||||
|
return {'success': False, 'error': 'Invalid signature'}
|
||||||
|
|
||||||
|
if event['type'] == 'checkout.session.completed':
|
||||||
|
session = event['data']['object']
|
||||||
|
|
||||||
|
# Process successful payment
|
||||||
|
user_id = session.get('client_reference_id')
|
||||||
|
amount = session['amount_total'] / 100 # Convert from cents
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
try:
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
user.add_balance(
|
||||||
|
amount=amount,
|
||||||
|
description=f"Wallet top-up via Stripe",
|
||||||
|
stripe_session_id=session['id']
|
||||||
|
)
|
||||||
|
return {'success': True, 'message': 'Payment processed successfully'}
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return {'success': False, 'error': 'User not found'}
|
||||||
|
|
||||||
|
return {'success': True, 'message': 'Event processed'}
|
||||||
|
|
||||||
|
def process_refund(self, session_id, amount=None):
|
||||||
|
"""Process refund for a payment"""
|
||||||
|
try:
|
||||||
|
session = stripe.checkout.Session.retrieve(session_id)
|
||||||
|
payment_intent = session.payment_intent
|
||||||
|
|
||||||
|
if amount:
|
||||||
|
refund = stripe.Refund.create(
|
||||||
|
payment_intent=payment_intent,
|
||||||
|
amount=int(amount * 100) # Convert to cents
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
refund = stripe.Refund.create(payment_intent=payment_intent)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'refund_id': refund.id,
|
||||||
|
'amount': refund.amount / 100,
|
||||||
|
'status': refund.status
|
||||||
|
}
|
||||||
|
|
||||||
|
except stripe.error.StripeError as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
3
wallet/views.py
Normal file
3
wallet/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
Loading…
Reference in New Issue
Block a user