mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 16:32:57 +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,
|
"price": 15.0,
|
||||||
"agent_type": "chat",
|
"agent_type": "chat",
|
||||||
"system_type": "webhook",
|
"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",
|
"webhook_url": "http://localhost:5678/webhook/5-whys-web",
|
||||||
"access_url_name": "",
|
"access_url_name": "",
|
||||||
"display_url_name": ""
|
"display_url_name": ""
|
||||||
|
|||||||
@ -14,392 +14,6 @@ from .services import AgentFileService
|
|||||||
from .utils import AgentCompat
|
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
|
@login_required
|
||||||
@ -465,6 +79,11 @@ def direct_access_display(request, slug):
|
|||||||
messages.error(request, 'This agent does not support direct access.')
|
messages.error(request, 'This agent does not support direct access.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# For now, redirect directly to external form
|
# Render generic template with iframe to external form
|
||||||
# Future: Can render iframe template or custom display logic
|
context = {
|
||||||
return redirect(agent.webhook_url)
|
'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' %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Career Navigator - Quantum Tasks AI{% endblock %}
|
{% block title %}{{ agent.name }} - Quantum Tasks AI{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
@ -37,7 +37,7 @@
|
|||||||
src="{{ form_url }}"
|
src="{{ form_url }}"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
scrolling="auto"
|
scrolling="auto"
|
||||||
title="Career Navigator">
|
title="{{ agent.name }}">
|
||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -74,43 +74,23 @@
|
|||||||
<p class="agent-description">{{ agent.short_description }}</p>
|
<p class="agent-description">{{ agent.short_description }}</p>
|
||||||
<div class="agent-footer">
|
<div class="agent-footer">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% if agent.slug == 'cybersec-career-navigator' %}
|
{% if agent.access_url_name and agent.display_url_name %}
|
||||||
<a href="{% url 'agents:career_navigator_access' %}" class="try-btn">
|
<!-- Direct Access Agent -->
|
||||||
Try Now →
|
<a href="{% url 'agents:direct_access_handler' agent.slug %}" class="try-btn">
|
||||||
</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">
|
|
||||||
Try Now →
|
Try Now →
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<!-- Webhook Agent -->
|
||||||
<a href="{% url 'agents:detail' agent.slug %}" class="try-btn">Try Now →</a>
|
<a href="{% url 'agents:detail' agent.slug %}" class="try-btn">Try Now →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if agent.slug == 'cybersec-career-navigator' %}
|
{% if agent.access_url_name and agent.display_url_name %}
|
||||||
<a href="{% url 'authentication:login' %}?next={% url 'agents:career_navigator_access' %}" class="try-btn login-required" style="width: 100%;">
|
<!-- Direct Access Agent -->
|
||||||
🔐 Login to Try
|
<a href="{% url 'authentication:login' %}?next={% url 'agents:direct_access_handler' agent.slug %}" class="try-btn login-required" style="width: 100%;">
|
||||||
</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%;">
|
|
||||||
🔐 Login to Try
|
🔐 Login to Try
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<!-- Webhook Agent -->
|
||||||
<a href="{% url 'authentication:login' %}?next={% url 'agents:detail' agent.slug %}" class="try-btn login-required" style="width: 100%;">
|
<a href="{% url 'authentication:login' %}?next={% url 'agents:detail' agent.slug %}" class="try-btn login-required" style="width: 100%;">
|
||||||
🔐 Login to Try
|
🔐 Login to Try
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -7,15 +7,7 @@ urlpatterns = [
|
|||||||
# Web interface
|
# Web interface
|
||||||
path('', views.agents_marketplace, name='marketplace'),
|
path('', views.agents_marketplace, name='marketplace'),
|
||||||
|
|
||||||
# Direct access routes
|
# Note: Direct access routes now handled by generic handlers below
|
||||||
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'),
|
|
||||||
|
|
||||||
# API endpoints - specific URLs first to avoid slug conflicts
|
# API endpoints - specific URLs first to avoid slug conflicts
|
||||||
path('api/execute/', views.execute_agent, name='execute_agent'),
|
path('api/execute/', views.execute_agent, name='execute_agent'),
|
||||||
|
|||||||
@ -26,14 +26,6 @@ from .web_views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .direct_access_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_handler,
|
||||||
direct_access_display
|
direct_access_display
|
||||||
)
|
)
|
||||||
|
|||||||
@ -39,10 +39,12 @@ Use existing categories first to avoid proliferation:
|
|||||||
|
|
||||||
## Creating New Agents
|
## Creating New Agents
|
||||||
|
|
||||||
### Step 1: Create JSON Configuration
|
### Only Step: Create JSON Configuration ⚡
|
||||||
|
|
||||||
Add new file in `agents/configs/agents/your-agent-name.json`:
|
Add new file in `agents/configs/agents/your-agent-name.json`:
|
||||||
|
|
||||||
|
**That's it! No other files needed.**
|
||||||
|
|
||||||
#### Webhook Agent Example:
|
#### Webhook Agent Example:
|
||||||
```json
|
```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
|
```bash
|
||||||
git add agents/configs/agents/your-agent-name.json
|
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
|
git push
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Done!
|
### Step 3: Done! ✅
|
||||||
- **Development:** Restart server to see new agent
|
- **Development:** Agent loads automatically (or restart: `python manage.py runserver`)
|
||||||
- **Production:** Railway auto-deploys and agent appears in marketplace
|
- **Production:** Railway auto-deploys and agent appears in marketplace
|
||||||
|
|
||||||
|
**No Python code, no URL routes, no templates needed!** 🎉
|
||||||
|
|
||||||
## JSON Configuration Reference
|
## JSON Configuration Reference
|
||||||
|
|
||||||
### Required Fields:
|
### Required Fields:
|
||||||
@ -145,6 +149,46 @@ git push
|
|||||||
- `url` - URL input with validation
|
- `url` - URL input with validation
|
||||||
- `checkbox` - Boolean checkbox
|
- `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)
|
## Custom Integration (Advanced)
|
||||||
|
|
||||||
For agents needing custom behavior, add views to appropriate modules:
|
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">
|
<div class="quick-agents-grid">
|
||||||
{% for agent in all_agents %}
|
{% for agent in all_agents %}
|
||||||
{% if agent.slug == 'cybersec-career-navigator' %}
|
{% if agent.access_url_name and agent.display_url_name %}
|
||||||
<a href="{% url 'agents:career_navigator_access' %}" class="quick-agent-card">
|
<!-- Direct Access Agent -->
|
||||||
|
<a href="{% url 'agents:direct_access_handler' agent.slug %}" class="quick-agent-card">
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<!-- Webhook Agent -->
|
||||||
<a href="{% url 'agents:detail' agent.slug %}" class="quick-agent-card">
|
<a href="{% url 'agents:detail' agent.slug %}" class="quick-agent-card">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="agent-icon">{{ agent.category.icon }}</div>
|
<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