🎯 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:
Claude 2025-08-15 09:35:43 +05:30
parent af67cfd077
commit a46b07e371
15 changed files with 143 additions and 1385 deletions

View 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"
}

View File

@ -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": ""

View File

@ -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)

View File

@ -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 %}

View File

@ -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>

View File

@ -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'),

View File

@ -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
)

View File

@ -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:

View File

@ -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();
});

View File

@ -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
View 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();
}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}