mirror of
https://github.com/thecyberlearn/quantum-ai.git
synced 2026-08-18 08:53:00 +00:00
Initial Django project setup with working homepage
- Complete Django project structure with apps/ organization - Working homepage with exact Next.js recreation (1039 lines) - User authentication system with wallet balance - Agent system with processors and models - Stripe payment integration setup - All Django migrations and URL routing - Comprehensive .gitignore file - Homepage loads successfully (HTTP 200) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
3fc5f01309
228
.gitignore
vendored
Normal file
228
.gitignore
vendored
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
||||||
|
# project, it is generally recommended to include it in version control.
|
||||||
|
# Uncomment the following line if you want to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Django specific
|
||||||
|
staticfiles/
|
||||||
|
media/
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
local_settings.py
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# Node.js (if using npm/yarn for frontend assets)
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# IDE specific files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS specific files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
server.log
|
||||||
|
cookies.txt
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Generated migration files (optional - some teams prefer to include these)
|
||||||
|
# migrations/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
|
||||||
|
# pytest
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Jupyter
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# Security sensitive files
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
secrets.jsonnetcop-ai-hub/
|
||||||
2678
DJANGO_RECREATION_GUIDE.md
Normal file
2678
DJANGO_RECREATION_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
0
apps/agents/__init__.py
Normal file
0
apps/agents/__init__.py
Normal file
3
apps/agents/admin.py
Normal file
3
apps/agents/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
131
apps/agents/agent_processors.py
Normal file
131
apps/agents/agent_processors.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
class AgentProcessor:
|
||||||
|
def __init__(self, agent_slug):
|
||||||
|
self.agent_slug = agent_slug
|
||||||
|
self.webhook_urls = {
|
||||||
|
'data-analyzer': settings.N8N_WEBHOOK_DATA_ANALYZER,
|
||||||
|
'five-whys': settings.N8N_WEBHOOK_FIVE_WHYS,
|
||||||
|
'job-posting-generator': settings.N8N_WEBHOOK_JOB_POSTING,
|
||||||
|
'faq-generator': settings.N8N_WEBHOOK_FAQ_GENERATOR,
|
||||||
|
'social-ads-generator': settings.N8N_WEBHOOK_SOCIAL_ADS,
|
||||||
|
'weather-reporter': settings.OPENWEATHER_API_KEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
def process_data_analyzer(self, file_obj, user_id):
|
||||||
|
"""Process file through N8N data analyzer webhook"""
|
||||||
|
webhook_url = self.webhook_urls.get('data-analyzer')
|
||||||
|
if not webhook_url:
|
||||||
|
raise ValueError("Data analyzer webhook URL not configured")
|
||||||
|
|
||||||
|
files = {'file': file_obj}
|
||||||
|
data = {'userId': user_id}
|
||||||
|
|
||||||
|
response = requests.post(webhook_url, files=files, data=data, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def process_five_whys(self, problem_description, user_id):
|
||||||
|
"""Process 5 whys analysis through N8N"""
|
||||||
|
webhook_url = self.webhook_urls.get('five-whys')
|
||||||
|
if not webhook_url:
|
||||||
|
raise ValueError("Five whys webhook URL not configured")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'problem': problem_description,
|
||||||
|
'userId': user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(webhook_url, json=data, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def process_weather_reporter(self, location):
|
||||||
|
"""Get weather data using OpenWeather API"""
|
||||||
|
api_key = settings.OPENWEATHER_API_KEY
|
||||||
|
if not api_key:
|
||||||
|
raise ValueError("OpenWeather API key not configured")
|
||||||
|
|
||||||
|
url = f"https://api.openweathermap.org/data/2.5/weather"
|
||||||
|
params = {
|
||||||
|
'q': location,
|
||||||
|
'appid': api_key,
|
||||||
|
'units': 'metric'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def process_job_posting(self, job_details, user_id):
|
||||||
|
"""Generate job posting through N8N"""
|
||||||
|
webhook_url = self.webhook_urls.get('job-posting-generator')
|
||||||
|
if not webhook_url:
|
||||||
|
raise ValueError("Job posting webhook URL not configured")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'jobDetails': job_details,
|
||||||
|
'userId': user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(webhook_url, json=data, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def process_social_ads(self, ad_requirements, user_id):
|
||||||
|
"""Generate social ads through N8N"""
|
||||||
|
webhook_url = self.webhook_urls.get('social-ads-generator')
|
||||||
|
if not webhook_url:
|
||||||
|
raise ValueError("Social ads webhook URL not configured")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'adRequirements': ad_requirements,
|
||||||
|
'userId': user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(webhook_url, json=data, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def process_faq_generator(self, content_source, user_id):
|
||||||
|
"""Generate FAQ through N8N"""
|
||||||
|
webhook_url = self.webhook_urls.get('faq-generator')
|
||||||
|
if not webhook_url:
|
||||||
|
raise ValueError("FAQ generator webhook URL not configured")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'contentSource': content_source,
|
||||||
|
'userId': user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(webhook_url, json=data, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def process_agent(self, **kwargs):
|
||||||
|
"""Main processing method - routes to appropriate processor"""
|
||||||
|
processor_map = {
|
||||||
|
'data-analyzer': self.process_data_analyzer,
|
||||||
|
'five-whys': self.process_five_whys,
|
||||||
|
'weather-reporter': self.process_weather_reporter,
|
||||||
|
'job-posting-generator': self.process_job_posting,
|
||||||
|
'social-ads-generator': self.process_social_ads,
|
||||||
|
'faq-generator': self.process_faq_generator,
|
||||||
|
}
|
||||||
|
|
||||||
|
processor = processor_map.get(self.agent_slug)
|
||||||
|
if not processor:
|
||||||
|
raise ValueError(f"No processor found for agent: {self.agent_slug}")
|
||||||
|
|
||||||
|
return processor(**kwargs)
|
||||||
6
apps/agents/apps.py
Normal file
6
apps/agents/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AgentsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'agents'
|
||||||
80
apps/agents/management/commands/populate_agents.py
Normal file
80
apps/agents/management/commands/populate_agents.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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}')
|
||||||
32
apps/agents/migrations/0001_initial.py
Normal file
32
apps/agents/migrations/0001_initial.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-07-08 08:17
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Agent',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('category', models.CharField(choices=[('analytics', 'Analytics'), ('utilities', 'Utilities'), ('content', 'Content'), ('marketing', 'Marketing'), ('customer-service', 'Customer Service')], max_length=50)),
|
||||||
|
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('icon', models.CharField(default='🤖', max_length=10)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('rating', models.DecimalField(decimal_places=1, default=Decimal('4.5'), max_digits=3)),
|
||||||
|
('review_count', models.IntegerField(default=0)),
|
||||||
|
('n8n_webhook_url', models.URLField(blank=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/agents/migrations/__init__.py
Normal file
0
apps/agents/migrations/__init__.py
Normal file
42
apps/agents/models.py
Normal file
42
apps/agents/models.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
from django.db import models
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
class Agent(models.Model):
|
||||||
|
CATEGORIES = [
|
||||||
|
('analytics', 'Analytics'),
|
||||||
|
('utilities', 'Utilities'),
|
||||||
|
('content', 'Content'),
|
||||||
|
('marketing', 'Marketing'),
|
||||||
|
('customer-service', 'Customer Service'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
description = models.TextField()
|
||||||
|
category = models.CharField(max_length=50, choices=CATEGORIES)
|
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
icon = models.CharField(max_length=10, default='🤖')
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
rating = models.DecimalField(max_digits=3, decimal_places=1, default=Decimal('4.5'))
|
||||||
|
review_count = models.IntegerField(default=0)
|
||||||
|
n8n_webhook_url = models.URLField(blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price_display(self):
|
||||||
|
return f"{self.price} AED"
|
||||||
|
|
||||||
|
def get_gradient_class(self):
|
||||||
|
gradient_map = {
|
||||||
|
'analytics': 'from-indigo-500 to-purple-600',
|
||||||
|
'utilities': 'from-sky-400 to-blue-500',
|
||||||
|
'content': 'from-purple-500 to-indigo-600',
|
||||||
|
'marketing': 'from-pink-500 to-rose-600',
|
||||||
|
'customer-service': 'from-blue-500 to-blue-600',
|
||||||
|
}
|
||||||
|
return gradient_map.get(self.category, 'from-gray-500 to-gray-600')
|
||||||
3
apps/agents/tests.py
Normal file
3
apps/agents/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
7
apps/agents/urls.py
Normal file
7
apps/agents/urls.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Agent management URLs would go here
|
||||||
|
# For now, agents are handled by core app
|
||||||
|
]
|
||||||
3
apps/agents/views.py
Normal file
3
apps/agents/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
0
apps/authentication/__init__.py
Normal file
0
apps/authentication/__init__.py
Normal file
3
apps/authentication/admin.py
Normal file
3
apps/authentication/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
apps/authentication/apps.py
Normal file
6
apps/authentication/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'authentication'
|
||||||
48
apps/authentication/migrations/0001_initial.py
Normal file
48
apps/authentication/migrations/0001_initial.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-07-08 08:17
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.utils.timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
|
('wallet_balance', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/authentication/migrations/__init__.py
Normal file
0
apps/authentication/migrations/__init__.py
Normal file
50
apps/authentication/models.py
Normal file
50
apps/authentication/models.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Create your models here.
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
email = models.EmailField(unique=True)
|
||||||
|
wallet_balance = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'email'
|
||||||
|
REQUIRED_FIELDS = ['username']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
|
|
||||||
|
def has_sufficient_balance(self, amount):
|
||||||
|
return self.wallet_balance >= Decimal(str(amount))
|
||||||
|
|
||||||
|
def deduct_balance(self, amount, description="", agent_slug=""):
|
||||||
|
if self.has_sufficient_balance(amount):
|
||||||
|
self.wallet_balance -= Decimal(str(amount))
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
# Create transaction record
|
||||||
|
from wallet.models import WalletTransaction
|
||||||
|
WalletTransaction.objects.create(
|
||||||
|
user=self,
|
||||||
|
amount=-Decimal(str(amount)),
|
||||||
|
type='agent_usage',
|
||||||
|
description=description,
|
||||||
|
agent_slug=agent_slug
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_balance(self, amount, description="", stripe_session_id=""):
|
||||||
|
self.wallet_balance += Decimal(str(amount))
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
# Create transaction record
|
||||||
|
from wallet.models import WalletTransaction
|
||||||
|
WalletTransaction.objects.create(
|
||||||
|
user=self,
|
||||||
|
amount=Decimal(str(amount)),
|
||||||
|
type='top_up',
|
||||||
|
description=description,
|
||||||
|
stripe_session_id=stripe_session_id
|
||||||
|
)
|
||||||
3
apps/authentication/tests.py
Normal file
3
apps/authentication/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
24
apps/authentication/urls.py
Normal file
24
apps/authentication/urls.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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'),
|
||||||
|
]
|
||||||
16
apps/authentication/views.py
Normal file
16
apps/authentication/views.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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})
|
||||||
0
apps/core/__init__.py
Normal file
0
apps/core/__init__.py
Normal file
3
apps/core/admin.py
Normal file
3
apps/core/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
apps/core/apps.py
Normal file
6
apps/core/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'core'
|
||||||
0
apps/core/migrations/__init__.py
Normal file
0
apps/core/migrations/__init__.py
Normal file
3
apps/core/models.py
Normal file
3
apps/core/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
3
apps/core/tests.py
Normal file
3
apps/core/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
17
apps/core/urls.py
Normal file
17
apps/core/urls.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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'),
|
||||||
|
]
|
||||||
452
apps/core/views.py
Normal file
452
apps/core/views.py
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
# 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()
|
||||||
|
})
|
||||||
|
|
||||||
0
apps/wallet/__init__.py
Normal file
0
apps/wallet/__init__.py
Normal file
3
apps/wallet/admin.py
Normal file
3
apps/wallet/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
apps/wallet/apps.py
Normal file
6
apps/wallet/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class WalletConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'wallet'
|
||||||
34
apps/wallet/migrations/0001_initial.py
Normal file
34
apps/wallet/migrations/0001_initial.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-07-08 08:17
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WalletTransaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('type', models.CharField(choices=[('top_up', 'Top Up'), ('agent_usage', 'Agent Usage'), ('refund', 'Refund')], max_length=20)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('agent_slug', models.CharField(blank=True, max_length=100)),
|
||||||
|
('stripe_session_id', models.CharField(blank=True, max_length=200)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wallet_transactions', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/wallet/migrations/__init__.py
Normal file
0
apps/wallet/migrations/__init__.py
Normal file
28
apps/wallet/models.py
Normal file
28
apps/wallet/models.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class WalletTransaction(models.Model):
|
||||||
|
TRANSACTION_TYPES = [
|
||||||
|
('top_up', 'Top Up'),
|
||||||
|
('agent_usage', 'Agent Usage'),
|
||||||
|
('refund', 'Refund'),
|
||||||
|
]
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='wallet_transactions')
|
||||||
|
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
type = models.CharField(max_length=20, choices=TRANSACTION_TYPES)
|
||||||
|
description = models.TextField()
|
||||||
|
agent_slug = models.CharField(max_length=100, blank=True)
|
||||||
|
stripe_session_id = models.CharField(max_length=200, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.email} - {self.amount} AED ({self.type})"
|
||||||
|
|
||||||
72
apps/wallet/stripe_handler.py
Normal file
72
apps/wallet/stripe_handler.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
|
|
||||||
3
apps/wallet/tests.py
Normal file
3
apps/wallet/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
9
apps/wallet/urls.py
Normal file
9
apps/wallet/urls.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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'),
|
||||||
|
]
|
||||||
76
apps/wallet/views.py
Normal file
76
apps/wallet/views.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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
manage.py
Executable file
22
manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_hub.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
0
netcop_hub/__init__.py
Normal file
0
netcop_hub/__init__.py
Normal file
16
netcop_hub/asgi.py
Normal file
16
netcop_hub/asgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for netcop_hub project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_hub.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
95
netcop_hub/settings.py
Normal file
95
netcop_hub/settings.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from decouple import config
|
||||||
|
import sys
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Add apps directory to Python path
|
||||||
|
sys.path.insert(0, str(BASE_DIR / 'apps'))
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECRET_KEY = config('SECRET_KEY', default='your-secret-key-here')
|
||||||
|
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||||
|
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'rest_framework',
|
||||||
|
'core',
|
||||||
|
'authentication',
|
||||||
|
'agents',
|
||||||
|
'wallet',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'netcop_hub.urls'
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [BASE_DIR / 'templates'],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Custom user model
|
||||||
|
AUTH_USER_MODEL = 'authentication.User'
|
||||||
|
|
||||||
|
# Static files
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
|
|
||||||
|
# Media files
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
# Stripe
|
||||||
|
STRIPE_SECRET_KEY = config('STRIPE_SECRET_KEY')
|
||||||
|
STRIPE_WEBHOOK_SECRET = config('STRIPE_WEBHOOK_SECRET')
|
||||||
|
|
||||||
|
# N8N Webhooks
|
||||||
|
N8N_WEBHOOK_DATA_ANALYZER = config('N8N_WEBHOOK_DATA_ANALYZER', default='')
|
||||||
|
N8N_WEBHOOK_FIVE_WHYS = config('N8N_WEBHOOK_FIVE_WHYS', default='')
|
||||||
|
N8N_WEBHOOK_JOB_POSTING = config('N8N_WEBHOOK_JOB_POSTING', default='')
|
||||||
|
N8N_WEBHOOK_FAQ_GENERATOR = config('N8N_WEBHOOK_FAQ_GENERATOR', default='')
|
||||||
|
N8N_WEBHOOK_SOCIAL_ADS = config('N8N_WEBHOOK_SOCIAL_ADS', default='')
|
||||||
|
|
||||||
|
# OpenWeather API
|
||||||
|
OPENWEATHER_API_KEY = config('OPENWEATHER_API_KEY', default='')
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default='').split(',')
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
15
netcop_hub/urls.py
Normal file
15
netcop_hub/urls.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('', include('core.urls')),
|
||||||
|
path('auth/', include('authentication.urls')),
|
||||||
|
path('agents/', include('agents.urls')),
|
||||||
|
path('wallet/', include('wallet.urls')),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
16
netcop_hub/wsgi.py
Normal file
16
netcop_hub/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for netcop_hub project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_hub.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
485
templates/agent_detail.html
Normal file
485
templates/agent_detail.html
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
{% 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 %}
|
||||||
74
templates/base.html
Normal file
74
templates/base.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{% 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>
|
||||||
36
templates/debug.html
Normal file
36
templates/debug.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% 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 %}
|
||||||
1039
templates/homepage.html
Normal file
1039
templates/homepage.html
Normal file
File diff suppressed because it is too large
Load Diff
45
templates/marketplace.html
Normal file
45
templates/marketplace.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% 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 %}
|
||||||
58
templates/reset_password.html
Normal file
58
templates/reset_password.html
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{% 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 %}
|
||||||
Loading…
Reference in New Issue
Block a user