mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 10:12:58 +00:00
💰 Fix wallet balance updates and agent result formatting
- Fix wallet balance display targeting correct #walletBalance element - Add proper result formatting for PDF analyzer (structured sections) - Add clean formatting for job posting generator (simple bold text) - Fix "Explore Other Agents" button functionality with slide panel - Add missing workflows-core.js base class for frontend functionality - Ensure wallet updates after successful AI execution (not before) - Add debugging and error handling for better user experience 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
33dcdc23ba
commit
bf2807f9e0
213
agents/views.py
213
agents/views.py
@ -189,46 +189,35 @@ def career_navigator_access(request):
|
|||||||
messages.error(request, 'Career Navigator is currently unavailable.')
|
messages.error(request, 'Career Navigator is currently unavailable.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Convert to compatible object
|
agent_price = float(agent_data['price'])
|
||||||
class AgentCompat:
|
|
||||||
def __init__(self, data):
|
|
||||||
self.slug = data['slug']
|
|
||||||
self.name = data['name']
|
|
||||||
self.price = float(data['price'])
|
|
||||||
self.webhook_url = data['webhook_url']
|
|
||||||
self.id = data['slug']
|
|
||||||
|
|
||||||
agent = AgentCompat(agent_data)
|
|
||||||
|
|
||||||
# Check if user has sufficient balance
|
# Check if user has sufficient balance
|
||||||
if not request.user.has_sufficient_balance(agent.price):
|
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.')
|
messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the Career Navigator.')
|
||||||
return redirect('wallet:wallet')
|
return redirect('wallet:wallet')
|
||||||
|
|
||||||
# Deduct fee from user wallet
|
# Deduct fee from user wallet
|
||||||
success = request.user.deduct_balance(
|
success = request.user.deduct_balance(
|
||||||
agent.price,
|
agent_price,
|
||||||
f'{agent.name} - Direct Access',
|
f'{agent_data["name"]} - Direct Access',
|
||||||
agent.slug
|
agent_data['slug']
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
messages.error(request, 'Failed to process payment. Please try again.')
|
messages.error(request, 'Failed to process payment. Please try again.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Get or create database record for foreign key compatibility
|
|
||||||
agent_db_record = AgentFileService.get_or_create_agent_db_record(agent_data)
|
|
||||||
|
|
||||||
# Create execution record for tracking
|
# Create execution record for tracking
|
||||||
execution = AgentExecution.objects.create(
|
execution = AgentExecution.objects.create(
|
||||||
agent=agent_db_record,
|
agent_slug=agent_data['slug'],
|
||||||
|
agent_name=agent_data['name'],
|
||||||
user=request.user,
|
user=request.user,
|
||||||
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
||||||
fee_charged=agent.price,
|
fee_charged=agent_price,
|
||||||
status='completed',
|
status='completed',
|
||||||
output_data={
|
output_data={
|
||||||
'type': 'direct_access',
|
'type': 'direct_access',
|
||||||
'message': f'Direct access granted to {agent.name}',
|
'message': f'Direct access granted to {agent_data["name"]}',
|
||||||
'access_method': 'try_now_button'
|
'access_method': 'try_now_button'
|
||||||
},
|
},
|
||||||
completed_at=timezone.now()
|
completed_at=timezone.now()
|
||||||
@ -271,7 +260,7 @@ def career_navigator_view(request):
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
recent_execution = AgentExecution.objects.filter(
|
recent_execution = AgentExecution.objects.filter(
|
||||||
agent__slug=agent.slug, # Changed to slug-based lookup
|
agent_slug=agent.slug, # Changed to slug-based lookup
|
||||||
user=request.user,
|
user=request.user,
|
||||||
status='completed',
|
status='completed',
|
||||||
created_at__gte=timezone.now() - timedelta(hours=2)
|
created_at__gte=timezone.now() - timedelta(hours=2)
|
||||||
@ -324,7 +313,7 @@ def ai_brand_strategist_view(request):
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
recent_execution = AgentExecution.objects.filter(
|
recent_execution = AgentExecution.objects.filter(
|
||||||
agent__slug=agent.slug, # Changed to slug-based lookup
|
agent_slug=agent.slug, # Changed to slug-based lookup
|
||||||
user=request.user,
|
user=request.user,
|
||||||
status='completed',
|
status='completed',
|
||||||
created_at__gte=timezone.now() - timedelta(hours=2)
|
created_at__gte=timezone.now() - timedelta(hours=2)
|
||||||
@ -361,46 +350,35 @@ def ai_brand_strategist_access(request):
|
|||||||
messages.error(request, 'AI Brand Strategist is currently unavailable.')
|
messages.error(request, 'AI Brand Strategist is currently unavailable.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Convert to compatible object
|
agent_price = float(agent_data['price'])
|
||||||
class AgentCompat:
|
|
||||||
def __init__(self, data):
|
|
||||||
self.slug = data['slug']
|
|
||||||
self.name = data['name']
|
|
||||||
self.price = float(data['price'])
|
|
||||||
self.webhook_url = data['webhook_url']
|
|
||||||
self.id = data['slug']
|
|
||||||
|
|
||||||
agent = AgentCompat(agent_data)
|
|
||||||
|
|
||||||
# Check if user has sufficient balance
|
# Check if user has sufficient balance
|
||||||
if not request.user.has_sufficient_balance(agent.price):
|
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.')
|
messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the AI Brand Strategist.')
|
||||||
return redirect('wallet:wallet')
|
return redirect('wallet:wallet')
|
||||||
|
|
||||||
# Deduct fee from user wallet
|
# Deduct fee from user wallet
|
||||||
success = request.user.deduct_balance(
|
success = request.user.deduct_balance(
|
||||||
agent.price,
|
agent_price,
|
||||||
f'{agent.name} - Direct Access',
|
f'{agent_data["name"]} - Direct Access',
|
||||||
agent.slug
|
agent_data['slug']
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
messages.error(request, 'Failed to process payment. Please try again.')
|
messages.error(request, 'Failed to process payment. Please try again.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Get or create database record for foreign key compatibility
|
|
||||||
agent_db_record = AgentFileService.get_or_create_agent_db_record(agent_data)
|
|
||||||
|
|
||||||
# Create execution record for tracking
|
# Create execution record for tracking
|
||||||
execution = AgentExecution.objects.create(
|
execution = AgentExecution.objects.create(
|
||||||
agent=agent_db_record,
|
agent_slug=agent_data['slug'],
|
||||||
|
agent_name=agent_data['name'],
|
||||||
user=request.user,
|
user=request.user,
|
||||||
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
||||||
fee_charged=agent.price,
|
fee_charged=agent_price,
|
||||||
status='completed',
|
status='completed',
|
||||||
output_data={
|
output_data={
|
||||||
'type': 'direct_access',
|
'type': 'direct_access',
|
||||||
'message': f'Direct access granted to {agent.name}',
|
'message': f'Direct access granted to {agent_data["name"]}',
|
||||||
'access_method': 'try_now_button'
|
'access_method': 'try_now_button'
|
||||||
},
|
},
|
||||||
completed_at=timezone.now()
|
completed_at=timezone.now()
|
||||||
@ -534,7 +512,7 @@ def chat_agent_view(request, agent):
|
|||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
# Get or create active chat session (using slug-based filter for file agents)
|
# Get or create active chat session (using slug-based filter for file agents)
|
||||||
chat_session = ChatSession.objects.filter(
|
chat_session = ChatSession.objects.filter(
|
||||||
agent__slug=agent_compat.slug, # Changed to slug-based lookup
|
agent_slug=agent_compat.slug, # Changed to slug-based lookup
|
||||||
user=request.user,
|
user=request.user,
|
||||||
status='active'
|
status='active'
|
||||||
).first()
|
).first()
|
||||||
@ -544,7 +522,7 @@ def chat_agent_view(request, agent):
|
|||||||
if session_id and not chat_session:
|
if session_id and not chat_session:
|
||||||
chat_session = ChatSession.objects.filter(
|
chat_session = ChatSession.objects.filter(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
agent__slug=agent_compat.slug, # Changed to slug-based lookup
|
agent_slug=agent_compat.slug, # Changed to slug-based lookup
|
||||||
user=request.user
|
user=request.user
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
@ -557,7 +535,7 @@ def chat_agent_view(request, agent):
|
|||||||
|
|
||||||
# Get previous sessions for this user and agent (excluding current active session)
|
# Get previous sessions for this user and agent (excluding current active session)
|
||||||
previous_sessions_query = ChatSession.objects.filter(
|
previous_sessions_query = ChatSession.objects.filter(
|
||||||
agent__slug=agent_compat.slug, # Changed to slug-based lookup
|
agent_slug=agent_compat.slug, # Changed to slug-based lookup
|
||||||
user=request.user
|
user=request.user
|
||||||
).exclude(status='active').order_by('-created_at')[:5] # Last 5 non-active sessions
|
).exclude(status='active').order_by('-created_at')[:5] # Last 5 non-active sessions
|
||||||
|
|
||||||
@ -632,24 +610,15 @@ def start_chat_session(request):
|
|||||||
if not agent_data or not agent_data.get('is_active', True) or agent_data.get('agent_type') != 'chat':
|
if not agent_data or not agent_data.get('is_active', True) or agent_data.get('agent_type') != 'chat':
|
||||||
return Response({'error': 'Chat agent not found'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': 'Chat agent not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# Convert to compatible object
|
agent_price = float(agent_data['price'])
|
||||||
class AgentCompat:
|
|
||||||
def __init__(self, data):
|
|
||||||
self.slug = data['slug']
|
|
||||||
self.name = data['name']
|
|
||||||
self.price = float(data['price'])
|
|
||||||
self.webhook_url = data['webhook_url']
|
|
||||||
self.id = data['slug']
|
|
||||||
|
|
||||||
agent = AgentCompat(agent_data)
|
|
||||||
|
|
||||||
# Check wallet balance
|
# Check wallet balance
|
||||||
if hasattr(request.user, 'wallet_balance') and request.user.wallet_balance < agent.price:
|
if hasattr(request.user, 'wallet_balance') and request.user.wallet_balance < agent_price:
|
||||||
return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Check for existing active session (using slug-based lookup)
|
# Check for existing active session (using slug-based lookup)
|
||||||
existing_session = ChatSession.objects.filter(
|
existing_session = ChatSession.objects.filter(
|
||||||
agent__slug=agent.slug,
|
agent_slug=agent_data['slug'],
|
||||||
user=request.user,
|
user=request.user,
|
||||||
status='active'
|
status='active'
|
||||||
).first()
|
).first()
|
||||||
@ -666,14 +635,12 @@ def start_chat_session(request):
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
# Get or create database record for foreign key compatibility
|
|
||||||
agent_db_record = AgentFileService.get_or_create_agent_db_record(agent_data)
|
|
||||||
|
|
||||||
chat_session = ChatSession.objects.create(
|
chat_session = ChatSession.objects.create(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
agent=agent_db_record,
|
agent_slug=agent_data['slug'],
|
||||||
|
agent_name=agent_data['name'],
|
||||||
user=request.user,
|
user=request.user,
|
||||||
fee_charged=agent.price,
|
fee_charged=agent_price,
|
||||||
status='active',
|
status='active',
|
||||||
expires_at=timezone.now() + timedelta(minutes=30)
|
expires_at=timezone.now() + timedelta(minutes=30)
|
||||||
)
|
)
|
||||||
@ -681,9 +648,9 @@ def start_chat_session(request):
|
|||||||
# Deduct fee from wallet
|
# Deduct fee from wallet
|
||||||
try:
|
try:
|
||||||
success = request.user.deduct_balance(
|
success = request.user.deduct_balance(
|
||||||
agent.price,
|
agent_price,
|
||||||
f'{agent.name} - Chat Session {session_id}',
|
f'{agent_data["name"]} - Chat Session {session_id}',
|
||||||
agent.slug
|
agent_data['slug']
|
||||||
)
|
)
|
||||||
if not success:
|
if not success:
|
||||||
# Delete the created session if payment fails
|
# Delete the created session if payment fails
|
||||||
@ -699,7 +666,7 @@ def start_chat_session(request):
|
|||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
# Send welcome message
|
# Send welcome message
|
||||||
welcome_message = f"""## Welcome to {agent.name}! 🔍
|
welcome_message = f"""## Welcome to {agent_data["name"]}! 🔍
|
||||||
|
|
||||||
I'm here to guide you through the **5 Whys methodology** - a powerful problem-solving technique to uncover root causes.
|
I'm here to guide you through the **5 Whys methodology** - a powerful problem-solving technique to uncover root causes.
|
||||||
|
|
||||||
@ -752,16 +719,20 @@ def send_chat_message(request):
|
|||||||
chat_session.save()
|
chat_session.save()
|
||||||
return Response({'error': 'Chat session has expired'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'Chat session has expired'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Get agent data for message limit
|
||||||
|
agent_data = AgentFileService.get_agent_by_slug(chat_session.agent_slug)
|
||||||
|
message_limit = agent_data.get('message_limit', 50) if agent_data else 50
|
||||||
|
|
||||||
# Check message limit (only count user messages)
|
# Check message limit (only count user messages)
|
||||||
current_user_message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count()
|
current_user_message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count()
|
||||||
if current_user_message_count >= chat_session.agent.message_limit:
|
if current_user_message_count >= message_limit:
|
||||||
# Auto-complete the session when message limit is reached
|
# Auto-complete the session when message limit is reached
|
||||||
chat_session.status = 'completed'
|
chat_session.status = 'completed'
|
||||||
chat_session.completed_at = timezone.now()
|
chat_session.completed_at = timezone.now()
|
||||||
chat_session.save()
|
chat_session.save()
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'error': f'Message limit reached ({chat_session.agent.message_limit} messages). Session completed. You can download your conversation or start a new session.'
|
'error': f'Message limit reached ({message_limit} messages). Session completed. You can download your conversation or start a new session.'
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Save user message
|
# Save user message
|
||||||
@ -796,17 +767,22 @@ CONTENT GUIDELINES:
|
|||||||
},
|
},
|
||||||
"sessionId": session_id,
|
"sessionId": session_id,
|
||||||
"userId": str(request.user.id),
|
"userId": str(request.user.id),
|
||||||
"agentId": chat_session.agent.slug,
|
"agentId": chat_session.agent_slug,
|
||||||
"messageType": "chat"
|
"messageType": "chat"
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Get webhook URL from agent data
|
||||||
|
webhook_url = agent_data['webhook_url'] if agent_data else None
|
||||||
|
if not webhook_url:
|
||||||
|
raise ValueError("Agent webhook URL not found")
|
||||||
|
|
||||||
# Validate webhook URL
|
# Validate webhook URL
|
||||||
validate_webhook_url(chat_session.agent.webhook_url)
|
validate_webhook_url(webhook_url)
|
||||||
|
|
||||||
# Send to webhook
|
# Send to webhook
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
chat_session.agent.webhook_url,
|
webhook_url,
|
||||||
json=webhook_payload,
|
json=webhook_payload,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
headers={'Content-Type': 'application/json'}
|
headers={'Content-Type': 'application/json'}
|
||||||
@ -994,9 +970,12 @@ def get_session_status(request, session_id):
|
|||||||
total_session_time = 30 * 60 # 30 minutes in seconds
|
total_session_time = 30 * 60 # 30 minutes in seconds
|
||||||
time_percentage = max(0, min(100, (time_remaining_seconds / total_session_time) * 100))
|
time_percentage = max(0, min(100, (time_remaining_seconds / total_session_time) * 100))
|
||||||
|
|
||||||
|
# Get agent data for message limit
|
||||||
|
agent_data = AgentFileService.get_agent_by_slug(chat_session.agent_slug)
|
||||||
|
message_limit = agent_data.get('message_limit', 50) if agent_data else 50
|
||||||
|
|
||||||
# Message calculations (only count user messages)
|
# Message calculations (only count user messages)
|
||||||
message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count()
|
message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count()
|
||||||
message_limit = chat_session.agent.message_limit
|
|
||||||
message_percentage = min(100, (message_count / message_limit) * 100)
|
message_percentage = min(100, (message_count / message_limit) * 100)
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
@ -1055,14 +1034,14 @@ def export_chat_pdf(chat_session, messages):
|
|||||||
alignment=1 # Center alignment
|
alignment=1 # Center alignment
|
||||||
)
|
)
|
||||||
|
|
||||||
story.append(Paragraph(f"5 Whys Analysis - {chat_session.agent.name}", title_style))
|
story.append(Paragraph(f"5 Whys Analysis - {chat_session.agent_name}", title_style))
|
||||||
story.append(Spacer(1, 12))
|
story.append(Spacer(1, 12))
|
||||||
|
|
||||||
# Session info
|
# Session info
|
||||||
info_style = styles['Normal']
|
info_style = styles['Normal']
|
||||||
story.append(Paragraph(f"<b>Session ID:</b> {chat_session.session_id}", info_style))
|
story.append(Paragraph(f"<b>Session ID:</b> {chat_session.session_id}", info_style))
|
||||||
story.append(Paragraph(f"<b>Date:</b> {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}", info_style))
|
story.append(Paragraph(f"<b>Date:</b> {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}", info_style))
|
||||||
story.append(Paragraph(f"<b>Agent:</b> {chat_session.agent.name}", info_style))
|
story.append(Paragraph(f"<b>Agent:</b> {chat_session.agent_name}", info_style))
|
||||||
story.append(Paragraph(f"<b>Total Messages:</b> {messages.count()}", info_style))
|
story.append(Paragraph(f"<b>Total Messages:</b> {messages.count()}", info_style))
|
||||||
story.append(Spacer(1, 20))
|
story.append(Spacer(1, 20))
|
||||||
|
|
||||||
@ -1093,7 +1072,7 @@ def export_chat_pdf(chat_session, messages):
|
|||||||
if message.message_type == 'user':
|
if message.message_type == 'user':
|
||||||
story.append(Paragraph(f"<b>You ({timestamp}):</b><br/>{message.content}", user_style))
|
story.append(Paragraph(f"<b>You ({timestamp}):</b><br/>{message.content}", user_style))
|
||||||
elif message.message_type == 'agent':
|
elif message.message_type == 'agent':
|
||||||
story.append(Paragraph(f"<b>{chat_session.agent.name} ({timestamp}):</b><br/>{message.content}", agent_style))
|
story.append(Paragraph(f"<b>{chat_session.agent_name} ({timestamp}):</b><br/>{message.content}", agent_style))
|
||||||
elif message.message_type == 'system':
|
elif message.message_type == 'system':
|
||||||
story.append(Paragraph(f"<i>System ({timestamp}): {message.content}</i>", styles['Normal']))
|
story.append(Paragraph(f"<i>System ({timestamp}): {message.content}</i>", styles['Normal']))
|
||||||
|
|
||||||
@ -1110,12 +1089,12 @@ def export_chat_txt(chat_session, messages):
|
|||||||
"""Generate TXT export of chat session"""
|
"""Generate TXT export of chat session"""
|
||||||
content = []
|
content = []
|
||||||
content.append("=" * 60)
|
content.append("=" * 60)
|
||||||
content.append(f"5 Whys Analysis - {chat_session.agent.name}")
|
content.append(f"5 Whys Analysis - {chat_session.agent_name}")
|
||||||
content.append("=" * 60)
|
content.append("=" * 60)
|
||||||
content.append("")
|
content.append("")
|
||||||
content.append(f"Session ID: {chat_session.session_id}")
|
content.append(f"Session ID: {chat_session.session_id}")
|
||||||
content.append(f"Date: {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}")
|
content.append(f"Date: {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}")
|
||||||
content.append(f"Agent: {chat_session.agent.name}")
|
content.append(f"Agent: {chat_session.agent_name}")
|
||||||
content.append(f"Total Messages: {messages.count()}")
|
content.append(f"Total Messages: {messages.count()}")
|
||||||
content.append("")
|
content.append("")
|
||||||
content.append("-" * 60)
|
content.append("-" * 60)
|
||||||
@ -1130,7 +1109,7 @@ def export_chat_txt(chat_session, messages):
|
|||||||
content.append(f"You ({timestamp}):")
|
content.append(f"You ({timestamp}):")
|
||||||
content.append(message.content)
|
content.append(message.content)
|
||||||
elif message.message_type == 'agent':
|
elif message.message_type == 'agent':
|
||||||
content.append(f"{chat_session.agent.name} ({timestamp}):")
|
content.append(f"{chat_session.agent_name} ({timestamp}):")
|
||||||
content.append(message.content)
|
content.append(message.content)
|
||||||
elif message.message_type == 'system':
|
elif message.message_type == 'system':
|
||||||
content.append(f"System ({timestamp}): {message.content}")
|
content.append(f"System ({timestamp}): {message.content}")
|
||||||
@ -1179,10 +1158,10 @@ def direct_access_handler(request, slug):
|
|||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Handle payment for paid agents
|
# Handle payment for paid agents
|
||||||
if agent.price > 0:
|
if agent_price > 0:
|
||||||
user_balance = request.user.wallet_balance
|
user_balance = request.user.wallet_balance
|
||||||
if user_balance < agent.price:
|
if user_balance < agent_price:
|
||||||
messages.error(request, f'Insufficient balance. You need {agent.price} AED but have {user_balance} AED.')
|
messages.error(request, f'Insufficient balance. You need {agent_price} AED but have {user_balance} AED.')
|
||||||
return redirect('wallet:wallet')
|
return redirect('wallet:wallet')
|
||||||
|
|
||||||
# Process payment
|
# Process payment
|
||||||
@ -1190,7 +1169,7 @@ def direct_access_handler(request, slug):
|
|||||||
from wallet.models import WalletTransaction
|
from wallet.models import WalletTransaction
|
||||||
WalletTransaction.objects.create(
|
WalletTransaction.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
amount=-agent.price,
|
amount=-agent_price,
|
||||||
type='agent_usage',
|
type='agent_usage',
|
||||||
description=f'Payment for {agent.name}',
|
description=f'Payment for {agent.name}',
|
||||||
agent_slug=agent.slug
|
agent_slug=agent.slug
|
||||||
@ -1270,7 +1249,7 @@ def lean_six_sigma_expert_view(request):
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
recent_execution = AgentExecution.objects.filter(
|
recent_execution = AgentExecution.objects.filter(
|
||||||
agent__slug=agent.slug, # Changed to slug-based lookup
|
agent_slug=agent.slug, # Changed to slug-based lookup
|
||||||
user=request.user,
|
user=request.user,
|
||||||
status='completed',
|
status='completed',
|
||||||
created_at__gte=timezone.now() - timedelta(hours=2)
|
created_at__gte=timezone.now() - timedelta(hours=2)
|
||||||
@ -1307,46 +1286,35 @@ def lean_six_sigma_expert_access(request):
|
|||||||
messages.error(request, 'Lean Six Sigma Expert is currently unavailable.')
|
messages.error(request, 'Lean Six Sigma Expert is currently unavailable.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Convert to compatible object
|
agent_price = float(agent_data['price'])
|
||||||
class AgentCompat:
|
|
||||||
def __init__(self, data):
|
|
||||||
self.slug = data['slug']
|
|
||||||
self.name = data['name']
|
|
||||||
self.price = float(data['price'])
|
|
||||||
self.webhook_url = data['webhook_url']
|
|
||||||
self.id = data['slug']
|
|
||||||
|
|
||||||
agent = AgentCompat(agent_data)
|
|
||||||
|
|
||||||
# Check if user has sufficient balance
|
# Check if user has sufficient balance
|
||||||
if not request.user.has_sufficient_balance(agent.price):
|
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.')
|
messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the Lean Six Sigma Expert.')
|
||||||
return redirect('wallet:wallet')
|
return redirect('wallet:wallet')
|
||||||
|
|
||||||
# Deduct fee from user wallet
|
# Deduct fee from user wallet
|
||||||
success = request.user.deduct_balance(
|
success = request.user.deduct_balance(
|
||||||
agent.price,
|
agent_price,
|
||||||
f'{agent.name} - Direct Access',
|
f'{agent_data["name"]} - Direct Access',
|
||||||
agent.slug
|
agent_data['slug']
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
messages.error(request, 'Failed to process payment. Please try again.')
|
messages.error(request, 'Failed to process payment. Please try again.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Get or create database record for foreign key compatibility
|
|
||||||
agent_db_record = AgentFileService.get_or_create_agent_db_record(agent_data)
|
|
||||||
|
|
||||||
# Create execution record for tracking
|
# Create execution record for tracking
|
||||||
execution = AgentExecution.objects.create(
|
execution = AgentExecution.objects.create(
|
||||||
agent=agent_db_record,
|
agent_slug=agent_data['slug'],
|
||||||
|
agent_name=agent_data['name'],
|
||||||
user=request.user,
|
user=request.user,
|
||||||
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
||||||
fee_charged=agent.price,
|
fee_charged=agent_price,
|
||||||
status='completed',
|
status='completed',
|
||||||
output_data={
|
output_data={
|
||||||
'type': 'direct_access',
|
'type': 'direct_access',
|
||||||
'message': f'Direct access granted to {agent.name}',
|
'message': f'Direct access granted to {agent_data["name"]}',
|
||||||
'access_method': 'try_now_button'
|
'access_method': 'try_now_button'
|
||||||
},
|
},
|
||||||
completed_at=timezone.now()
|
completed_at=timezone.now()
|
||||||
@ -1389,7 +1357,7 @@ def swot_analysis_expert_view(request):
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
recent_execution = AgentExecution.objects.filter(
|
recent_execution = AgentExecution.objects.filter(
|
||||||
agent__slug=agent.slug, # Changed to slug-based lookup
|
agent_slug=agent.slug, # Changed to slug-based lookup
|
||||||
user=request.user,
|
user=request.user,
|
||||||
status='completed',
|
status='completed',
|
||||||
created_at__gte=timezone.now() - timedelta(hours=2)
|
created_at__gte=timezone.now() - timedelta(hours=2)
|
||||||
@ -1426,46 +1394,35 @@ def swot_analysis_expert_access(request):
|
|||||||
messages.error(request, 'SWOT Analysis Expert is currently unavailable.')
|
messages.error(request, 'SWOT Analysis Expert is currently unavailable.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Convert to compatible object
|
agent_price = float(agent_data['price'])
|
||||||
class AgentCompat:
|
|
||||||
def __init__(self, data):
|
|
||||||
self.slug = data['slug']
|
|
||||||
self.name = data['name']
|
|
||||||
self.price = float(data['price'])
|
|
||||||
self.webhook_url = data['webhook_url']
|
|
||||||
self.id = data['slug']
|
|
||||||
|
|
||||||
agent = AgentCompat(agent_data)
|
|
||||||
|
|
||||||
# Check if user has sufficient balance
|
# Check if user has sufficient balance
|
||||||
if not request.user.has_sufficient_balance(agent.price):
|
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.')
|
messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the SWOT Analysis Expert.')
|
||||||
return redirect('wallet:wallet')
|
return redirect('wallet:wallet')
|
||||||
|
|
||||||
# Deduct fee from user wallet
|
# Deduct fee from user wallet
|
||||||
success = request.user.deduct_balance(
|
success = request.user.deduct_balance(
|
||||||
agent.price,
|
agent_price,
|
||||||
f'{agent.name} - Direct Access',
|
f'{agent_data["name"]} - Direct Access',
|
||||||
agent.slug
|
agent_data['slug']
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
messages.error(request, 'Failed to process payment. Please try again.')
|
messages.error(request, 'Failed to process payment. Please try again.')
|
||||||
return redirect('agents:marketplace')
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
# Get or create database record for foreign key compatibility
|
|
||||||
agent_db_record = AgentFileService.get_or_create_agent_db_record(agent_data)
|
|
||||||
|
|
||||||
# Create execution record for tracking
|
# Create execution record for tracking
|
||||||
execution = AgentExecution.objects.create(
|
execution = AgentExecution.objects.create(
|
||||||
agent=agent_db_record,
|
agent_slug=agent_data['slug'],
|
||||||
|
agent_name=agent_data['name'],
|
||||||
user=request.user,
|
user=request.user,
|
||||||
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
input_data={'action': 'direct_access', 'source': 'try_now_button'},
|
||||||
fee_charged=agent.price,
|
fee_charged=agent_price,
|
||||||
status='completed',
|
status='completed',
|
||||||
output_data={
|
output_data={
|
||||||
'type': 'direct_access',
|
'type': 'direct_access',
|
||||||
'message': f'Direct access granted to {agent.name}',
|
'message': f'Direct access granted to {agent_data["name"]}',
|
||||||
'access_method': 'try_now_button'
|
'access_method': 'try_now_button'
|
||||||
},
|
},
|
||||||
completed_at=timezone.now()
|
completed_at=timezone.now()
|
||||||
|
|||||||
@ -180,16 +180,23 @@ class AgentsCore extends WorkflowsCore {
|
|||||||
// Process successful execution
|
// Process successful execution
|
||||||
this.constructor.hideProcessing();
|
this.constructor.hideProcessing();
|
||||||
|
|
||||||
// Update wallet balance if fee was charged
|
// Update wallet balance ONLY after successful AI execution
|
||||||
if (data.fee_charged) {
|
if (data.fee_charged) {
|
||||||
const currentBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0');
|
const currentBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0');
|
||||||
const newBalance = currentBalance - parseFloat(data.fee_charged);
|
const newBalance = currentBalance - parseFloat(data.fee_charged);
|
||||||
|
|
||||||
|
console.log('Charging wallet after successful execution:', {
|
||||||
|
currentBalance,
|
||||||
|
feeCharged: data.fee_charged,
|
||||||
|
newBalance,
|
||||||
|
executionStatus: data.status
|
||||||
|
});
|
||||||
|
|
||||||
// Update the wallet balance display
|
// Update the wallet balance display
|
||||||
this.constructor.updateWalletBalance(newBalance);
|
this.constructor.updateWalletBalance(newBalance);
|
||||||
|
|
||||||
// Update the data attribute for future calculations
|
// Show notification about successful charge
|
||||||
document.body.setAttribute('data-user-balance', newBalance.toString());
|
this.constructor.showToast(`💰 Charged ${data.fee_charged} AED - Service completed!`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display results
|
// Display results
|
||||||
@ -222,362 +229,319 @@ class AgentsCore extends WorkflowsCore {
|
|||||||
if (data.new_balance !== undefined) {
|
if (data.new_balance !== undefined) {
|
||||||
// Update wallet balance display
|
// Update wallet balance display
|
||||||
this.constructor.updateWalletBalance(data.new_balance);
|
this.constructor.updateWalletBalance(data.new_balance);
|
||||||
document.body.setAttribute('data-user-balance', data.new_balance.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Wallet deduction error:', error);
|
console.error('Wallet deduction error:', error);
|
||||||
// Continue execution even if wallet update fails
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display results from file processing
|
* Display execution results
|
||||||
*/
|
*/
|
||||||
displayFileProcessingResults(data) {
|
displayExecutionResults(data) {
|
||||||
const resultsContainer = document.getElementById('resultsContainer');
|
const resultsContainer = document.getElementById('resultsContainer');
|
||||||
const resultsContent = document.getElementById('resultsContent');
|
const resultsContent = document.getElementById('resultsContent');
|
||||||
|
|
||||||
if (!resultsContainer || !resultsContent) return;
|
if (!resultsContainer || !resultsContent) {
|
||||||
|
console.log('Results containers not found');
|
||||||
let content = '';
|
|
||||||
|
|
||||||
// Handle different response formats from file processing
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
if (data.sections && Array.isArray(data.sections)) {
|
|
||||||
// Multi-section response (array format)
|
|
||||||
content = data.sections.map(section => {
|
|
||||||
const heading = section.heading || 'Section';
|
|
||||||
const sectionContent = section.content || '';
|
|
||||||
return `## ${heading}\n\n${sectionContent}`;
|
|
||||||
}).join('\n\n');
|
|
||||||
} else if (data.sections && typeof data.sections === 'object') {
|
|
||||||
// Multi-section response (object format)
|
|
||||||
content = Object.entries(data.sections).map(([section, text]) => {
|
|
||||||
return `## ${section.replace('_', ' ').toUpperCase()}\n\n${text}`;
|
|
||||||
}).join('\n\n');
|
|
||||||
} else if (data.output || data.result || data.summary) {
|
|
||||||
content = data.output || data.result || data.summary;
|
|
||||||
} else if (data.error) {
|
|
||||||
content = `Error: ${data.error}`;
|
|
||||||
} else {
|
|
||||||
content = JSON.stringify(data, null, 2);
|
|
||||||
}
|
|
||||||
} else if (typeof data === 'string') {
|
|
||||||
content = data;
|
|
||||||
} else {
|
|
||||||
content = 'File processed 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display results from agent execution
|
|
||||||
*/
|
|
||||||
displayExecutionResults(executionData) {
|
|
||||||
const resultsContainer = document.getElementById('resultsContainer');
|
|
||||||
const resultsContent = document.getElementById('resultsContent');
|
|
||||||
|
|
||||||
if (!resultsContainer || !resultsContent) return;
|
|
||||||
|
|
||||||
let content = '';
|
|
||||||
|
|
||||||
// Handle different response formats
|
|
||||||
if (executionData.output_data && typeof executionData.output_data === 'object') {
|
|
||||||
// Handle N8N response formats
|
|
||||||
const output = executionData.output_data;
|
|
||||||
content = output.output || output.text || output.content || output.result || output.message || JSON.stringify(output, null, 2);
|
|
||||||
} else if (executionData.output_data && typeof executionData.output_data === 'string') {
|
|
||||||
content = executionData.output_data;
|
|
||||||
} else {
|
|
||||||
content = `Agent executed successfully!\n\nExecution ID: ${executionData.id}\nStatus: ${executionData.status}\nFee Charged: ${executionData.fee_charged} AED`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create wrapper div
|
// Clear previous results
|
||||||
const wrapper = document.createElement('div');
|
resultsContent.innerHTML = '';
|
||||||
wrapper.className = 'results-content';
|
|
||||||
|
|
||||||
// Split content into lines and process safely
|
// Extract output content with better format handling and debugging
|
||||||
const lines = content.split('\n');
|
let content = '';
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
// Add debugging to see what we're getting
|
||||||
const line = lines[i].trim();
|
console.log('=== DEBUGGING EXECUTION RESULTS ===');
|
||||||
|
console.log('Full data object:', data);
|
||||||
|
console.log('output_data type:', typeof data.output_data);
|
||||||
|
console.log('output_data content:', data.output_data);
|
||||||
|
console.log('Agent slug:', this.agentSlug);
|
||||||
|
|
||||||
if (!line) {
|
// Check for PDF analyzer direct response format FIRST
|
||||||
// Add line break for empty lines
|
if (data.sections && Array.isArray(data.sections)) {
|
||||||
if (i > 0) wrapper.appendChild(document.createElement('br'));
|
console.log('Using PDF direct response format');
|
||||||
continue;
|
content = this.formatPDFAnalysisResults(data.sections);
|
||||||
}
|
}
|
||||||
|
// Check for PDF analyzer array response format
|
||||||
let element;
|
else if (Array.isArray(data) && data[0] && data[0].sections) {
|
||||||
|
console.log('Using PDF array response format');
|
||||||
// Handle headers (but escape content)
|
content = this.formatPDFAnalysisResults(data[0].sections);
|
||||||
if (line.startsWith('### ')) {
|
}
|
||||||
element = document.createElement('h3');
|
// Then check for standard output_data format
|
||||||
element.textContent = line.substring(4);
|
else if (data.output_data && typeof data.output_data === 'object') {
|
||||||
} else if (line.startsWith('## ')) {
|
// Handle PDF analyzer nested response format
|
||||||
element = document.createElement('h2');
|
if (Array.isArray(data.output_data) && data.output_data[0] && data.output_data[0].sections) {
|
||||||
element.textContent = line.substring(3);
|
console.log('Using PDF nested analysis format');
|
||||||
} else if (line.startsWith('# ')) {
|
content = this.formatPDFAnalysisResults(data.output_data[0].sections);
|
||||||
element = document.createElement('h1');
|
}
|
||||||
element.textContent = line.substring(2);
|
// Handle standard webhook response format
|
||||||
|
else if (data.output_data.output) {
|
||||||
|
console.log('Using standard output format');
|
||||||
|
// Check if this is a job posting and format it specially
|
||||||
|
if (this.agentSlug === 'job-posting-generator') {
|
||||||
|
content = this.formatJobPostingResults(data.output_data.output);
|
||||||
} else {
|
} else {
|
||||||
// Handle regular text with basic formatting
|
content = data.output_data.output;
|
||||||
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'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Handle other response formats
|
||||||
|
else if (data.output_data.result || data.output_data.content) {
|
||||||
|
console.log('Using result/content format');
|
||||||
|
content = data.output_data.result || data.output_data.content;
|
||||||
|
}
|
||||||
|
// Show the actual data structure instead of generic message
|
||||||
|
else {
|
||||||
|
console.log('Using JSON fallback format');
|
||||||
|
content = `<div style="background: #f5f5f5; padding: 15px; border-radius: 5px; font-family: monospace; white-space: pre-wrap;">${JSON.stringify(data.output_data, null, 2)}</div>`;
|
||||||
|
}
|
||||||
|
} else if (data.output_data) {
|
||||||
|
console.log('Using string format');
|
||||||
|
content = data.output_data.toString();
|
||||||
|
} else {
|
||||||
|
console.log('No output_data found - using fallback');
|
||||||
|
// Show the full response to debug what's missing
|
||||||
|
content = `<div style="background: #fff3cd; padding: 15px; border: 1px solid #ffeaa7; border-radius: 5px;">
|
||||||
|
<h4>Execution Details:</h4>
|
||||||
|
<p><strong>Status:</strong> ${data.status || 'unknown'}</p>
|
||||||
|
<p><strong>Agent:</strong> ${this.agentSlug}</p>
|
||||||
|
<p><strong>Execution ID:</strong> ${data.id || 'unknown'}</p>
|
||||||
|
<p><strong>Error:</strong> ${data.error_message || 'No error message'}</p>
|
||||||
|
<details>
|
||||||
|
<summary>Full Response Data</summary>
|
||||||
|
<pre style="background: #f8f9fa; padding: 10px; border-radius: 3px; overflow-x: auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</details>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
console.log('Final content length:', content.length);
|
||||||
|
console.log('=====================================');
|
||||||
|
|
||||||
container.appendChild(wrapper);
|
// Create content element
|
||||||
|
const contentDiv = document.createElement('div');
|
||||||
|
contentDiv.className = 'results-content';
|
||||||
|
|
||||||
|
// Use innerHTML for formatted PDF results, textContent for others
|
||||||
|
if (content.includes('<h') || content.includes('<div')) {
|
||||||
|
contentDiv.innerHTML = content;
|
||||||
|
} else {
|
||||||
|
contentDiv.textContent = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsContent.appendChild(contentDiv);
|
||||||
|
|
||||||
|
// Show results container
|
||||||
|
resultsContainer.style.display = 'block';
|
||||||
|
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
|
||||||
|
this.resetSubmitButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format text with basic styling while preventing XSS
|
* Format PDF analysis results - simple and clean
|
||||||
*/
|
*/
|
||||||
formatTextSecurely(element, text) {
|
formatPDFAnalysisResults(sections) {
|
||||||
// Simple approach: handle bold and italic formatting securely
|
let html = '<div class="simple-results">';
|
||||||
const parts = [];
|
|
||||||
let currentText = text;
|
|
||||||
|
|
||||||
// Process **bold** text
|
sections.forEach(section => {
|
||||||
currentText = currentText.replace(/\*\*(.*?)\*\*/g, (match, content) => {
|
let content = section.content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||||
const placeholder = `__BOLD_${parts.length}__`;
|
content = content.replace(/\n/g, '<br>');
|
||||||
parts.push({type: 'bold', content: content});
|
|
||||||
return placeholder;
|
html += `
|
||||||
|
<div class="result-section">
|
||||||
|
<h3>${section.heading}</h3>
|
||||||
|
<div>${content}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process *italic* text
|
html += '</div>';
|
||||||
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
|
// Simple, clean styling
|
||||||
const segments = currentText.split(/(__(?:BOLD|ITALIC)_\d+__)/);
|
html += `
|
||||||
|
<style>
|
||||||
segments.forEach(segment => {
|
.simple-results {
|
||||||
if (segment.startsWith('__BOLD_')) {
|
font-family: system-ui, sans-serif;
|
||||||
const index = parseInt(segment.match(/\d+/)[0]);
|
line-height: 1.5;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
});
|
.result-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.result-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.result-section h3 {
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.result-section div {
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.result-section strong {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize dynamic form validation based on form schema
|
* Process markdown-like content and convert to HTML
|
||||||
|
*/
|
||||||
|
processMarkdownContent(content) {
|
||||||
|
// Handle numbered lists (1. **Title**: Description)
|
||||||
|
content = content.replace(/(\d+)\.\s\*\*(.*?)\*\*:\s*(.*?)(?=\n\d+\.|\n-|$)/g,
|
||||||
|
'<ol><li><strong>$2</strong>: $3</li></ol>');
|
||||||
|
|
||||||
|
// Fix multiple consecutive ol tags
|
||||||
|
content = content.replace(/<\/ol>\s*<ol>/g, '');
|
||||||
|
|
||||||
|
// Handle bullet points (- **Title**: Description)
|
||||||
|
content = content.replace(/(?:^|\n)-\s\*\*(.*?)\*\*:\s*(.*?)(?=\n-|$)/g,
|
||||||
|
'<ul><li><strong>$1</strong>: $2</li></ul>');
|
||||||
|
|
||||||
|
// Fix multiple consecutive ul tags
|
||||||
|
content = content.replace(/<\/ul>\s*<ul>/g, '');
|
||||||
|
|
||||||
|
// Handle standalone numbered items without the list wrapper
|
||||||
|
content = content.replace(/(\d+)\.\s\*\*(.*?)\*\*:\s*(.*?)(?=\n|$)/g,
|
||||||
|
'<div class="numbered-item"><strong>$1. $2</strong>: $3</div>');
|
||||||
|
|
||||||
|
// Handle standalone bold items
|
||||||
|
content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
|
||||||
|
// Handle line breaks
|
||||||
|
content = content.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
// Clean up extra breaks around lists
|
||||||
|
content = content.replace(/<br>\s*<(ol|ul|div class="numbered-item")>/g, '<$1>');
|
||||||
|
content = content.replace(/<\/(ol|ul)>\s*<br>/g, '</$1>');
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format job posting results - simple and clean
|
||||||
|
*/
|
||||||
|
formatJobPostingResults(jobPostingText) {
|
||||||
|
// Just convert markdown bold to HTML and preserve line breaks
|
||||||
|
let content = jobPostingText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
content = content.replace(/\n\n/g, '<br><br>');
|
||||||
|
content = content.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="simple-job-posting">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.simple-job-posting {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.simple-job-posting strong {
|
||||||
|
color: #222;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display file processing results
|
||||||
|
*/
|
||||||
|
displayFileProcessingResults(data) {
|
||||||
|
// Use same method as regular execution results
|
||||||
|
this.displayExecutionResults(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic form validation based on form schema
|
||||||
*/
|
*/
|
||||||
initializeDynamicFormValidation() {
|
initializeDynamicFormValidation() {
|
||||||
const fields = document.querySelectorAll('#agentForm [name]');
|
const formGroups = document.querySelectorAll('.form-group');
|
||||||
|
|
||||||
fields.forEach(field => {
|
formGroups.forEach(group => {
|
||||||
const fieldName = field.getAttribute('name');
|
const field = group.querySelector('input, select, textarea');
|
||||||
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
|
if (field) {
|
||||||
field.addEventListener('blur', () => this.validateField(fieldName));
|
field.addEventListener('blur', () => this.validateField(field));
|
||||||
field.addEventListener('input', () => this.constructor.clearFieldError(fieldName));
|
field.addEventListener('input', () => this.constructor.clearFieldError(field.id));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validateField(fieldName) {
|
/**
|
||||||
const field = document.getElementById(fieldName);
|
* Validate individual field
|
||||||
if (!field) return true;
|
*/
|
||||||
|
validateField(field) {
|
||||||
|
const value = field.value.trim();
|
||||||
|
const isRequired = field.hasAttribute('required');
|
||||||
|
|
||||||
const value = field.type === 'checkbox' ? field.checked : field.value.trim();
|
if (isRequired && !value) {
|
||||||
const required = field.hasAttribute('required');
|
this.constructor.showFieldError(field.id, 'This field is required');
|
||||||
|
|
||||||
// Basic required field validation
|
|
||||||
if (required && (!value || value === '')) {
|
|
||||||
this.constructor.showFieldError(fieldName, `${fieldName.replace('_', ' ')} is required`);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specific validation based on field type
|
// Type-specific validation
|
||||||
if (field.type === 'textarea' && value && value.length < 10) {
|
if (field.type === 'email' && value && !this.isValidEmail(value)) {
|
||||||
this.constructor.showFieldError(fieldName, 'Please provide more detailed information (at least 10 characters)');
|
this.constructor.showFieldError(field.id, 'Please enter a valid email address');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'url' && value && !this.isValidURL(value)) {
|
if (field.type === 'url' && value && !this.isValidUrl(value)) {
|
||||||
this.constructor.showFieldError(fieldName, 'Please enter a valid URL');
|
this.constructor.showFieldError(field.id, 'Please enter a valid URL');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.constructor.clearFieldError(fieldName);
|
this.constructor.clearFieldError(field.id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidURL(string) {
|
/**
|
||||||
try {
|
* Check if form is valid
|
||||||
new URL(string);
|
*/
|
||||||
return true;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isFormValid() {
|
isFormValid() {
|
||||||
const fields = document.querySelectorAll('#agentForm [name]');
|
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
const formGroups = document.querySelectorAll('.form-group');
|
||||||
|
|
||||||
fields.forEach(field => {
|
formGroups.forEach(group => {
|
||||||
const fieldName = field.getAttribute('name');
|
const field = group.querySelector('input, select, textarea');
|
||||||
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
|
if (field && !this.validateField(field)) {
|
||||||
if (!this.validateField(fieldName)) {
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle JotForm agent execution (CyberSec Career Navigator)
|
* Email validation helper
|
||||||
*/
|
*/
|
||||||
async handleJotFormAgent() {
|
isValidEmail(email) {
|
||||||
// Check authentication and balance
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
if (!this.constructor.checkAuthentication()) return;
|
return emailRegex.test(email);
|
||||||
if (!this.constructor.checkBalance(this.price)) return;
|
|
||||||
|
|
||||||
const submitBtn = document.getElementById('generateBtn');
|
|
||||||
if (submitBtn) {
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
submitBtn.textContent = '⏳ Processing Payment...';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create execution record and charge wallet via form API
|
|
||||||
const response = await fetch('/agents/api/form/access/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
agent_slug: this.agentSlug
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
// Update wallet balance
|
|
||||||
this.constructor.updateWalletBalance(result.new_balance);
|
|
||||||
|
|
||||||
// Show white-label interface
|
|
||||||
this.showWhiteLabelInterface(result.interface_url);
|
|
||||||
|
|
||||||
this.constructor.showToast('✅ Payment processed! Access granted to Quantum AI Career Navigator', 'success');
|
|
||||||
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
throw new Error(error.error || 'Failed to process payment');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Form agent error:', error);
|
|
||||||
this.constructor.showToast(`❌ ${error.message}`, 'error');
|
|
||||||
this.resetSubmitButton();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show white-label interface in results container
|
* URL validation helper
|
||||||
*/
|
*/
|
||||||
showWhiteLabelInterface(interfaceUrl) {
|
isValidUrl(url) {
|
||||||
const resultsContainer = document.getElementById('resultsContainer');
|
try {
|
||||||
const resultsContent = document.getElementById('resultsContent');
|
new URL(url);
|
||||||
|
return true;
|
||||||
if (resultsContainer && resultsContent) {
|
} catch {
|
||||||
// Update header
|
return false;
|
||||||
const widgetTitle = resultsContainer.querySelector('.widget-title');
|
|
||||||
if (widgetTitle) {
|
|
||||||
widgetTitle.innerHTML = '<span class="widget-icon">🎓</span>Quantum AI Career Navigator';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create white-label interface
|
|
||||||
resultsContent.innerHTML = `
|
|
||||||
<div class="career-nav-container" style="text-align: center; margin-bottom: 20px;">
|
|
||||||
<h3 style="color: #0369a1; margin-bottom: 10px;">🎓 Quantum AI Career Navigator</h3>
|
|
||||||
<p style="color: #6b7280; margin-bottom: 20px;">Meet Jessica, your personal AI cybersecurity career advisor. Share your goals and get expert guidance tailored to your journey.</p>
|
|
||||||
</div>
|
|
||||||
<div class="career-interface-container" style="width: 100%; min-height: 600px; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
||||||
<iframe
|
|
||||||
src="${interfaceUrl}"
|
|
||||||
style="width: 100%; min-height: 600px; border: none; background: white;"
|
|
||||||
frameborder="0"
|
|
||||||
scrolling="auto"
|
|
||||||
title="Quantum AI Career Navigator - Your Personal Career Advisor">
|
|
||||||
</iframe>
|
|
||||||
</div>
|
|
||||||
<div class="career-footer" style="margin-top: 20px; padding: 15px; background: #f8fafc; border-radius: 8px; text-align: center;">
|
|
||||||
<p style="color: #6b7280; font-size: 14px; margin: 0;">
|
|
||||||
💡 <strong>Pro Tip:</strong> Be specific about your experience level and career goals for the most personalized advice from your AI advisor!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Hide action buttons since this is an interactive interface
|
|
||||||
const actionButtons = resultsContainer.querySelector('.results-actions');
|
|
||||||
if (actionButtons) {
|
|
||||||
actionButtons.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show results container
|
|
||||||
resultsContainer.style.display = 'block';
|
|
||||||
|
|
||||||
// Scroll to results
|
|
||||||
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
|
|
||||||
// Reset submit button
|
|
||||||
this.resetSubmitButton();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -588,14 +552,34 @@ class AgentsCore extends WorkflowsCore {
|
|||||||
const submitBtn = document.getElementById('generateBtn');
|
const submitBtn = document.getElementById('generateBtn');
|
||||||
if (submitBtn) {
|
if (submitBtn) {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
const agentSlug = document.body.getAttribute('data-agent-slug') || 'agent';
|
const agentIcon = this.getAgentIcon();
|
||||||
const agentName = agentSlug.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
submitBtn.textContent = `${agentIcon} Execute ${this.getAgentName()} (${this.price} AED)`;
|
||||||
submitBtn.textContent = `🚀 Execute ${agentName} (${this.price} AED)`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result action functions (global for button onclick handlers)
|
/**
|
||||||
|
* Get agent icon (fallback to generic icon)
|
||||||
|
*/
|
||||||
|
getAgentIcon() {
|
||||||
|
const iconMap = {
|
||||||
|
'social-ads-generator': '📢',
|
||||||
|
'job-posting-generator': '💼',
|
||||||
|
'pdf-summarizer': '📄',
|
||||||
|
'five-whys-analyzer': '❓'
|
||||||
|
};
|
||||||
|
|
||||||
|
return iconMap[this.agentSlug] || '🤖';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get agent display name
|
||||||
|
*/
|
||||||
|
getAgentName() {
|
||||||
|
return this.agentSlug.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global functions for button onclick handlers
|
||||||
function copyResults() {
|
function copyResults() {
|
||||||
const content = document.getElementById('resultsContent');
|
const content = document.getElementById('resultsContent');
|
||||||
if (content) {
|
if (content) {
|
||||||
@ -616,10 +600,7 @@ function downloadResults() {
|
|||||||
function resetForm() {
|
function resetForm() {
|
||||||
const form = document.getElementById('agentForm');
|
const form = document.getElementById('agentForm');
|
||||||
if (form) {
|
if (form) {
|
||||||
// Reset form but preserve CSRF token
|
|
||||||
const csrfToken = form.querySelector('[name="csrfmiddlewaretoken"]').value;
|
|
||||||
form.reset();
|
form.reset();
|
||||||
form.querySelector('[name="csrfmiddlewaretoken"]').value = csrfToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultsContainer = document.getElementById('resultsContainer');
|
const resultsContainer = document.getElementById('resultsContainer');
|
||||||
@ -629,29 +610,11 @@ function resetForm() {
|
|||||||
if (processingStatus) processingStatus.style.display = 'none';
|
if (processingStatus) processingStatus.style.display = 'none';
|
||||||
|
|
||||||
// Clear validation errors
|
// Clear validation errors
|
||||||
const fields = document.querySelectorAll('#agentForm [name]');
|
const formGroups = document.querySelectorAll('.form-group');
|
||||||
fields.forEach(field => {
|
formGroups.forEach(group => {
|
||||||
const fieldName = field.getAttribute('name');
|
const field = group.querySelector('input, select, textarea');
|
||||||
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
|
if (field) {
|
||||||
WorkflowsCore.clearFieldError(fieldName);
|
WorkflowsCore.clearFieldError(field.id);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset file upload UI components
|
|
||||||
const fileUploadContainers = document.querySelectorAll('.file-upload-container');
|
|
||||||
fileUploadContainers.forEach(container => {
|
|
||||||
const input = container.querySelector('.form-file-input');
|
|
||||||
const label = container.querySelector('.file-upload-label');
|
|
||||||
const selectedDiv = container.querySelector('.file-selected');
|
|
||||||
|
|
||||||
if (input) {
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
if (label) {
|
|
||||||
label.style.display = 'block';
|
|
||||||
}
|
|
||||||
if (selectedDiv) {
|
|
||||||
selectedDiv.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -662,8 +625,55 @@ function resetForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quick Agents Panel Functions
|
||||||
|
function toggleQuickAgents() {
|
||||||
|
const panel = document.getElementById('quickAgentsPanel');
|
||||||
|
const overlay = document.getElementById('quickAgentsOverlay');
|
||||||
|
|
||||||
|
if (panel && overlay) {
|
||||||
|
const isOpen = panel.getAttribute('aria-hidden') === 'false';
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
// Close panel
|
||||||
|
panel.setAttribute('aria-hidden', 'true');
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
panel.style.transform = 'translateX(100%)';
|
||||||
|
overlay.style.opacity = '0';
|
||||||
|
overlay.style.visibility = 'hidden';
|
||||||
|
} else {
|
||||||
|
// Open panel
|
||||||
|
panel.setAttribute('aria-hidden', 'false');
|
||||||
|
overlay.setAttribute('aria-hidden', 'false');
|
||||||
|
panel.style.transform = 'translateX(0)';
|
||||||
|
overlay.style.opacity = '1';
|
||||||
|
overlay.style.visibility = 'visible';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeQuickAgents() {
|
||||||
|
const panel = document.getElementById('quickAgentsPanel');
|
||||||
|
const overlay = document.getElementById('quickAgentsOverlay');
|
||||||
|
|
||||||
|
if (panel && overlay) {
|
||||||
|
panel.setAttribute('aria-hidden', 'true');
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
panel.style.transform = 'translateX(100%)';
|
||||||
|
overlay.style.opacity = '0';
|
||||||
|
overlay.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close panel with Escape key
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeQuickAgents();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize AgentsCore when DOM is ready
|
// Initialize AgentsCore when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize processor (data attributes set by template)
|
if (document.getElementById('agentForm')) {
|
||||||
window.agentsCore = new AgentsCore();
|
window.agentsCore = new AgentsCore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
356
static/js/workflows-core.js
Normal file
356
static/js/workflows-core.js
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
/**
|
||||||
|
* Workflows Core - Base functionality for all agents and workflows
|
||||||
|
* Provides common utilities, toast notifications, and form handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
class WorkflowsCore {
|
||||||
|
constructor() {
|
||||||
|
// Base initialization
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate unique session ID
|
||||||
|
*/
|
||||||
|
static generateSessionId() {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.random().toString(36).substring(2, 8);
|
||||||
|
return `session_${timestamp}_${random}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show toast notification
|
||||||
|
*/
|
||||||
|
static showToast(message, type = 'info') {
|
||||||
|
console.log('Showing toast:', message, type);
|
||||||
|
|
||||||
|
// Create toast container if it doesn't exist
|
||||||
|
let toastContainer = document.getElementById('toast-container');
|
||||||
|
if (!toastContainer) {
|
||||||
|
toastContainer = document.createElement('div');
|
||||||
|
toastContainer.id = 'toast-container';
|
||||||
|
toastContainer.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 400px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Safely append to body
|
||||||
|
const targetElement = document.body || document.documentElement;
|
||||||
|
if (targetElement) {
|
||||||
|
targetElement.appendChild(toastContainer);
|
||||||
|
console.log('Created toast container');
|
||||||
|
} else {
|
||||||
|
console.error('Cannot create toast container - no body or documentElement');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast toast-${type}`;
|
||||||
|
toast.style.cssText = `
|
||||||
|
background: ${type === 'error' ? '#fee2e2' : type === 'success' ? '#d1fae5' : '#dbeafe'};
|
||||||
|
color: ${type === 'error' ? '#dc2626' : type === 'success' ? '#059669' : '#2563eb'};
|
||||||
|
border: 1px solid ${type === 'error' ? '#fecaca' : type === 'success' ? '#a7f3d0' : '#93c5fd'};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add icon and message
|
||||||
|
const icon = type === 'error' ? '❌' : type === 'success' ? '✅' : 'ℹ️';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<span style="margin-right: 8px; font-size: 14px;">${icon}</span>
|
||||||
|
<span style="flex: 1; font-size: 14px; font-weight: 500;">${message}</span>
|
||||||
|
<button onclick="this.parentElement.remove()" style="
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add animation styles if not already added
|
||||||
|
if (!document.getElementById('toast-styles')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'toast-styles';
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Auto-remove after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentElement) {
|
||||||
|
toast.remove();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated
|
||||||
|
*/
|
||||||
|
static checkAuthentication() {
|
||||||
|
const isAuthenticated = document.body.getAttribute('data-user-authenticated') === 'true';
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
this.showToast('Please login to use this agent', 'error');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/auth/login/';
|
||||||
|
}, 2000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has sufficient balance
|
||||||
|
*/
|
||||||
|
static checkBalance(requiredAmount) {
|
||||||
|
const userBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0');
|
||||||
|
if (userBalance < requiredAmount) {
|
||||||
|
this.showToast(`Insufficient balance! You need ${requiredAmount} AED.`, 'error');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/wallet/';
|
||||||
|
}, 2000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show processing status
|
||||||
|
*/
|
||||||
|
static showProcessing(message = 'Processing...') {
|
||||||
|
const processingStatus = document.getElementById('processingStatus');
|
||||||
|
if (processingStatus) {
|
||||||
|
const statusText = processingStatus.querySelector('.status-text');
|
||||||
|
if (statusText) {
|
||||||
|
statusText.textContent = message;
|
||||||
|
}
|
||||||
|
processingStatus.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide processing status
|
||||||
|
*/
|
||||||
|
static hideProcessing() {
|
||||||
|
const processingStatus = document.getElementById('processingStatus');
|
||||||
|
if (processingStatus) {
|
||||||
|
processingStatus.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show field error
|
||||||
|
*/
|
||||||
|
static showFieldError(fieldName, message) {
|
||||||
|
const errorElement = document.getElementById(`${fieldName}-error`);
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.textContent = message;
|
||||||
|
errorElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = document.getElementById(fieldName);
|
||||||
|
if (field) {
|
||||||
|
field.classList.add('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear field error
|
||||||
|
*/
|
||||||
|
static clearFieldError(fieldName) {
|
||||||
|
const errorElement = document.getElementById(`${fieldName}-error`);
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = document.getElementById(fieldName);
|
||||||
|
if (field) {
|
||||||
|
field.classList.remove('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update wallet balance display - TARGET CORRECT ELEMENT
|
||||||
|
*/
|
||||||
|
static updateWalletBalance(newBalance) {
|
||||||
|
console.log('Updating wallet balance to:', newBalance);
|
||||||
|
|
||||||
|
// Target the specific walletBalance span element
|
||||||
|
const walletElement = document.getElementById('walletBalance');
|
||||||
|
|
||||||
|
if (walletElement) {
|
||||||
|
const oldText = walletElement.textContent;
|
||||||
|
walletElement.textContent = newBalance.toFixed(2); // Just the number, no symbols
|
||||||
|
console.log(`✅ Updated walletBalance from "${oldText}" to "${walletElement.textContent}"`);
|
||||||
|
} else {
|
||||||
|
console.warn('❌ walletBalance element not found, trying fallback approaches');
|
||||||
|
|
||||||
|
// Fallback 1: Try other common selectors
|
||||||
|
const fallbackElement = document.querySelector('.balance') ||
|
||||||
|
document.querySelector('[data-wallet-balance]') ||
|
||||||
|
document.querySelector('a[href*="wallet"]');
|
||||||
|
|
||||||
|
if (fallbackElement) {
|
||||||
|
const oldText = fallbackElement.textContent;
|
||||||
|
fallbackElement.textContent = `💰 ${newBalance.toFixed(2)} AED`;
|
||||||
|
console.log(`✅ Updated fallback element from "${oldText}" to "${fallbackElement.textContent}"`);
|
||||||
|
} else {
|
||||||
|
// Fallback 2: Find any element with AED in text
|
||||||
|
console.log('Trying manual search for wallet elements...');
|
||||||
|
let found = false;
|
||||||
|
document.querySelectorAll('*').forEach(el => {
|
||||||
|
if (!found && el.textContent && el.textContent.includes('AED') && el.textContent.match(/\d+\.\d+/)) {
|
||||||
|
const oldText = el.textContent;
|
||||||
|
el.textContent = `💰 ${newBalance.toFixed(2)} AED`;
|
||||||
|
console.log(`✅ Updated manual element: ${oldText} -> ${el.textContent}`);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
console.error('❌ No wallet balance elements found to update');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update data attribute for future calculations
|
||||||
|
document.body.setAttribute('data-user-balance', newBalance.toString());
|
||||||
|
console.log(`Updated data-user-balance to: ${newBalance.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug function to show wallet elements
|
||||||
|
*/
|
||||||
|
static debugWalletElements() {
|
||||||
|
console.log('=== WALLET DEBUG INFO ===');
|
||||||
|
document.querySelectorAll('*').forEach(el => {
|
||||||
|
const text = el.textContent?.trim();
|
||||||
|
if (text && text.includes('AED')) {
|
||||||
|
console.log('AED Element:', {
|
||||||
|
tag: el.tagName,
|
||||||
|
class: el.className,
|
||||||
|
id: el.id,
|
||||||
|
text: text,
|
||||||
|
hasDataAttr: el.hasAttribute('data-wallet-balance'),
|
||||||
|
href: el.href || 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('========================');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy text to clipboard
|
||||||
|
*/
|
||||||
|
static copyToClipboard(text, successMessage = 'Copied to clipboard!') {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.showToast(successMessage, 'success');
|
||||||
|
}).catch(() => {
|
||||||
|
this.fallbackCopyToClipboard(text, successMessage);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.fallbackCopyToClipboard(text, successMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback copy method for older browsers
|
||||||
|
*/
|
||||||
|
static fallbackCopyToClipboard(text, successMessage) {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
textArea.style.top = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
this.showToast(successMessage, 'success');
|
||||||
|
} catch (err) {
|
||||||
|
this.showToast('Failed to copy to clipboard', 'error');
|
||||||
|
} finally {
|
||||||
|
textArea.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download text as file
|
||||||
|
*/
|
||||||
|
static downloadAsFile(content, filename, successMessage = 'File downloaded!') {
|
||||||
|
try {
|
||||||
|
const blob = new Blob([content], { type: 'text/plain' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.style.display = 'none';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
a.remove();
|
||||||
|
this.showToast(successMessage, 'success');
|
||||||
|
} catch (err) {
|
||||||
|
this.showToast('Failed to download file', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduct balance via Django API (for direct N8N integrations)
|
||||||
|
*/
|
||||||
|
static async deductBalance(amount, description, agentSlug) {
|
||||||
|
try {
|
||||||
|
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
|
||||||
|
|
||||||
|
const response = await fetch('/wallet/api/deduct/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
amount: amount,
|
||||||
|
description: description,
|
||||||
|
agent_slug: agentSlug
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
this.updateWalletBalance(data.new_balance);
|
||||||
|
document.body.setAttribute('data-user-balance', data.new_balance.toString());
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to deduct balance:', response.status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Balance deduction error:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make available globally
|
||||||
|
window.WorkflowsCore = WorkflowsCore;
|
||||||
Loading…
Reference in New Issue
Block a user