mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 08:52:58 +00:00
🎯 Fix webhook agents by removing custom JavaScript conflicts
- Remove job-posting-generator.js and social-ads.js custom files - All webhook agents now use unified agents-core.js system - Consistent behavior: Form → Django API → N8N → Results - Fixes job posting and social ads result display issues - Simplifies maintenance with single JavaScript codebase - Aligns with file-based agent creation goal (JSON only) ✅ All 4 webhook agents now work consistently ✅ All 5 direct access agents use generic handlers ✅ Agent creation: Just add JSON file, no custom code needed 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
af67cfd077
commit
a46b07e371
16
agents/configs/agents/ai-voice-agent.json
Normal file
16
agents/configs/agents/ai-voice-agent.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"slug": "ai-voice-agent",
|
||||
"name": "AI Voice Agent",
|
||||
"short_description": "Transform your content with AI-powered voice generation and audio solutions",
|
||||
"description": "Create professional voiceovers, podcasts, and audio content using advanced AI voice technology. Perfect for marketing campaigns, educational content, and multimedia projects.",
|
||||
"category": "marketing",
|
||||
"price": 0.0,
|
||||
"agent_type": "form",
|
||||
"system_type": "direct_access",
|
||||
"form_schema": {
|
||||
"fields": []
|
||||
},
|
||||
"webhook_url": "https://agent.jotform.com/0198a8860b46796895f2a40367a6cea4df0c/voice",
|
||||
"access_url_name": "agents:direct_access_handler",
|
||||
"display_url_name": "agents:direct_access_display"
|
||||
}
|
||||
@ -7,7 +7,18 @@
|
||||
"price": 15.0,
|
||||
"agent_type": "chat",
|
||||
"system_type": "webhook",
|
||||
"form_schema": null,
|
||||
"form_schema": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "problem_description",
|
||||
"type": "textarea",
|
||||
"label": "Describe the problem you want to analyze",
|
||||
"placeholder": "Describe the issue, failure, or problem you're experiencing",
|
||||
"required": true,
|
||||
"rows": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"webhook_url": "http://localhost:5678/webhook/5-whys-web",
|
||||
"access_url_name": "",
|
||||
"display_url_name": ""
|
||||
|
||||
@ -14,392 +14,6 @@ from .services import AgentFileService
|
||||
from .utils import AgentCompat
|
||||
|
||||
|
||||
def career_navigator_access(request):
|
||||
"""Handle Try Now button click - charge wallet and redirect to form"""
|
||||
if not request.user.is_authenticated:
|
||||
# Clear all existing messages before adding login message
|
||||
storage = messages.get_messages(request)
|
||||
for _ in storage:
|
||||
pass # Consume all messages
|
||||
# Add login message to session for after login redirect
|
||||
request.session['post_login_message'] = 'Please complete your login to access the Career Navigator.'
|
||||
return redirect('authentication:login')
|
||||
|
||||
# Get the career navigator agent
|
||||
agent_data = AgentFileService.get_agent_by_slug('cybersec-career-navigator')
|
||||
if not agent_data or not agent_data.get('is_active', True):
|
||||
messages.error(request, 'Career Navigator is currently unavailable.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
agent_price = float(agent_data['price'])
|
||||
|
||||
# Check if user has sufficient balance
|
||||
if not request.user.has_sufficient_balance(agent_price):
|
||||
messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the Career Navigator.')
|
||||
return redirect('wallet:wallet')
|
||||
|
||||
# Deduct fee from user wallet
|
||||
success = request.user.deduct_balance(
|
||||
agent_price,
|
||||
f'{agent_data["name"]} - Direct Access',
|
||||
agent_data['slug']
|
||||
)
|
||||
|
||||
if not success:
|
||||
messages.error(request, 'Failed to process payment. Please try again.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# Create execution record for tracking
|
||||
execution = AgentExecution.objects.create(
|
||||
agent_slug=agent_data['slug'],
|
||||
agent_name=agent_data['name'],
|
||||
user=request.user,
|
||||
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
||||
fee_charged=agent_price,
|
||||
status='completed',
|
||||
output_data={
|
||||
'type': 'direct_access',
|
||||
'message': f'Direct access granted to {agent_data["name"]}',
|
||||
'access_method': 'try_now_button'
|
||||
},
|
||||
completed_at=timezone.now()
|
||||
)
|
||||
|
||||
# Redirect directly to form - no message needed
|
||||
return redirect('agents:career_navigator')
|
||||
|
||||
|
||||
def career_navigator_view(request):
|
||||
"""Display the career navigator form page"""
|
||||
if not request.user.is_authenticated:
|
||||
# Clear all existing messages before adding login message
|
||||
storage = messages.get_messages(request)
|
||||
for _ in storage:
|
||||
pass # Consume all messages
|
||||
# Add login message to session for after login redirect
|
||||
request.session['post_login_message'] = 'Please complete your login to access the Career Navigator.'
|
||||
return redirect('authentication:login')
|
||||
|
||||
# Get the career navigator agent
|
||||
agent_data = AgentFileService.get_agent_by_slug('cybersec-career-navigator')
|
||||
if not agent_data or not agent_data.get('is_active', True):
|
||||
messages.error(request, 'Career Navigator is currently unavailable.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# Convert to compatible object
|
||||
agent = AgentCompat(agent_data)
|
||||
|
||||
# Check if user has a recent execution (within last 2 hours) or just redirect to payment
|
||||
recent_execution = AgentExecution.objects.filter(
|
||||
agent_slug=agent.slug, # Changed to slug-based lookup
|
||||
user=request.user,
|
||||
status='completed',
|
||||
created_at__gte=timezone.now() - timedelta(hours=2)
|
||||
).first()
|
||||
|
||||
if not recent_execution:
|
||||
messages.info(request, 'Please click "Try Now" to access your Career Navigator consultation.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
context = {
|
||||
'agent': agent,
|
||||
'form_url': agent.webhook_url,
|
||||
'user_balance': request.user.wallet_balance,
|
||||
'execution': recent_execution
|
||||
}
|
||||
|
||||
return render(request, 'career_navigator.html', context)
|
||||
|
||||
|
||||
def ai_brand_strategist_view(request):
|
||||
"""Display the AI Brand Strategist form page"""
|
||||
if not request.user.is_authenticated:
|
||||
# Clear all existing messages before adding login message
|
||||
storage = messages.get_messages(request)
|
||||
for _ in storage:
|
||||
pass # Consume all messages
|
||||
# Add login message to session for after login redirect
|
||||
request.session['post_login_message'] = 'Please complete your login to access the AI Brand Strategist.'
|
||||
return redirect('authentication:login')
|
||||
|
||||
# Get the AI Brand Strategist agent
|
||||
agent_data = AgentFileService.get_agent_by_slug('ai-brand-strategist')
|
||||
if not agent_data or not agent_data.get('is_active', True):
|
||||
messages.error(request, 'AI Brand Strategist is currently unavailable.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# Convert to compatible object
|
||||
agent = AgentCompat(agent_data)
|
||||
|
||||
# Check if user has a recent execution (within last 2 hours) or just redirect to payment
|
||||
recent_execution = AgentExecution.objects.filter(
|
||||
agent_slug=agent.slug, # Changed to slug-based lookup
|
||||
user=request.user,
|
||||
status='completed',
|
||||
created_at__gte=timezone.now() - timedelta(hours=2)
|
||||
).first()
|
||||
|
||||
if not recent_execution:
|
||||
messages.info(request, 'Please click "Try Now" to access your AI Brand Strategist consultation.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
context = {
|
||||
'agent': agent,
|
||||
'form_url': agent.webhook_url,
|
||||
'user_balance': request.user.wallet_balance,
|
||||
'execution': recent_execution
|
||||
}
|
||||
|
||||
return render(request, 'ai_brand_strategist.html', context)
|
||||
|
||||
|
||||
def ai_brand_strategist_access(request):
|
||||
"""Handle Try Now button click - charge wallet and redirect to form"""
|
||||
if not request.user.is_authenticated:
|
||||
# Clear all existing messages before adding login message
|
||||
storage = messages.get_messages(request)
|
||||
for _ in storage:
|
||||
pass # Consume all messages
|
||||
# Add login message to session for after login redirect
|
||||
request.session['post_login_message'] = 'Please complete your login to access the AI Brand Strategist.'
|
||||
return redirect('authentication:login')
|
||||
|
||||
# Get the AI Brand Strategist agent
|
||||
agent_data = AgentFileService.get_agent_by_slug('ai-brand-strategist')
|
||||
if not agent_data or not agent_data.get('is_active', True):
|
||||
messages.error(request, 'AI Brand Strategist is currently unavailable.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
agent_price = float(agent_data['price'])
|
||||
|
||||
# Check if user has sufficient balance
|
||||
if not request.user.has_sufficient_balance(agent_price):
|
||||
messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the AI Brand Strategist.')
|
||||
return redirect('wallet:wallet')
|
||||
|
||||
# Deduct fee from user wallet
|
||||
success = request.user.deduct_balance(
|
||||
agent_price,
|
||||
f'{agent_data["name"]} - Direct Access',
|
||||
agent_data['slug']
|
||||
)
|
||||
|
||||
if not success:
|
||||
messages.error(request, 'Failed to process payment. Please try again.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# Create execution record for tracking
|
||||
execution = AgentExecution.objects.create(
|
||||
agent_slug=agent_data['slug'],
|
||||
agent_name=agent_data['name'],
|
||||
user=request.user,
|
||||
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
||||
fee_charged=agent_price,
|
||||
status='completed',
|
||||
output_data={
|
||||
'type': 'direct_access',
|
||||
'message': f'Direct access granted to {agent_data["name"]}',
|
||||
'access_method': 'try_now_button'
|
||||
},
|
||||
completed_at=timezone.now()
|
||||
)
|
||||
|
||||
# Redirect directly to form - no message needed
|
||||
return redirect('agents:ai_brand_strategist')
|
||||
|
||||
|
||||
def lean_six_sigma_expert_view(request):
|
||||
"""Display the Lean Six Sigma Expert form page"""
|
||||
if not request.user.is_authenticated:
|
||||
# Clear all existing messages before adding login message
|
||||
storage = messages.get_messages(request)
|
||||
for _ in storage:
|
||||
pass # Consume all messages
|
||||
# Add login message to session for after login redirect
|
||||
request.session['post_login_message'] = 'Please complete your login to access the Lean Six Sigma Expert.'
|
||||
return redirect('authentication:login')
|
||||
|
||||
# Get the Lean Six Sigma Expert agent
|
||||
agent_data = AgentFileService.get_agent_by_slug('lean-six-sigma-expert')
|
||||
if not agent_data or not agent_data.get('is_active', True):
|
||||
messages.error(request, 'Lean Six Sigma Expert is currently unavailable.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# Convert to compatible object
|
||||
agent = AgentCompat(agent_data)
|
||||
|
||||
# Check if user has a recent execution (within last 2 hours) or just redirect to payment
|
||||
recent_execution = AgentExecution.objects.filter(
|
||||
agent_slug=agent.slug, # Changed to slug-based lookup
|
||||
user=request.user,
|
||||
status='completed',
|
||||
created_at__gte=timezone.now() - timedelta(hours=2)
|
||||
).first()
|
||||
|
||||
if not recent_execution:
|
||||
messages.info(request, 'Please click "Try Now" to access your Lean Six Sigma Expert consultation.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
context = {
|
||||
'agent': agent,
|
||||
'form_url': agent.webhook_url,
|
||||
'user_balance': request.user.wallet_balance,
|
||||
'execution': recent_execution
|
||||
}
|
||||
|
||||
return render(request, 'lean_six_sigma_expert.html', context)
|
||||
|
||||
|
||||
def lean_six_sigma_expert_access(request):
|
||||
"""Handle Try Now button click - charge wallet and redirect to form"""
|
||||
if not request.user.is_authenticated:
|
||||
# Clear all existing messages before adding login message
|
||||
storage = messages.get_messages(request)
|
||||
for _ in storage:
|
||||
pass # Consume all messages
|
||||
# Add login message to session for after login redirect
|
||||
request.session['post_login_message'] = 'Please complete your login to access the Lean Six Sigma Expert.'
|
||||
return redirect('authentication:login')
|
||||
|
||||
# Get the Lean Six Sigma Expert agent
|
||||
agent_data = AgentFileService.get_agent_by_slug('lean-six-sigma-expert')
|
||||
if not agent_data or not agent_data.get('is_active', True):
|
||||
messages.error(request, 'Lean Six Sigma Expert is currently unavailable.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
agent_price = float(agent_data['price'])
|
||||
|
||||
# Check if user has sufficient balance
|
||||
if not request.user.has_sufficient_balance(agent_price):
|
||||
messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the Lean Six Sigma Expert.')
|
||||
return redirect('wallet:wallet')
|
||||
|
||||
# Deduct fee from user wallet
|
||||
success = request.user.deduct_balance(
|
||||
agent_price,
|
||||
f'{agent_data["name"]} - Direct Access',
|
||||
agent_data['slug']
|
||||
)
|
||||
|
||||
if not success:
|
||||
messages.error(request, 'Failed to process payment. Please try again.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# Create execution record for tracking
|
||||
execution = AgentExecution.objects.create(
|
||||
agent_slug=agent_data['slug'],
|
||||
agent_name=agent_data['name'],
|
||||
user=request.user,
|
||||
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
||||
fee_charged=agent_price,
|
||||
status='completed',
|
||||
output_data={
|
||||
'type': 'direct_access',
|
||||
'message': f'Direct access granted to {agent_data["name"]}',
|
||||
'access_method': 'try_now_button'
|
||||
},
|
||||
completed_at=timezone.now()
|
||||
)
|
||||
|
||||
# Redirect directly to form - no message needed
|
||||
return redirect('agents:lean_six_sigma_expert')
|
||||
|
||||
|
||||
def swot_analysis_expert_view(request):
|
||||
"""Display the SWOT Analysis Expert form page"""
|
||||
if not request.user.is_authenticated:
|
||||
# Clear all existing messages before adding login message
|
||||
storage = messages.get_messages(request)
|
||||
for _ in storage:
|
||||
pass # Consume all messages
|
||||
# Add login message to session for after login redirect
|
||||
request.session['post_login_message'] = 'Please complete your login to access the SWOT Analysis Expert.'
|
||||
return redirect('authentication:login')
|
||||
|
||||
# Get the SWOT Analysis Expert agent
|
||||
agent_data = AgentFileService.get_agent_by_slug('swot-analysis-expert')
|
||||
if not agent_data or not agent_data.get('is_active', True):
|
||||
messages.error(request, 'SWOT Analysis Expert is currently unavailable.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# Convert to compatible object
|
||||
agent = AgentCompat(agent_data)
|
||||
|
||||
# Check if user has a recent execution (within last 2 hours) or just redirect to payment
|
||||
recent_execution = AgentExecution.objects.filter(
|
||||
agent_slug=agent.slug, # Changed to slug-based lookup
|
||||
user=request.user,
|
||||
status='completed',
|
||||
created_at__gte=timezone.now() - timedelta(hours=2)
|
||||
).first()
|
||||
|
||||
if not recent_execution:
|
||||
messages.info(request, 'Please click "Try Now" to access your SWOT Analysis Expert consultation.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
context = {
|
||||
'agent': agent,
|
||||
'form_url': agent.webhook_url,
|
||||
'user_balance': request.user.wallet_balance,
|
||||
'execution': recent_execution
|
||||
}
|
||||
|
||||
return render(request, 'swot_analysis_expert.html', context)
|
||||
|
||||
|
||||
def swot_analysis_expert_access(request):
|
||||
"""Handle Try Now button click - charge wallet and redirect to form"""
|
||||
if not request.user.is_authenticated:
|
||||
# Clear all existing messages before adding login message
|
||||
storage = messages.get_messages(request)
|
||||
for _ in storage:
|
||||
pass # Consume all messages
|
||||
# Add login message to session for after login redirect
|
||||
request.session['post_login_message'] = 'Please complete your login to access the SWOT Analysis Expert.'
|
||||
return redirect('authentication:login')
|
||||
|
||||
# Get the SWOT Analysis Expert agent
|
||||
agent_data = AgentFileService.get_agent_by_slug('swot-analysis-expert')
|
||||
if not agent_data or not agent_data.get('is_active', True):
|
||||
messages.error(request, 'SWOT Analysis Expert is currently unavailable.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
agent_price = float(agent_data['price'])
|
||||
|
||||
# Check if user has sufficient balance
|
||||
if not request.user.has_sufficient_balance(agent_price):
|
||||
messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the SWOT Analysis Expert.')
|
||||
return redirect('wallet:wallet')
|
||||
|
||||
# Deduct fee from user wallet
|
||||
success = request.user.deduct_balance(
|
||||
agent_price,
|
||||
f'{agent_data["name"]} - Direct Access',
|
||||
agent_data['slug']
|
||||
)
|
||||
|
||||
if not success:
|
||||
messages.error(request, 'Failed to process payment. Please try again.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# Create execution record for tracking
|
||||
execution = AgentExecution.objects.create(
|
||||
agent_slug=agent_data['slug'],
|
||||
agent_name=agent_data['name'],
|
||||
user=request.user,
|
||||
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
||||
fee_charged=agent_price,
|
||||
status='completed',
|
||||
output_data={
|
||||
'type': 'direct_access',
|
||||
'message': f'Direct access granted to {agent_data["name"]}',
|
||||
'access_method': 'try_now_button'
|
||||
},
|
||||
completed_at=timezone.now()
|
||||
)
|
||||
|
||||
# Redirect directly to form - no message needed
|
||||
return redirect('agents:swot_analysis_expert')
|
||||
|
||||
|
||||
@login_required
|
||||
@ -465,6 +79,11 @@ def direct_access_display(request, slug):
|
||||
messages.error(request, 'This agent does not support direct access.')
|
||||
return redirect('agents:marketplace')
|
||||
|
||||
# For now, redirect directly to external form
|
||||
# Future: Can render iframe template or custom display logic
|
||||
return redirect(agent.webhook_url)
|
||||
# Render generic template with iframe to external form
|
||||
context = {
|
||||
'agent': agent,
|
||||
'form_url': agent.webhook_url,
|
||||
'user_balance': request.user.wallet_balance if hasattr(request.user, 'wallet_balance') else 0
|
||||
}
|
||||
|
||||
return render(request, 'agents/direct_access_agent.html', context)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Career Navigator - Quantum Tasks AI{% endblock %}
|
||||
{% block title %}{{ agent.name }} - Quantum Tasks AI{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
@ -37,7 +37,7 @@
|
||||
src="{{ form_url }}"
|
||||
frameborder="0"
|
||||
scrolling="auto"
|
||||
title="Career Navigator">
|
||||
title="{{ agent.name }}">
|
||||
</iframe>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -74,43 +74,23 @@
|
||||
<p class="agent-description">{{ agent.short_description }}</p>
|
||||
<div class="agent-footer">
|
||||
{% if user.is_authenticated %}
|
||||
{% if agent.slug == 'cybersec-career-navigator' %}
|
||||
<a href="{% url 'agents:career_navigator_access' %}" class="try-btn">
|
||||
Try Now →
|
||||
</a>
|
||||
{% elif agent.slug == 'ai-brand-strategist' %}
|
||||
<a href="{% url 'agents:ai_brand_strategist_access' %}" class="try-btn">
|
||||
Try Now →
|
||||
</a>
|
||||
{% elif agent.slug == 'lean-six-sigma-expert' %}
|
||||
<a href="{% url 'agents:lean_six_sigma_expert_access' %}" class="try-btn">
|
||||
Try Now →
|
||||
</a>
|
||||
{% elif agent.slug == 'swot-analysis-expert' %}
|
||||
<a href="{% url 'agents:swot_analysis_expert_access' %}" class="try-btn">
|
||||
{% if agent.access_url_name and agent.display_url_name %}
|
||||
<!-- Direct Access Agent -->
|
||||
<a href="{% url 'agents:direct_access_handler' agent.slug %}" class="try-btn">
|
||||
Try Now →
|
||||
</a>
|
||||
{% else %}
|
||||
<!-- Webhook Agent -->
|
||||
<a href="{% url 'agents:detail' agent.slug %}" class="try-btn">Try Now →</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if agent.slug == 'cybersec-career-navigator' %}
|
||||
<a href="{% url 'authentication:login' %}?next={% url 'agents:career_navigator_access' %}" class="try-btn login-required" style="width: 100%;">
|
||||
🔐 Login to Try
|
||||
</a>
|
||||
{% elif agent.slug == 'ai-brand-strategist' %}
|
||||
<a href="{% url 'authentication:login' %}?next={% url 'agents:ai_brand_strategist_access' %}" class="try-btn login-required" style="width: 100%;">
|
||||
🔐 Login to Try
|
||||
</a>
|
||||
{% elif agent.slug == 'lean-six-sigma-expert' %}
|
||||
<a href="{% url 'authentication:login' %}?next={% url 'agents:lean_six_sigma_expert_access' %}" class="try-btn login-required" style="width: 100%;">
|
||||
🔐 Login to Try
|
||||
</a>
|
||||
{% elif agent.slug == 'swot-analysis-expert' %}
|
||||
<a href="{% url 'authentication:login' %}?next={% url 'agents:swot_analysis_expert_access' %}" class="try-btn login-required" style="width: 100%;">
|
||||
{% if agent.access_url_name and agent.display_url_name %}
|
||||
<!-- Direct Access Agent -->
|
||||
<a href="{% url 'authentication:login' %}?next={% url 'agents:direct_access_handler' agent.slug %}" class="try-btn login-required" style="width: 100%;">
|
||||
🔐 Login to Try
|
||||
</a>
|
||||
{% else %}
|
||||
<!-- Webhook Agent -->
|
||||
<a href="{% url 'authentication:login' %}?next={% url 'agents:detail' agent.slug %}" class="try-btn login-required" style="width: 100%;">
|
||||
🔐 Login to Try
|
||||
</a>
|
||||
|
||||
@ -7,15 +7,7 @@ urlpatterns = [
|
||||
# Web interface
|
||||
path('', views.agents_marketplace, name='marketplace'),
|
||||
|
||||
# Direct access routes
|
||||
path('career-navigator/', views.career_navigator_view, name='career_navigator'),
|
||||
path('career-navigator/access/', views.career_navigator_access, name='career_navigator_access'),
|
||||
path('ai-brand-strategist/', views.ai_brand_strategist_view, name='ai_brand_strategist'),
|
||||
path('ai-brand-strategist/access/', views.ai_brand_strategist_access, name='ai_brand_strategist_access'),
|
||||
path('lean-six-sigma-expert/', views.lean_six_sigma_expert_view, name='lean_six_sigma_expert'),
|
||||
path('lean-six-sigma-expert/access/', views.lean_six_sigma_expert_access, name='lean_six_sigma_expert_access'),
|
||||
path('swot-analysis-expert/', views.swot_analysis_expert_view, name='swot_analysis_expert'),
|
||||
path('swot-analysis-expert/access/', views.swot_analysis_expert_access, name='swot_analysis_expert_access'),
|
||||
# Note: Direct access routes now handled by generic handlers below
|
||||
|
||||
# API endpoints - specific URLs first to avoid slug conflicts
|
||||
path('api/execute/', views.execute_agent, name='execute_agent'),
|
||||
|
||||
@ -26,14 +26,6 @@ from .web_views import (
|
||||
)
|
||||
|
||||
from .direct_access_views import (
|
||||
career_navigator_access,
|
||||
career_navigator_view,
|
||||
ai_brand_strategist_view,
|
||||
ai_brand_strategist_access,
|
||||
lean_six_sigma_expert_view,
|
||||
lean_six_sigma_expert_access,
|
||||
swot_analysis_expert_view,
|
||||
swot_analysis_expert_access,
|
||||
direct_access_handler,
|
||||
direct_access_display
|
||||
)
|
||||
|
||||
@ -39,10 +39,12 @@ Use existing categories first to avoid proliferation:
|
||||
|
||||
## Creating New Agents
|
||||
|
||||
### Step 1: Create JSON Configuration
|
||||
### Only Step: Create JSON Configuration ⚡
|
||||
|
||||
Add new file in `agents/configs/agents/your-agent-name.json`:
|
||||
|
||||
**That's it! No other files needed.**
|
||||
|
||||
#### Webhook Agent Example:
|
||||
```json
|
||||
{
|
||||
@ -99,17 +101,19 @@ Add new file in `agents/configs/agents/your-agent-name.json`:
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Commit to Git
|
||||
### Step 2: Commit to Git (Optional for Production)
|
||||
```bash
|
||||
git add agents/configs/agents/your-agent-name.json
|
||||
git commit -m "Add new agent: Your Agent Name"
|
||||
git commit -m "Add new agent: Your Agent Name 🤖 Generated with Claude Code"
|
||||
git push
|
||||
```
|
||||
|
||||
### Step 3: Done!
|
||||
- **Development:** Restart server to see new agent
|
||||
### Step 3: Done! ✅
|
||||
- **Development:** Agent loads automatically (or restart: `python manage.py runserver`)
|
||||
- **Production:** Railway auto-deploys and agent appears in marketplace
|
||||
|
||||
**No Python code, no URL routes, no templates needed!** 🎉
|
||||
|
||||
## JSON Configuration Reference
|
||||
|
||||
### Required Fields:
|
||||
@ -145,6 +149,46 @@ git push
|
||||
- `url` - URL input with validation
|
||||
- `checkbox` - Boolean checkbox
|
||||
|
||||
## System Architecture Benefits 🚀
|
||||
|
||||
### True File-Based System
|
||||
- **Zero Manual Coding**: Generic handlers for both agent types automatically handle everything
|
||||
- **Zero URL Configuration**: Dynamic routing based on JSON config properties
|
||||
- **Zero Templates**: Single generic template works for all direct access agents
|
||||
- **Zero Database Setup**: Pure file-based loading with intelligent caching
|
||||
|
||||
### Agent Type Handling
|
||||
- **Webhook Agents**: Automatically generate dynamic forms from `form_schema`
|
||||
- **Direct Access Agents**: Automatically handle payment processing + external redirect
|
||||
- **Both Types**: Work with only JSON configuration, no additional code
|
||||
|
||||
### Development Workflow Comparison
|
||||
```bash
|
||||
# ❌ Old Complex Way (5+ steps)
|
||||
1. Create JSON config
|
||||
2. Write Python view functions
|
||||
3. Add URL routes
|
||||
4. Create HTML templates
|
||||
5. Update view imports
|
||||
6. Test and debug
|
||||
|
||||
# ✅ New Simple Way (1 step)
|
||||
1. Create JSON config
|
||||
# Done! Everything else is automatic 🎉
|
||||
```
|
||||
|
||||
### How Both Agent Types Work Now
|
||||
|
||||
**Webhook Agents:**
|
||||
- JSON config → Dynamic form via `agent_detail_view`
|
||||
- Form submission → N8N webhook → Results display
|
||||
- No individual Python code needed
|
||||
|
||||
**Direct Access Agents:**
|
||||
- JSON config → Generic payment handler
|
||||
- Payment → Generic iframe display
|
||||
- No individual Python code needed
|
||||
|
||||
## Custom Integration (Advanced)
|
||||
|
||||
For agents needing custom behavior, add views to appropriate modules:
|
||||
|
||||
@ -1,428 +0,0 @@
|
||||
/**
|
||||
* Job Posting Generator - Agent-Specific JavaScript
|
||||
* Handles unique functionality for Job Posting Generator agent
|
||||
* Uses WorkflowsCore architecture like other agents
|
||||
*/
|
||||
|
||||
class JobPostingGeneratorProcessor extends WorkflowsCore {
|
||||
constructor() {
|
||||
super();
|
||||
this.agentSlug = 'job-posting-generator';
|
||||
this.webhookUrl = 'http://localhost:5678/webhook/43f84411-eaaa-488c-9b1f-856e90d0aaf6';
|
||||
this.price = 4.0; // Will be overridden by template data
|
||||
this.sessionId = this.constructor.generateSessionId();
|
||||
|
||||
// Initialize on page load
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Set data attributes from page
|
||||
const priceElement = document.body.getAttribute('data-agent-price');
|
||||
if (priceElement) {
|
||||
this.price = parseFloat(priceElement);
|
||||
}
|
||||
|
||||
// Initialize form submission
|
||||
const form = document.getElementById('agentForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', this.handleFormSubmission.bind(this));
|
||||
}
|
||||
|
||||
// Initialize form validation
|
||||
this.initializeFormValidation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission with hybrid N8N/Django approach
|
||||
*/
|
||||
async handleFormSubmission(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.isFormValid()) {
|
||||
this.constructor.showToast('Please fill in all required fields correctly', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check authentication and balance
|
||||
if (!this.constructor.checkAuthentication()) return;
|
||||
if (!this.constructor.checkBalance(this.price)) return;
|
||||
|
||||
// Show processing status and disable submit button
|
||||
this.constructor.showProcessing('Creating your professional job posting...');
|
||||
|
||||
const submitBtn = document.getElementById('generateBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Generating...';
|
||||
}
|
||||
|
||||
try {
|
||||
// Direct N8N integration
|
||||
await this.processViaDirectN8N(e.target);
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast('❌ Connection error. Please try again.', 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct N8N processing for better performance
|
||||
*/
|
||||
async processViaDirectN8N(form) {
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Extract form data
|
||||
const jobTitle = formData.get('job_title').trim();
|
||||
const companyName = formData.get('company_name').trim();
|
||||
const jobDescription = formData.get('job_description').trim();
|
||||
const seniorityLevel = formData.get('seniority_level');
|
||||
const contractType = formData.get('contract_type');
|
||||
const location = formData.get('location').trim();
|
||||
const language = formData.get('language') || 'English';
|
||||
|
||||
// Create message for N8N
|
||||
const messageText = `Create a professional job posting for: ${jobTitle} at ${companyName}. Description: ${jobDescription}. Seniority: ${seniorityLevel}. Contract: ${contractType}. Location: ${location}. Language: ${language}. Make it comprehensive and attractive to candidates.`;
|
||||
|
||||
const webhookData = {
|
||||
sessionId: this.sessionId,
|
||||
message: { text: messageText }
|
||||
};
|
||||
|
||||
// Direct N8N webhook call
|
||||
const response = await fetch(this.webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(webhookData),
|
||||
signal: AbortSignal.timeout(60000) // 60 second timeout
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`N8N error: ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
let data;
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
data = await response.json().catch(() => response.text());
|
||||
} else {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
// Process successful N8N response
|
||||
this.constructor.hideProcessing();
|
||||
|
||||
// Deduct wallet balance via Django API
|
||||
await this.constructor.deductBalance(
|
||||
this.price,
|
||||
`Job Posting Generator - ${jobTitle} at ${companyName}`,
|
||||
this.agentSlug
|
||||
);
|
||||
|
||||
// Display results using the enhanced display function
|
||||
this.displayDirectN8NResults(data, jobTitle, companyName);
|
||||
|
||||
this.constructor.showToast('✅ Job posting generated successfully!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('N8N processing error:', error);
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast('❌ Processing failed. Please try again.', 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Form validation specific to Job Posting Generator
|
||||
*/
|
||||
initializeFormValidation() {
|
||||
const requiredFields = ['job_title', 'company_name', 'job_description', 'seniority_level', 'contract_type', 'location'];
|
||||
|
||||
requiredFields.forEach(fieldName => {
|
||||
const field = document.getElementById(fieldName);
|
||||
if (field) {
|
||||
field.addEventListener('blur', () => this.validateField(fieldName));
|
||||
field.addEventListener('input', () => this.validateField(fieldName));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validateField(fieldName) {
|
||||
const field = document.getElementById(fieldName);
|
||||
if (!field) return true;
|
||||
|
||||
const value = field.value.trim();
|
||||
|
||||
switch (fieldName) {
|
||||
case 'job_title':
|
||||
if (!value) {
|
||||
this.constructor.showFieldError(fieldName, 'Job title is required');
|
||||
return false;
|
||||
}
|
||||
if (value.length < 3) {
|
||||
this.constructor.showFieldError(fieldName, 'Job title should be at least 3 characters');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'company_name':
|
||||
if (!value) {
|
||||
this.constructor.showFieldError(fieldName, 'Company name is required');
|
||||
return false;
|
||||
}
|
||||
if (value.length < 2) {
|
||||
this.constructor.showFieldError(fieldName, 'Company name should be at least 2 characters');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'job_description':
|
||||
if (!value) {
|
||||
this.constructor.showFieldError(fieldName, 'Job description is required');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'seniority_level':
|
||||
case 'contract_type':
|
||||
if (!value) {
|
||||
const fieldLabel = fieldName.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
this.constructor.showFieldError(fieldName, `${fieldLabel} is required`);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'location':
|
||||
if (!value) {
|
||||
this.constructor.showFieldError(fieldName, 'Location is required');
|
||||
return false;
|
||||
}
|
||||
if (value.length < 3) {
|
||||
this.constructor.showFieldError(fieldName, 'Location should be at least 3 characters');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.constructor.clearFieldError(fieldName);
|
||||
return true;
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
const requiredFields = ['job_title', 'company_name', 'job_description', 'seniority_level', 'contract_type', 'location'];
|
||||
|
||||
let isValid = true;
|
||||
requiredFields.forEach(fieldName => {
|
||||
if (!this.validateField(fieldName)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display results from direct N8N call
|
||||
*/
|
||||
displayDirectN8NResults(data, jobTitle, companyName) {
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const resultsContent = document.getElementById('resultsContent');
|
||||
|
||||
if (!resultsContainer || !resultsContent) return;
|
||||
|
||||
let content = '';
|
||||
|
||||
// Handle different N8N response formats
|
||||
if (typeof data === 'string') {
|
||||
content = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
content = data.output || data.text || data.content || data.job_posting || data.result || data.message || JSON.stringify(data, null, 2);
|
||||
} else {
|
||||
content = 'Job posting generated successfully!';
|
||||
}
|
||||
|
||||
// Clear and populate results securely
|
||||
resultsContent.textContent = '';
|
||||
this.renderSecureJobContent(resultsContent, content);
|
||||
|
||||
// Show results container
|
||||
resultsContainer.style.display = 'block';
|
||||
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure content rendering for job postings without innerHTML to prevent XSS
|
||||
*/
|
||||
renderSecureJobContent(container, content) {
|
||||
// Sanitize and validate content
|
||||
if (!content || typeof content !== 'string') {
|
||||
container.textContent = 'No content available';
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapper div
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'job-posting-content';
|
||||
|
||||
// Split content into lines and process safely
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (!line) {
|
||||
// Add line break for empty lines
|
||||
if (i > 0) wrapper.appendChild(document.createElement('br'));
|
||||
continue;
|
||||
}
|
||||
|
||||
let element;
|
||||
|
||||
// Handle headers (but escape content)
|
||||
if (line.startsWith('### ')) {
|
||||
element = document.createElement('h3');
|
||||
element.className = 'job-section-title';
|
||||
element.textContent = line.substring(4);
|
||||
} else if (line.startsWith('## ')) {
|
||||
element = document.createElement('h2');
|
||||
element.className = 'job-section-title';
|
||||
element.textContent = line.substring(3);
|
||||
} else if (line.startsWith('# ')) {
|
||||
element = document.createElement('h1');
|
||||
element.className = 'job-section-title';
|
||||
element.textContent = line.substring(2);
|
||||
} else if (line.startsWith('- ')) {
|
||||
// Handle list items
|
||||
element = document.createElement('li');
|
||||
element.textContent = line.substring(2);
|
||||
} else {
|
||||
// Handle regular text with basic formatting
|
||||
element = document.createElement('p');
|
||||
element.className = 'job-paragraph';
|
||||
this.formatJobTextSecurely(element, line);
|
||||
}
|
||||
|
||||
wrapper.appendChild(element);
|
||||
}
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format job posting text with basic styling while preventing XSS
|
||||
*/
|
||||
formatJobTextSecurely(element, text) {
|
||||
// Simple approach: handle bold and italic formatting securely
|
||||
const parts = [];
|
||||
let currentText = text;
|
||||
|
||||
// Process **bold** text
|
||||
currentText = currentText.replace(/\*\*(.*?)\*\*/g, (match, content) => {
|
||||
const placeholder = `__BOLD_${parts.length}__`;
|
||||
parts.push({type: 'bold', content: content});
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Process *italic* text
|
||||
currentText = currentText.replace(/\*(.*?)\*/g, (match, content) => {
|
||||
const placeholder = `__ITALIC_${parts.length}__`;
|
||||
parts.push({type: 'italic', content: content});
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Split by placeholders and create DOM elements
|
||||
const segments = currentText.split(/(__(?:BOLD|ITALIC)_\d+__)/);
|
||||
|
||||
segments.forEach(segment => {
|
||||
if (segment.startsWith('__BOLD_')) {
|
||||
const index = parseInt(segment.match(/\d+/)[0]);
|
||||
const strong = document.createElement('strong');
|
||||
strong.textContent = parts[index].content;
|
||||
element.appendChild(strong);
|
||||
} else if (segment.startsWith('__ITALIC_')) {
|
||||
const index = parseInt(segment.match(/\d+/)[0]);
|
||||
const em = document.createElement('em');
|
||||
em.textContent = parts[index].content;
|
||||
element.appendChild(em);
|
||||
} else if (segment) {
|
||||
element.appendChild(document.createTextNode(segment));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset submit button to original state
|
||||
*/
|
||||
resetSubmitButton() {
|
||||
const submitBtn = document.getElementById('generateBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = `💼 Generate Job Posting (${this.price} AED)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result action functions (global for button onclick handlers)
|
||||
function copyResults() {
|
||||
const content = document.getElementById('resultsContent');
|
||||
if (content) {
|
||||
const text = content.textContent || '';
|
||||
WorkflowsCore.copyToClipboard(text, 'Job posting copied to clipboard!');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadResults() {
|
||||
const content = document.getElementById('resultsContent');
|
||||
if (content) {
|
||||
const text = content.textContent || '';
|
||||
const jobTitle = document.getElementById('job_title')?.value || 'job-posting';
|
||||
const filename = `${jobTitle.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}.txt`;
|
||||
WorkflowsCore.downloadAsFile(text, filename, 'Job posting downloaded!');
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
const form = document.getElementById('agentForm');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
|
||||
if (resultsContainer) resultsContainer.style.display = 'none';
|
||||
if (processingStatus) processingStatus.style.display = 'none';
|
||||
|
||||
// Clear validation errors
|
||||
const fields = ['job_title', 'company_name', 'job_description', 'seniority_level', 'contract_type', 'location'];
|
||||
fields.forEach(field => WorkflowsCore.clearFieldError(field));
|
||||
|
||||
// Scroll back to form
|
||||
const formSection = document.getElementById('agentForm');
|
||||
if (formSection) {
|
||||
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Job Posting Generator Processor when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize processor (data attributes set by template)
|
||||
window.jobPostingGeneratorProcessor = new JobPostingGeneratorProcessor();
|
||||
});
|
||||
@ -1,389 +0,0 @@
|
||||
/**
|
||||
* Social Ads Generator - Agent-Specific JavaScript
|
||||
* Handles unique functionality for Social Ads Generator agent
|
||||
*/
|
||||
|
||||
class SocialAdsProcessor extends WorkflowsCore {
|
||||
constructor() {
|
||||
super();
|
||||
this.agentSlug = 'social-ads-generator';
|
||||
this.webhookUrl = 'http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d';
|
||||
this.price = 5.0; // Will be overridden by template data
|
||||
this.sessionId = this.constructor.generateSessionId();
|
||||
|
||||
// Initialize on page load
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Set data attributes from page
|
||||
const priceElement = document.body.getAttribute('data-agent-price');
|
||||
if (priceElement) {
|
||||
this.price = parseFloat(priceElement);
|
||||
}
|
||||
|
||||
// Initialize form submission
|
||||
const form = document.getElementById('agentForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', this.handleFormSubmission.bind(this));
|
||||
}
|
||||
|
||||
// Initialize form validation
|
||||
this.initializeFormValidation();
|
||||
|
||||
// Set initial radio selection if any exist
|
||||
const firstRadio = document.querySelector('.radio-card');
|
||||
if (firstRadio && !document.querySelector('.radio-card.selected')) {
|
||||
firstRadio.classList.add('selected');
|
||||
const input = firstRadio.querySelector('input[type="radio"]');
|
||||
if (input) input.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission with hybrid N8N/Django approach
|
||||
*/
|
||||
async handleFormSubmission(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.isFormValid()) {
|
||||
this.constructor.showToast('Please fill in all required fields correctly', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check authentication and balance
|
||||
if (!this.constructor.checkAuthentication()) return;
|
||||
if (!this.constructor.checkBalance(this.price)) return;
|
||||
|
||||
// Show processing status and disable submit button
|
||||
this.constructor.showProcessing('Generating your social ads...');
|
||||
|
||||
const submitBtn = document.getElementById('generateBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Generating...';
|
||||
}
|
||||
|
||||
try {
|
||||
// Direct N8N integration
|
||||
await this.processViaDirectN8N(e.target);
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast('❌ Connection error. Please try again.', 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct N8N processing for better performance
|
||||
*/
|
||||
async processViaDirectN8N(form) {
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Extract form data
|
||||
const description = formData.get('description').trim();
|
||||
const platform = formData.get('social_platform');
|
||||
const emoji = formData.get('include_emoji');
|
||||
const language = formData.get('language');
|
||||
|
||||
// Create message for N8N
|
||||
const messageText = `Create compelling social media ads for: ${description}. Target platform: ${platform}. Include emojis: ${emoji}. Language: ${language}. Make it engaging and professional.`;
|
||||
|
||||
const webhookData = {
|
||||
sessionId: this.sessionId,
|
||||
message: { text: messageText }
|
||||
};
|
||||
|
||||
// Direct N8N webhook call
|
||||
const response = await fetch(this.webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(webhookData),
|
||||
signal: AbortSignal.timeout(60000) // 60 second timeout
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`N8N error: ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
let data;
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
data = await response.json().catch(() => response.text());
|
||||
} else {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
// Process successful N8N response
|
||||
this.constructor.hideProcessing();
|
||||
|
||||
// Deduct wallet balance via Django API
|
||||
await this.constructor.deductBalance(
|
||||
this.price,
|
||||
`Social Ads Generator - ${description.substring(0, 50)}...`,
|
||||
this.agentSlug
|
||||
);
|
||||
|
||||
// Display results using the enhanced display function
|
||||
this.displayDirectN8NResults(data, platform, language);
|
||||
|
||||
this.constructor.showToast('✅ Social ads generated successfully!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('N8N processing error:', error);
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast('❌ Processing failed. Please try again.', 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display results from direct N8N call
|
||||
*/
|
||||
displayDirectN8NResults(data, platform, language) {
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const resultsContent = document.getElementById('resultsContent');
|
||||
|
||||
if (!resultsContainer || !resultsContent) return;
|
||||
|
||||
let content = '';
|
||||
|
||||
// Handle different N8N response formats
|
||||
if (typeof data === 'string') {
|
||||
content = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
content = data.output || data.text || data.content || data.ad_copy || data.result || data.message || JSON.stringify(data, null, 2);
|
||||
} else {
|
||||
content = 'Social ads generated successfully!';
|
||||
}
|
||||
|
||||
// Clear and populate results securely
|
||||
resultsContent.textContent = '';
|
||||
this.renderSecureContent(resultsContent, content);
|
||||
|
||||
// Show results container
|
||||
resultsContainer.style.display = 'block';
|
||||
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure content rendering without innerHTML to prevent XSS
|
||||
*/
|
||||
renderSecureContent(container, content) {
|
||||
// Sanitize and validate content
|
||||
if (!content || typeof content !== 'string') {
|
||||
container.textContent = 'No content available';
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapper paragraph
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'results-content';
|
||||
|
||||
// Split content into lines and process safely
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (!line) {
|
||||
// Add line break for empty lines
|
||||
if (i > 0) wrapper.appendChild(document.createElement('br'));
|
||||
continue;
|
||||
}
|
||||
|
||||
let element;
|
||||
|
||||
// Handle headers (but escape content)
|
||||
if (line.startsWith('### ')) {
|
||||
element = document.createElement('h3');
|
||||
element.textContent = line.substring(4);
|
||||
} else if (line.startsWith('## ')) {
|
||||
element = document.createElement('h2');
|
||||
element.textContent = line.substring(3);
|
||||
} else if (line.startsWith('# ')) {
|
||||
element = document.createElement('h1');
|
||||
element.textContent = line.substring(2);
|
||||
} else {
|
||||
// Handle regular text with basic formatting
|
||||
element = document.createElement('span');
|
||||
this.formatTextSecurely(element, line);
|
||||
}
|
||||
|
||||
wrapper.appendChild(element);
|
||||
|
||||
// Add line break if not the last line
|
||||
if (i < lines.length - 1) {
|
||||
wrapper.appendChild(document.createElement('br'));
|
||||
}
|
||||
}
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format text with basic styling while preventing XSS
|
||||
*/
|
||||
formatTextSecurely(element, text) {
|
||||
// Simple approach: handle bold and italic formatting securely
|
||||
const parts = [];
|
||||
let currentText = text;
|
||||
|
||||
// Process **bold** text
|
||||
currentText = currentText.replace(/\*\*(.*?)\*\*/g, (match, content) => {
|
||||
const placeholder = `__BOLD_${parts.length}__`;
|
||||
parts.push({type: 'bold', content: content});
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Process *italic* text
|
||||
currentText = currentText.replace(/\*(.*?)\*/g, (match, content) => {
|
||||
const placeholder = `__ITALIC_${parts.length}__`;
|
||||
parts.push({type: 'italic', content: content});
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Split by placeholders and create DOM elements
|
||||
const segments = currentText.split(/(__(?:BOLD|ITALIC)_\d+__)/);
|
||||
|
||||
segments.forEach(segment => {
|
||||
if (segment.startsWith('__BOLD_')) {
|
||||
const index = parseInt(segment.match(/\d+/)[0]);
|
||||
const strong = document.createElement('strong');
|
||||
strong.textContent = parts[index].content;
|
||||
element.appendChild(strong);
|
||||
} else if (segment.startsWith('__ITALIC_')) {
|
||||
const index = parseInt(segment.match(/\d+/)[0]);
|
||||
const em = document.createElement('em');
|
||||
em.textContent = parts[index].content;
|
||||
element.appendChild(em);
|
||||
} else if (segment) {
|
||||
element.appendChild(document.createTextNode(segment));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation specific to Social Ads Generator
|
||||
*/
|
||||
initializeFormValidation() {
|
||||
const fields = ['description', 'social_platform', 'include_emoji'];
|
||||
|
||||
fields.forEach(fieldName => {
|
||||
const field = document.getElementById(fieldName);
|
||||
if (field) {
|
||||
field.addEventListener('blur', () => this.validateField(fieldName));
|
||||
field.addEventListener('input', () => this.constructor.clearFieldError(fieldName));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validateField(fieldName) {
|
||||
const field = document.getElementById(fieldName);
|
||||
const value = field.value.trim();
|
||||
|
||||
switch (fieldName) {
|
||||
case 'description':
|
||||
if (!value) {
|
||||
this.constructor.showFieldError(fieldName, 'Please provide a description of your product or service');
|
||||
return false;
|
||||
} else if (value.length < 10) {
|
||||
this.constructor.showFieldError(fieldName, 'Description must be at least 10 characters long');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'social_platform':
|
||||
if (!value) {
|
||||
this.constructor.showFieldError(fieldName, 'Please select a social media platform');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'include_emoji':
|
||||
if (!value) {
|
||||
this.constructor.showFieldError(fieldName, 'Please select whether to include emojis');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.constructor.clearFieldError(fieldName);
|
||||
return true;
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
const fields = ['description', 'social_platform', 'include_emoji'];
|
||||
let isValid = true;
|
||||
|
||||
fields.forEach(fieldName => {
|
||||
if (!this.validateField(fieldName)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reset submit button to original state
|
||||
*/
|
||||
resetSubmitButton() {
|
||||
const submitBtn = document.getElementById('generateBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = `📢 Generate Social Ads (${this.price} AED)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result action functions (global for button onclick handlers)
|
||||
function copyResults() {
|
||||
const content = document.getElementById('resultsContent');
|
||||
if (content) {
|
||||
const text = content.textContent || '';
|
||||
WorkflowsCore.copyToClipboard(text, 'Social ads copied to clipboard!');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadResults() {
|
||||
const content = document.getElementById('resultsContent');
|
||||
if (content) {
|
||||
const text = content.textContent || '';
|
||||
WorkflowsCore.downloadAsFile(text, 'social-ads-results.txt', 'Social ads downloaded!');
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
const form = document.getElementById('agentForm');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
const processingStatus = document.getElementById('processingStatus');
|
||||
|
||||
if (resultsContainer) resultsContainer.style.display = 'none';
|
||||
if (processingStatus) processingStatus.style.display = 'none';
|
||||
|
||||
// Clear validation errors
|
||||
const fields = ['description', 'social_platform', 'include_emoji'];
|
||||
fields.forEach(fieldName => WorkflowsCore.clearFieldError(fieldName));
|
||||
|
||||
// Scroll back to form
|
||||
const formSection = document.getElementById('agentForm');
|
||||
if (formSection) {
|
||||
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Social Ads Processor when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize processor (data attributes set by template)
|
||||
window.socialAdsProcessor = new SocialAdsProcessor();
|
||||
});
|
||||
43
static/js/wallet-test.js
Normal file
43
static/js/wallet-test.js
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Quick wallet balance update test
|
||||
* Run this in browser console to test wallet balance updates
|
||||
*/
|
||||
|
||||
function testWalletUpdate() {
|
||||
console.log('Testing wallet balance update...');
|
||||
|
||||
// Get current balance from data attribute
|
||||
const currentBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0');
|
||||
console.log('Current balance from data attribute:', currentBalance);
|
||||
|
||||
// Get header balance element
|
||||
const headerBalance = document.querySelector('[data-wallet-balance]');
|
||||
console.log('Header balance element found:', !!headerBalance);
|
||||
if (headerBalance) {
|
||||
console.log('Current header text:', headerBalance.textContent);
|
||||
}
|
||||
|
||||
// Test updating to a new balance
|
||||
const testBalance = currentBalance - 6.0;
|
||||
console.log('Testing update to:', testBalance);
|
||||
|
||||
if (window.WorkflowsCore) {
|
||||
WorkflowsCore.updateWalletBalance(testBalance);
|
||||
console.log('✅ WorkflowsCore.updateWalletBalance() called');
|
||||
|
||||
// Check if it worked
|
||||
if (headerBalance) {
|
||||
console.log('New header text:', headerBalance.textContent);
|
||||
}
|
||||
console.log('New data attribute:', document.body.getAttribute('data-user-balance'));
|
||||
} else {
|
||||
console.log('❌ WorkflowsCore not available');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run test
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', testWalletUpdate);
|
||||
} else {
|
||||
testWalletUpdate();
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}AI Brand Strategist - Quantum Tasks AI{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Override main-container for full-width iframe */
|
||||
.main-container {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
height: calc(100vh - 80px); /* Account for header height */
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Hide footer for this page */
|
||||
.footer {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
src="{{ form_url }}"
|
||||
frameborder="0"
|
||||
scrolling="auto"
|
||||
title="AI Brand Strategist">
|
||||
</iframe>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -8,9 +8,11 @@
|
||||
|
||||
<div class="quick-agents-grid">
|
||||
{% for agent in all_agents %}
|
||||
{% if agent.slug == 'cybersec-career-navigator' %}
|
||||
<a href="{% url 'agents:career_navigator_access' %}" class="quick-agent-card">
|
||||
{% if agent.access_url_name and agent.display_url_name %}
|
||||
<!-- Direct Access Agent -->
|
||||
<a href="{% url 'agents:direct_access_handler' agent.slug %}" class="quick-agent-card">
|
||||
{% else %}
|
||||
<!-- Webhook Agent -->
|
||||
<a href="{% url 'agents:detail' agent.slug %}" class="quick-agent-card">
|
||||
{% endif %}
|
||||
<div class="agent-icon">{{ agent.category.icon }}</div>
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Lean Six Sigma Expert - Quantum Tasks AI{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Override main-container for full-width iframe */
|
||||
.main-container {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
height: calc(100vh - 80px); /* Account for header height */
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Hide footer for this page */
|
||||
.footer {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
src="{{ form_url }}"
|
||||
frameborder="0"
|
||||
scrolling="auto"
|
||||
title="Lean Six Sigma Expert">
|
||||
</iframe>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,38 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}SWOT Analysis Expert - Quantum Tasks AI{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.main-container {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
height: calc(100vh - 80px);
|
||||
}
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
.footer {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
src="{{ form_url }}"
|
||||
frameborder="0"
|
||||
scrolling="auto"
|
||||
title="SWOT Analysis Expert">
|
||||
</iframe>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user