Fix persona generation 504: async flow + remove debug logging
- Backend: /generate-personas-full now returns task_id immediately (202) and runs generation as a background asyncio task, delivering results via WebSocket task_completed event (bypasses GCP LB 30s timeout) - Frontend: AIRecruiter listens for ws:task_completed to process personas instead of awaiting the long HTTP response - Remove 53 debug console.log calls from websocketServiceNew.ts including session_id exposure and a self-test emit that was firing fake events - Remove debug logs from WebSocketContextNew, AIRecruiter, personaGenerator Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c00728f375
commit
aa4090888d
6 changed files with 301 additions and 615 deletions
|
|
@ -1016,108 +1016,48 @@ async def batch_generate_summaries():
|
|||
return jsonify({"error": "Internal server error", "message": f"An unexpected error occurred: {str(e)}"}), 500
|
||||
|
||||
|
||||
@ai_personas_bp.route('/generate-personas-full', methods=['POST'])
|
||||
@jwt_required()
|
||||
async def generate_personas_full():
|
||||
"""
|
||||
Unified endpoint for complete persona generation with cancellation support.
|
||||
|
||||
Combines the two-stage generation process (basic profiles + completion) into a single
|
||||
endpoint with proper task management and cancellation checkpoints.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"audience_brief": "A detailed description of the audience context...",
|
||||
"research_objective": "Optional research objective...",
|
||||
"count": 5,
|
||||
"temperature": 0.8,
|
||||
"customer_data_session_id": "optional_session_id",
|
||||
"llm_model": "gemini-3-pro-preview",
|
||||
"target_folder_id": "optional_folder_id"
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON object with task_id and generated personas (when complete)
|
||||
"""
|
||||
user_id = get_jwt_identity()
|
||||
data = await request.get_json() or {}
|
||||
|
||||
# Extract and validate parameters
|
||||
audience_brief = data.get('audience_brief')
|
||||
if not audience_brief or len(audience_brief.strip()) < 10:
|
||||
return jsonify({"error": "Missing or invalid audience brief", "message": "Audience brief must be at least 10 characters"}), 400
|
||||
|
||||
research_objective = data.get('research_objective')
|
||||
count = data.get('count', 5)
|
||||
if count < 1 or count > 10:
|
||||
return jsonify({"error": "Invalid count", "message": "Count must be between 1 and 10"}), 400
|
||||
|
||||
temperature = data.get('temperature', 1.0)
|
||||
if not (0 <= temperature <= 1.5):
|
||||
temperature = 1.0
|
||||
|
||||
customer_data_session_id = data.get('customer_data_session_id')
|
||||
llm_model = data.get('llm_model', 'gemini-3-pro-preview')
|
||||
target_folder_id = data.get('target_folder_id')
|
||||
|
||||
try:
|
||||
# Register current task for cancellation with comprehensive metadata
|
||||
async with CancellableTask("persona_full_generation", user_id, {
|
||||
"count": count,
|
||||
"type": "unified_generation",
|
||||
"llm_model": llm_model,
|
||||
"target_folder_id": target_folder_id
|
||||
}) as task_id:
|
||||
|
||||
current_app.logger.info(f"Starting unified persona generation for {count} personas using {llm_model}")
|
||||
async def _run_persona_generation_bg(
|
||||
app,
|
||||
task_id: str,
|
||||
user_id: str,
|
||||
audience_brief: str,
|
||||
research_objective,
|
||||
count: int,
|
||||
temperature: float,
|
||||
customer_data_session_id,
|
||||
llm_model: str,
|
||||
target_folder_id
|
||||
):
|
||||
"""Background coroutine: generates personas and notifies via WebSocket."""
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
|
||||
async with app.app_context():
|
||||
try:
|
||||
# Stage 1
|
||||
print(f"🔄 Backend: Unified generation started - Task ID: {task_id}")
|
||||
|
||||
# Return task_id immediately so frontend can start cancellation tracking
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
if user_id:
|
||||
await websocket_manager.emit_to_user(
|
||||
user_id,
|
||||
'task_started',
|
||||
{
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_full_generation',
|
||||
'message': f'Started generating {count} personas'
|
||||
}
|
||||
)
|
||||
|
||||
# Stage 1: Generate basic profiles with cancellation check
|
||||
if asyncio.current_task().cancelled():
|
||||
raise asyncio.CancelledError("Task was cancelled")
|
||||
|
||||
print(f"🔄 Backend: Stage 1 - Generating {count} basic profiles...")
|
||||
basic_profiles = await generate_basic_personas(
|
||||
audience_brief=audience_brief,
|
||||
research_objective=research_objective,
|
||||
count=count,
|
||||
temperature=temperature, # Use temperature from request
|
||||
temperature=temperature,
|
||||
customer_data_session_id=customer_data_session_id,
|
||||
llm_model=llm_model
|
||||
)
|
||||
|
||||
print(f"✅ Backend: Stage 1 complete - {len(basic_profiles)} basic profiles generated")
|
||||
|
||||
# Stage 2: Complete personas in parallel with cancellation support
|
||||
|
||||
# Stage 2
|
||||
completed_personas = []
|
||||
failed_personas = []
|
||||
|
||||
print(f"🔄 Backend: Stage 2 - Completing {len(basic_profiles)} personas in parallel...")
|
||||
|
||||
|
||||
async def complete_single_persona(i: int, basic_profile: dict):
|
||||
"""Complete a single persona with cancellation checks."""
|
||||
try:
|
||||
# Check for cancellation before starting
|
||||
if asyncio.current_task().cancelled():
|
||||
raise asyncio.CancelledError("Task was cancelled")
|
||||
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
print(f"🔄 Backend: Completing persona {i+1}/{len(basic_profiles)}: {basic_profile.get('name', 'Unknown')}")
|
||||
|
||||
# Complete the persona
|
||||
persona_data = await generate_persona(
|
||||
basic_persona=basic_profile,
|
||||
temperature=temperature,
|
||||
|
|
@ -1127,146 +1067,168 @@ async def generate_personas_full():
|
|||
research_objective=research_objective
|
||||
)
|
||||
|
||||
# Check cancellation before summary generation
|
||||
if asyncio.current_task().cancelled():
|
||||
raise asyncio.CancelledError("Task was cancelled")
|
||||
|
||||
# Generate AI summary
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
try:
|
||||
summary_data = await generate_persona_summary(
|
||||
persona_data=persona_data,
|
||||
temperature=temperature,
|
||||
llm_model=llm_model
|
||||
)
|
||||
|
||||
persona_data['aiSynthesizedBio'] = summary_data['aiSynthesizedBio']
|
||||
persona_data['qualitativeAttributes'] = summary_data['qualitativeAttributes']
|
||||
persona_data['topPersonalityTraits'] = summary_data['topPersonalityTraits']
|
||||
|
||||
except Exception as summary_error:
|
||||
current_app.logger.warning(f"Failed to generate summary for persona: {str(summary_error)}")
|
||||
|
||||
# Add generation prompts
|
||||
app.logger.warning(f"Failed to generate summary for persona: {str(summary_error)}")
|
||||
|
||||
if audience_brief:
|
||||
persona_data['audience_brief'] = audience_brief
|
||||
if research_objective:
|
||||
persona_data['research_objective'] = research_objective
|
||||
|
||||
# Check cancellation before database save
|
||||
|
||||
if asyncio.current_task().cancelled():
|
||||
raise asyncio.CancelledError("Task was cancelled")
|
||||
|
||||
# Remove generated ID before saving
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
if 'id' in persona_data:
|
||||
del persona_data['id']
|
||||
|
||||
# Save to database
|
||||
|
||||
persona_id = await Persona.create(persona_data, user_id)
|
||||
persona_data['_id'] = str(persona_id)
|
||||
|
||||
print(f"✅ Backend: Persona {i+1}/{len(basic_profiles)} completed: {persona_data.get('name')}")
|
||||
return {'success': True, 'persona': persona_data, 'index': i}
|
||||
|
||||
|
||||
except asyncio.CancelledError:
|
||||
raise # Re-raise cancellation
|
||||
raise
|
||||
except Exception as persona_error:
|
||||
print(f"❌ Backend: Failed to complete persona {i+1}: {persona_error}")
|
||||
return {
|
||||
'success': False,
|
||||
'index': i,
|
||||
'name': basic_profile.get('name', f'Persona {i+1}'),
|
||||
'error': str(persona_error)
|
||||
}
|
||||
|
||||
# Execute all persona completions in parallel
|
||||
try:
|
||||
completion_tasks = [
|
||||
complete_single_persona(i, profile)
|
||||
for i, profile in enumerate(basic_profiles)
|
||||
]
|
||||
|
||||
# Wait for all personas to complete or fail
|
||||
results = await asyncio.gather(*completion_tasks, return_exceptions=True)
|
||||
|
||||
# Process results
|
||||
for result in results:
|
||||
if isinstance(result, asyncio.CancelledError):
|
||||
raise result # Re-raise cancellation
|
||||
elif isinstance(result, Exception):
|
||||
failed_personas.append({
|
||||
'index': -1,
|
||||
'name': 'Unknown',
|
||||
'error': str(result)
|
||||
})
|
||||
elif result['success']:
|
||||
completed_personas.append(result['persona'])
|
||||
else:
|
||||
failed_personas.append({
|
||||
'index': result['index'],
|
||||
'name': result['name'],
|
||||
'error': result['error']
|
||||
})
|
||||
|
||||
except asyncio.CancelledError:
|
||||
raise # Re-raise cancellation
|
||||
|
||||
return {'success': False, 'index': i, 'name': basic_profile.get('name', f'Persona {i+1}'), 'error': str(persona_error)}
|
||||
|
||||
results = await asyncio.gather(
|
||||
*[complete_single_persona(i, p) for i, p in enumerate(basic_profiles)],
|
||||
return_exceptions=True
|
||||
)
|
||||
|
||||
for result in results:
|
||||
if isinstance(result, asyncio.CancelledError):
|
||||
raise result
|
||||
elif isinstance(result, Exception):
|
||||
failed_personas.append({'index': -1, 'name': 'Unknown', 'error': str(result)})
|
||||
elif result['success']:
|
||||
completed_personas.append(result['persona'])
|
||||
else:
|
||||
failed_personas.append({'index': result['index'], 'name': result['name'], 'error': result['error']})
|
||||
|
||||
print(f"✅ Backend: Stage 2 complete - {len(completed_personas)} personas completed, {len(failed_personas)} failed")
|
||||
|
||||
# Check cancellation before folder assignment
|
||||
if asyncio.current_task().cancelled():
|
||||
raise asyncio.CancelledError("Task was cancelled")
|
||||
|
||||
# Add personas to target folder if specified
|
||||
|
||||
# Folder assignment
|
||||
if target_folder_id and completed_personas:
|
||||
try:
|
||||
from app.models.folder import Folder
|
||||
persona_ids = [p['_id'] for p in completed_personas]
|
||||
await Folder.add_personas_batch(target_folder_id, persona_ids)
|
||||
print(f"✅ Backend: Added {len(persona_ids)} personas to folder {target_folder_id}")
|
||||
await Folder.add_personas_batch(target_folder_id, [p['_id'] for p in completed_personas])
|
||||
except Exception as folder_error:
|
||||
current_app.logger.warning(f"Failed to add personas to folder: {folder_error}")
|
||||
|
||||
# Clean up customer data if provided
|
||||
app.logger.warning(f"Failed to add personas to folder: {folder_error}")
|
||||
|
||||
# Customer data cleanup
|
||||
if customer_data_session_id:
|
||||
try:
|
||||
customer_data_service.cleanup_session(customer_data_session_id)
|
||||
except Exception as cleanup_error:
|
||||
current_app.logger.warning(f"Failed to cleanup customer data: {cleanup_error}")
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"🎉 Backend: Unified generation complete - {len(completed_personas)} personas created")
|
||||
|
||||
# Emit completion event via WebSocket
|
||||
if user_id:
|
||||
await websocket_manager.emit_to_user(
|
||||
user_id,
|
||||
'task_completed',
|
||||
{
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_full_generation',
|
||||
'message': f'Successfully generated {len(completed_personas)} personas',
|
||||
'personas_created': len(completed_personas),
|
||||
'errors': len(failed_personas)
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"message": f"Successfully generated and saved {len(completed_personas)} personas using {llm_model}",
|
||||
"personas": completed_personas,
|
||||
"persona_ids": [p['_id'] for p in completed_personas],
|
||||
"task_id": task_id,
|
||||
"errors": failed_personas if failed_personas else None,
|
||||
"partial_success": len(failed_personas) > 0 and len(completed_personas) > 0
|
||||
}), 201
|
||||
|
||||
except asyncio.CancelledError:
|
||||
current_app.logger.info(f"Unified persona generation cancelled for user {user_id}")
|
||||
return jsonify({"error": "Generation cancelled", "message": "Persona generation was cancelled by user"}), 499
|
||||
except PersonaGenerationError as e:
|
||||
current_app.logger.error(f"Unified persona generation error: {str(e)}")
|
||||
return jsonify({"error": "Failed to generate personas", "message": str(e)}), 500
|
||||
|
||||
await websocket_manager.emit_to_user(user_id, 'task_completed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_full_generation',
|
||||
'message': f'Successfully generated {len(completed_personas)} personas',
|
||||
'personas': completed_personas,
|
||||
'errors': failed_personas if failed_personas else None,
|
||||
'partial_success': len(failed_personas) > 0 and len(completed_personas) > 0
|
||||
})
|
||||
|
||||
except asyncio.CancelledError:
|
||||
app.logger.info(f"Persona generation task {task_id} cancelled")
|
||||
await websocket_manager.emit_to_user(user_id, 'task_cancelled', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_full_generation',
|
||||
'message': 'Generation cancelled'
|
||||
})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Persona generation task {task_id} failed: {str(e)}")
|
||||
await websocket_manager.emit_to_user(user_id, 'task_failed', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_full_generation',
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
|
||||
@ai_personas_bp.route('/generate-personas-full', methods=['POST'])
|
||||
@jwt_required()
|
||||
async def generate_personas_full():
|
||||
"""
|
||||
Async persona generation: returns task_id immediately (202),
|
||||
delivers results via WebSocket task_completed event.
|
||||
"""
|
||||
user_id = get_jwt_identity()
|
||||
data = await request.get_json() or {}
|
||||
|
||||
audience_brief = data.get('audience_brief')
|
||||
if not audience_brief or len(audience_brief.strip()) < 10:
|
||||
return jsonify({"error": "Missing or invalid audience brief", "message": "Audience brief must be at least 10 characters"}), 400
|
||||
|
||||
research_objective = data.get('research_objective')
|
||||
count = data.get('count', 5)
|
||||
if count < 1 or count > 10:
|
||||
return jsonify({"error": "Invalid count", "message": "Count must be between 1 and 10"}), 400
|
||||
|
||||
temperature = data.get('temperature', 1.0)
|
||||
if not (0 <= temperature <= 1.5):
|
||||
temperature = 1.0
|
||||
|
||||
customer_data_session_id = data.get('customer_data_session_id')
|
||||
llm_model = data.get('llm_model', 'gemini-3-pro-preview')
|
||||
target_folder_id = data.get('target_folder_id')
|
||||
|
||||
try:
|
||||
from app.services.task_manager import get_task_manager
|
||||
from app.websocket_manager_async import get_async_websocket_manager
|
||||
|
||||
task_manager = get_task_manager()
|
||||
task_id = task_manager.generate_task_id()
|
||||
app = current_app._get_current_object()
|
||||
|
||||
bg_task = asyncio.create_task(
|
||||
_run_persona_generation_bg(
|
||||
app, task_id, user_id,
|
||||
audience_brief, research_objective,
|
||||
count, temperature, customer_data_session_id,
|
||||
llm_model, target_folder_id
|
||||
)
|
||||
)
|
||||
await task_manager.register_task(bg_task, 'persona_full_generation', user_id, {
|
||||
'count': count,
|
||||
'llm_model': llm_model,
|
||||
'target_folder_id': target_folder_id
|
||||
}, task_id=task_id)
|
||||
|
||||
current_app.logger.info(f"Starting unified persona generation for {count} personas using {llm_model}")
|
||||
|
||||
websocket_manager = get_async_websocket_manager()
|
||||
await websocket_manager.emit_to_user(user_id, 'task_started', {
|
||||
'task_id': task_id,
|
||||
'task_type': 'persona_full_generation',
|
||||
'message': f'Started generating {count} personas'
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'task_id': task_id,
|
||||
'message': f'Generation started for {count} personas'
|
||||
}), 202
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Unexpected error in unified persona generation: {str(e)}")
|
||||
return jsonify({"error": "Internal server error", "message": "An unexpected error occurred"}), 500
|
||||
current_app.logger.error(f"Failed to start persona generation: {str(e)}")
|
||||
return jsonify({"error": "Internal server error", "message": "Failed to start generation"}), 500
|
||||
|
||||
|
||||
@ai_personas_bp.route('/upload-customer-data', methods=['POST'])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { z } from "zod";
|
||||
import { Users } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
|
@ -34,6 +34,8 @@ export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecr
|
|||
const [generationToastId, setGenerationToastId] = useState<string | null>(null);
|
||||
const [isProgressModalOpen, setIsProgressModalOpen] = useState(false);
|
||||
const [progressModalDescription, setProgressModalDescription] = useState('');
|
||||
// Ref to track current task_id in WebSocket event handlers (avoids stale closure)
|
||||
const taskIdRef = useRef<string | null>(null);
|
||||
|
||||
// Check URL params for state restoration
|
||||
useEffect(() => {
|
||||
|
|
@ -61,186 +63,127 @@ export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecr
|
|||
}
|
||||
}, [generationState.isGenerating, generationState.isCancelling, generationToastId]);
|
||||
|
||||
// Handle WebSocket task completion — personas arrive here
|
||||
useEffect(() => {
|
||||
const handleTaskCompleted = (event: CustomEvent) => {
|
||||
const data = event.detail;
|
||||
if (data.task_id !== taskIdRef.current) return;
|
||||
|
||||
generationControls.completeGeneration();
|
||||
if (generationToastId) {
|
||||
toast.dismiss(generationToastId);
|
||||
setGenerationToastId(null);
|
||||
}
|
||||
|
||||
const personas: Persona[] = data.personas || [];
|
||||
const errors = data.errors || [];
|
||||
|
||||
if (personas.length > 0) {
|
||||
if (data.partial_success && errors.length > 0) {
|
||||
toast.success("Some personas generated successfully", {
|
||||
description: `${personas.length} created, ${errors.length} failed.`,
|
||||
duration: 8000
|
||||
});
|
||||
} else {
|
||||
toast.success("Personas generated and saved successfully", {
|
||||
description: `${personas.length} personas saved${targetFolderName ? ` to "${targetFolderName}"` : ''}.`
|
||||
});
|
||||
}
|
||||
setTimeout(() => navigate('/synthetic-users?mode=view'), 1500);
|
||||
} else {
|
||||
generationControls.failGeneration("No personas were generated");
|
||||
toast.error("Failed to generate personas", { description: "No personas were returned." });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTaskFailed = (event: CustomEvent) => {
|
||||
const data = event.detail;
|
||||
if (data.task_id !== taskIdRef.current) return;
|
||||
|
||||
const errorMessage = data.message || "Please try again or adjust your parameters";
|
||||
generationControls.failGeneration(errorMessage);
|
||||
if (generationToastId) {
|
||||
toast.dismiss(generationToastId);
|
||||
setGenerationToastId(null);
|
||||
}
|
||||
toast.error("Failed to generate personas", { description: errorMessage, duration: 6000 });
|
||||
};
|
||||
|
||||
window.addEventListener('ws:task_completed', handleTaskCompleted as EventListener);
|
||||
window.addEventListener('ws:task_failed', handleTaskFailed as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener('ws:task_completed', handleTaskCompleted as EventListener);
|
||||
window.removeEventListener('ws:task_failed', handleTaskFailed as EventListener);
|
||||
};
|
||||
}, [generationToastId, targetFolderName]);
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
try {
|
||||
// Start generation without task ID - will be set when real task ID arrives via WebSocket
|
||||
generationControls.startGeneration();
|
||||
|
||||
// Validate count before proceeding
|
||||
const count = parseInt(values.personaCount);
|
||||
if (isNaN(count) || count < 1 || count > 10) {
|
||||
toast.error("Invalid number of personas", {
|
||||
description: "Please enter a number between 1 and 10"
|
||||
});
|
||||
toast.error("Invalid number of personas", { description: "Please enter a number between 1 and 10" });
|
||||
generationControls.resetGeneration();
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust the expected time based on the count
|
||||
const estimatedTime = count <= 2 ? "30-60 seconds" :
|
||||
count <= 4 ? "1-2 minutes" :
|
||||
count <= 6 ? "2-3 minutes" :
|
||||
"3-5 minutes";
|
||||
count <= 4 ? "1-2 minutes" :
|
||||
count <= 6 ? "2-3 minutes" : "3-5 minutes";
|
||||
|
||||
// Open progress modal with description
|
||||
setProgressModalDescription(`Creating ${count} synthetic persona${count !== 1 ? 's' : ''} based on your criteria. Estimated time: ${estimatedTime}.`);
|
||||
setIsProgressModalOpen(true);
|
||||
|
||||
const toastId = toast.info("Generating AI personas", {
|
||||
description: `Creating ${count} synthetic personas based on your brief. This may take ${estimatedTime}. Please be patient.`,
|
||||
description: `Creating ${count} synthetic personas. This may take ${estimatedTime}.`,
|
||||
duration: 10000
|
||||
});
|
||||
setGenerationToastId(toastId);
|
||||
|
||||
// Show folder info in toast if available
|
||||
|
||||
if (targetFolderId && targetFolderName) {
|
||||
console.log(`Target folder for new personas: ID=${targetFolderId}, Name=${targetFolderName}`);
|
||||
toast.info(`Creating personas in "${targetFolderName}" folder`, {
|
||||
duration: 3000
|
||||
});
|
||||
} else {
|
||||
console.log("No target folder specified for new personas");
|
||||
toast.info(`Creating personas in "${targetFolderName}" folder`, { duration: 3000 });
|
||||
}
|
||||
|
||||
// Log which model is being used for generation
|
||||
console.log(`🤖 Starting persona generation with model: ${values.llm_model || 'gemini-2.5-pro'}`);
|
||||
|
||||
const response = await generateSyntheticPersonas(
|
||||
values.audienceBrief,
|
||||
|
||||
// Quick call — returns task_id immediately (202), generation runs in background
|
||||
const { task_id } = await generateSyntheticPersonas(
|
||||
values.audienceBrief,
|
||||
values.researchObjective,
|
||||
count,
|
||||
values.dataFile,
|
||||
targetFolderId,
|
||||
values.llm_model,
|
||||
// Pass a callback to set task ID as soon as we get it
|
||||
(taskId: string) => {
|
||||
console.log('🔥 Early task ID callback:', taskId);
|
||||
taskIdRef.current = taskId;
|
||||
generationControls.setTaskId(taskId);
|
||||
},
|
||||
values.temperature
|
||||
).catch(error => {
|
||||
// Check if generation was cancelled early (before first API response)
|
||||
if (generationState.taskId?.startsWith('temp-') && !generationState.isGenerating) {
|
||||
console.log('🔥 Generation was cancelled early - not propagating error');
|
||||
return null; // Don't propagate the error if cancelled
|
||||
}
|
||||
throw error; // Re-throw other errors
|
||||
});
|
||||
|
||||
// Handle early cancellation
|
||||
if (!response) {
|
||||
console.log('🔥 Generation was cancelled - no response to process');
|
||||
return; // Exit early if cancelled
|
||||
}
|
||||
|
||||
// Check if response includes task ID and set it
|
||||
console.log('🔥 Full response:', response);
|
||||
console.log('🔥 Response task_id:', response.task_id);
|
||||
if (response.task_id) {
|
||||
console.log('🔥 Setting task ID:', response.task_id);
|
||||
generationControls.setTaskId(response.task_id);
|
||||
} else {
|
||||
console.log('🔥 No task_id in response!');
|
||||
}
|
||||
|
||||
// Extract personas from the response
|
||||
const personas = response.personas || response;
|
||||
|
||||
// Check for partial success (some personas generated, some failed)
|
||||
if (personas && personas.length > 0) {
|
||||
// Log successful generation with model info
|
||||
console.log(`✅ Successfully generated ${personas.length} personas using model: ${values.llm_model || 'gemini-2.5-pro'}`);
|
||||
|
||||
// Mark generation as complete
|
||||
generationControls.completeGeneration();
|
||||
|
||||
// Dismiss the generation toast
|
||||
if (generationToastId) {
|
||||
toast.dismiss(generationToastId);
|
||||
setGenerationToastId(null);
|
||||
}
|
||||
|
||||
// Check if we got a response with partial success info
|
||||
if (response.partial_success || (response.errors && response.errors.length > 0)) {
|
||||
// Some personas succeeded but others failed
|
||||
toast.success("Some personas generated successfully", {
|
||||
description: `${personas.length} synthetic personas were created using ${values.llm_model || 'Gemini 3 Pro'}. ${response.errors?.length || 0} failed due to timeout or other errors.`,
|
||||
duration: 8000
|
||||
});
|
||||
|
||||
// Show details about the failures
|
||||
if (response.errors && response.errors.length > 0) {
|
||||
setTimeout(() => {
|
||||
toast.error("Some personas failed to generate", {
|
||||
description: `${response.errors.length} personas timed out. The server took too long to generate them. The successfully generated personas have been saved${targetFolderId ? ` in the selected folder` : ''}.`,
|
||||
duration: 10000
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
// All personas succeeded
|
||||
toast.success("Personas generated and saved successfully", {
|
||||
description: `${personas.length} synthetic personas have been created using ${values.llm_model || 'Gemini 3 Pro'} and saved ${targetFolderId ? `to the "${targetFolderName}" folder` : 'to the database'}.`
|
||||
});
|
||||
}
|
||||
|
||||
// Wait a moment before navigating to show completion
|
||||
setTimeout(() => {
|
||||
navigate('/synthetic-users?mode=view');
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error("No personas were generated");
|
||||
}
|
||||
);
|
||||
|
||||
taskIdRef.current = task_id;
|
||||
generationControls.setTaskId(task_id);
|
||||
|
||||
} catch (error) {
|
||||
// Check if this was a cancellation (expected behavior)
|
||||
if (error.response?.status === 499) {
|
||||
// Task was cancelled - this is handled by the WebSocket cancellation system
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`❌ Error generating personas using model: ${values.llm_model || 'gemini-2.5-pro'}:`, error);
|
||||
|
||||
if (error.response?.status === 499) return;
|
||||
|
||||
let errorMessage = "Please try again or adjust your parameters";
|
||||
let errorTitle = "Failed to generate personas";
|
||||
|
||||
// Check for specific error types
|
||||
if (error.code === "ECONNABORTED" || error.message?.includes("timeout") || error.response?.status === 504) {
|
||||
errorTitle = "Generation timeout";
|
||||
errorMessage = "AI persona generation timed out. This often happens when generating multiple complex personas. Try generating fewer personas (2-3) or try again later.";
|
||||
} else if (error.response?.status === 500) {
|
||||
errorTitle = "Server error";
|
||||
|
||||
// Try to extract the error message from the response if available
|
||||
if (error.response?.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
} else if (error.response?.data?.error) {
|
||||
errorMessage = error.response.data.error;
|
||||
} else {
|
||||
errorMessage = "The server encountered an error processing your request. Please try again later.";
|
||||
}
|
||||
let errorTitle = "Failed to start generation";
|
||||
|
||||
if (error.response?.status === 400) {
|
||||
errorMessage = error.response.data?.message || errorMessage;
|
||||
} else if (error.response?.status === 401) {
|
||||
errorTitle = "Authentication required";
|
||||
errorMessage = "Please log in to generate personas.";
|
||||
} else if (error.message?.includes("504 Deadline Exceeded")) {
|
||||
errorTitle = "Generation timeout";
|
||||
errorMessage = "The AI model took too long to generate personas. Try generating fewer personas or simplify your brief.";
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
// Mark generation as failed
|
||||
|
||||
generationControls.failGeneration(errorMessage);
|
||||
|
||||
// Dismiss the generation toast
|
||||
if (generationToastId) {
|
||||
toast.dismiss(generationToastId);
|
||||
setGenerationToastId(null);
|
||||
}
|
||||
|
||||
toast.error(errorTitle, {
|
||||
description: errorMessage,
|
||||
duration: 6000 // Show for longer to ensure user sees it
|
||||
});
|
||||
toast.error(errorTitle, { description: errorMessage, duration: 6000 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,25 +24,17 @@ export function WebSocketProvider({ children }: WebSocketProviderProps) {
|
|||
|
||||
// GPT-5 FIX: Get token from localStorage directly as fallback
|
||||
const getAccessToken = () => {
|
||||
const authToken = token || localStorage.getItem('auth_token');
|
||||
console.log('🔧 [GPT-5 Context] Getting token:', authToken ? 'Found' : 'Missing');
|
||||
return authToken || '';
|
||||
return token || localStorage.getItem('auth_token') || '';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!socketInitialized) {
|
||||
console.log('🔧 [GPT-5 Context] Initializing singleton socket');
|
||||
initSocket(getAccessToken);
|
||||
socketInitialized = true;
|
||||
}
|
||||
|
||||
// Only connect if we have a valid token
|
||||
const currentToken = getAccessToken();
|
||||
if (currentToken) {
|
||||
console.log('🔧 [GPT-5 Context] Connecting socket with token');
|
||||
if (getAccessToken()) {
|
||||
connectSocket();
|
||||
} else {
|
||||
console.log('🔧 [GPT-5 Context] No token available, skipping WebSocket connection');
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ export const aiPersonasApi = {
|
|||
llm_model: llmModel || 'gemini-2.5-pro',
|
||||
target_folder_id: targetFolderId
|
||||
}, {
|
||||
timeout: 180000 // 3 minutes for complete generation
|
||||
timeout: 10000 // 10 seconds — endpoint returns immediately with task_id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
/**
|
||||
* GPT-5 Singleton WebSocket Service
|
||||
* Fixes listener binding issues by ensuring stable socket instance and re-binding on every connect
|
||||
* Singleton WebSocket Service
|
||||
*/
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
const BASE_URL = import.meta.env.DEV
|
||||
const BASE_URL = import.meta.env.DEV
|
||||
? "http://localhost:5137"
|
||||
: (import.meta.env.VITE_WEBSOCKET_URL || window.location.origin);
|
||||
|
||||
|
|
@ -14,16 +13,12 @@ let socket: Socket | null = null;
|
|||
let currentRoom: string | null = null;
|
||||
let coreListenersBound = false;
|
||||
|
||||
// GPT-5 FIX: Pass token getter function to ensure fresh tokens on reconnect
|
||||
export function initSocket(getToken: () => string): Socket {
|
||||
if (socket) {
|
||||
// Keep token fresh for future reconnects
|
||||
socket.io.opts.auth = { token: getToken() };
|
||||
return socket;
|
||||
}
|
||||
|
||||
console.log('🔧 [GPT-5] Creating singleton socket:', BASE_URL, SOCKET_PATH);
|
||||
|
||||
socket = io(BASE_URL, {
|
||||
path: SOCKET_PATH,
|
||||
transports: ["websocket"],
|
||||
|
|
@ -32,125 +27,77 @@ export function initSocket(getToken: () => string): Socket {
|
|||
timeout: 60000,
|
||||
pingInterval: 45000,
|
||||
pingTimeout: 120000,
|
||||
// Using auth callback guarantees latest token on every (re)connect
|
||||
auth: (cb) => cb({ token: getToken() }),
|
||||
});
|
||||
|
||||
// Always refresh token before reconnect attempts
|
||||
socket.io.on("reconnect_attempt", () => {
|
||||
console.log('🔧 [GPT-5] Reconnect attempt - refreshing token');
|
||||
socket!.io.opts.auth = { token: getToken() };
|
||||
});
|
||||
|
||||
// GPT-5 CRITICAL: Bind listeners on EVERY connect (initial + reconnects)
|
||||
const onConnected = () => {
|
||||
console.log('🔧 [GPT-5] Socket connected, rebinding listeners and rejoining room');
|
||||
bindCoreListeners(); // idempotent
|
||||
if (currentRoom) rejoinRoom(); // keep room after reconnect
|
||||
};
|
||||
socket.on("connect", onConnected);
|
||||
socket.on("connect", () => {
|
||||
bindCoreListeners();
|
||||
if (currentRoom) rejoinRoom();
|
||||
});
|
||||
|
||||
// GPT-5 FIX: Route all events through onAny since specific listeners are broken
|
||||
socket.onAny((event, ...args) => {
|
||||
console.log(`🔧 [GPT-5 onAny] ${event}:`, args);
|
||||
|
||||
// Route to specific handlers manually since socket.on() doesn't work
|
||||
const payload = args[0]; // First argument is always the payload
|
||||
|
||||
const payload = args[0];
|
||||
|
||||
switch (event) {
|
||||
case 'joined_focus_group':
|
||||
console.log('🔧 [GPT-5] *** ROUTING joined_focus_group from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:joined_focus_group", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'left_focus_group':
|
||||
console.log('🔧 [GPT-5] *** ROUTING left_focus_group from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:left_focus_group", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'message_update':
|
||||
console.log('🔧 [GPT-5] *** ROUTING message_update from onAny ***');
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent("ws:message_update", { detail: payload }));
|
||||
console.log('🔧 [GPT-5] DISPATCHED window event ws:message_update SUCCESS (via onAny)');
|
||||
} catch (error) {
|
||||
console.error('🔧 [GPT-5] ERROR dispatching window event (via onAny):', error);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent("ws:message_update", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'ai_status_update':
|
||||
console.log('🔧 [GPT-5] *** ROUTING ai_status_update from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:ai_status_update", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'moderator_status_update':
|
||||
console.log('🔧 [GPT-5] *** ROUTING moderator_status_update from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:moderator_status_update", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'theme_update':
|
||||
console.log('🔧 [GPT-5] *** ROUTING theme_update from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:theme_update", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'focus_group_update':
|
||||
console.log('🔧 [GPT-5] *** ROUTING focus_group_update from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:focus_group_update", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'mode_event_update':
|
||||
console.log('🔧 [GPT-5] *** ROUTING mode_event_update from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:mode_event_update", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'connected':
|
||||
console.log('🔧 [GPT-5] *** ROUTING connected from onAny ***');
|
||||
// Handle connected events if needed
|
||||
break;
|
||||
|
||||
case 'task_started':
|
||||
console.log('🔧 [GPT-5] *** ROUTING task_started from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:task_started", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'task_cancelled':
|
||||
console.log('🔧 [GPT-5] *** ROUTING task_cancelled from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:task_cancelled", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'task_completed':
|
||||
console.log('🔧 [GPT-5] *** ROUTING task_completed from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:task_completed", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'task_failed':
|
||||
console.log('🔧 [GPT-5] *** ROUTING task_failed from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:task_failed", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'bulk_export_progress':
|
||||
console.log('🔧 [GPT-5] *** ROUTING bulk_export_progress from onAny ***');
|
||||
window.dispatchEvent(new CustomEvent("ws:bulk_export_progress", { detail: payload }));
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('🔧 [GPT-5] *** ROUTING error from onAny ***', payload);
|
||||
console.error('[WebSocket] Error:', payload);
|
||||
break;
|
||||
|
||||
case 'auth_error':
|
||||
console.error('🔧 [GPT-5] *** ROUTING auth_error from onAny ***', payload);
|
||||
console.error('[WebSocket] Auth error');
|
||||
window.dispatchEvent(new CustomEvent("ws:auth_error", { detail: payload }));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Connection debugging
|
||||
socket.on("connect_error", (error) => {
|
||||
console.error('🔧 [GPT-5] Connect error:', error);
|
||||
console.error('[WebSocket] Connect error:', error.message);
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log('🔧 [GPT-5] Disconnected:', reason);
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Disconnected:', reason);
|
||||
});
|
||||
|
||||
return socket;
|
||||
|
|
@ -158,179 +105,65 @@ export function initSocket(getToken: () => string): Socket {
|
|||
|
||||
export function connectSocket() {
|
||||
if (socket && !socket.connected) {
|
||||
console.log('🔧 [GPT-5] Connecting socket');
|
||||
socket.connect();
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnectSocket() {
|
||||
if (socket) {
|
||||
console.log('🔧 [GPT-5] Disconnecting socket');
|
||||
socket.disconnect();
|
||||
currentRoom = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function joinFocusGroup(focus_group_id: string, ack?: (resp: any) => void) {
|
||||
console.log('🔧 [GPT-5] Joining focus group:', focus_group_id);
|
||||
currentRoom = focus_group_id;
|
||||
|
||||
|
||||
if (!socket?.connected) {
|
||||
console.log('🔧 [GPT-5] Socket not connected, will auto-rejoin on connect');
|
||||
// Force connection and try again
|
||||
connectSocket();
|
||||
setTimeout(() => {
|
||||
if (socket?.connected) {
|
||||
console.log('🔧 [GPT-5] Retrying join after connection established');
|
||||
socket.emit("join_focus_group", { focus_group_id }, (resp: any) => {
|
||||
console.log('🔧 [GPT-5] join_focus_group RETRY ACK:', resp);
|
||||
ack?.(resp);
|
||||
});
|
||||
} else {
|
||||
console.log('🔧 [GPT-5] Still not connected, will rejoin on next connect event');
|
||||
socket.emit("join_focus_group", { focus_group_id }, (resp: any) => ack?.(resp));
|
||||
}
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit("join_focus_group", { focus_group_id }, (resp: any) => {
|
||||
console.log('🔧 [GPT-5] join_focus_group ACK:', resp);
|
||||
ack?.(resp);
|
||||
});
|
||||
|
||||
socket.emit("join_focus_group", { focus_group_id }, (resp: any) => ack?.(resp));
|
||||
}
|
||||
|
||||
export function leaveFocusGroup(focus_group_id: string) {
|
||||
console.log('🔧 [GPT-5] Leaving focus group:', focus_group_id);
|
||||
if (currentRoom === focus_group_id) {
|
||||
currentRoom = null;
|
||||
}
|
||||
|
||||
if (socket?.connected) {
|
||||
socket.emit("leave_focus_group", { focus_group_id });
|
||||
}
|
||||
}
|
||||
|
||||
// GPT-5 FIX: Auto-rejoin room on reconnect
|
||||
function rejoinRoom() {
|
||||
if (!socket?.connected || !currentRoom) return;
|
||||
|
||||
console.log('🔧 [GPT-5] Auto-rejoining room after reconnect:', currentRoom);
|
||||
socket.emit("join_focus_group", { focus_group_id: currentRoom });
|
||||
}
|
||||
|
||||
// GPT-5 CRITICAL: Ensure we never lose handlers across reconnects or AI mode toggles
|
||||
function bindCoreListeners() {
|
||||
if (!socket) {
|
||||
console.log('🔧 [GPT-5] bindCoreListeners called but socket is null!');
|
||||
return;
|
||||
}
|
||||
if (!socket) return;
|
||||
|
||||
// GPT-5 FIX: Always rebind listeners on reconnect - don't let flag prevent rebinding
|
||||
if (coreListenersBound) {
|
||||
console.log('🔧 [GPT-5] Listeners already bound, but rebinding anyway for safety');
|
||||
}
|
||||
// Already bound — listeners are re-added via onAny, no need to duplicate
|
||||
if (coreListenersBound) return;
|
||||
|
||||
console.log('🔧 [GPT-5] bindCoreListeners called - socket exists, binding listeners');
|
||||
|
||||
// IMPORTANT: Use stable function references so off() later removes exactly these
|
||||
const onJoined = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] joined_focus_group:', payload);
|
||||
window.dispatchEvent(new CustomEvent("ws:joined_focus_group", { detail: payload }));
|
||||
};
|
||||
|
||||
const onLeft = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] left_focus_group:', payload);
|
||||
window.dispatchEvent(new CustomEvent("ws:left_focus_group", { detail: payload }));
|
||||
};
|
||||
|
||||
const onMsg = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] *** MESSAGE_UPDATE LISTENER FIRED! ***');
|
||||
console.log('🔧 [GPT-5] message_update payload:', payload);
|
||||
console.log('🔧 [GPT-5] DISPATCHING window event ws:message_update');
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent("ws:message_update", { detail: payload }));
|
||||
console.log('🔧 [GPT-5] DISPATCHED window event ws:message_update SUCCESS');
|
||||
} catch (error) {
|
||||
console.error('🔧 [GPT-5] ERROR dispatching window event:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const onAI = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] ai_status_update:', payload);
|
||||
console.log('🔧 [GPT-5] DISPATCHING window event ws:ai_status_update');
|
||||
window.dispatchEvent(new CustomEvent("ws:ai_status_update", { detail: payload }));
|
||||
console.log('🔧 [GPT-5] DISPATCHED window event ws:ai_status_update');
|
||||
};
|
||||
|
||||
const onMod = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] moderator_status_update:', payload);
|
||||
window.dispatchEvent(new CustomEvent("ws:moderator_status_update", { detail: payload }));
|
||||
};
|
||||
|
||||
const onTheme = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] theme_update:', payload);
|
||||
window.dispatchEvent(new CustomEvent("ws:theme_update", { detail: payload }));
|
||||
};
|
||||
|
||||
const onFG = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] focus_group_update:', payload);
|
||||
window.dispatchEvent(new CustomEvent("ws:focus_group_update", { detail: payload }));
|
||||
};
|
||||
|
||||
const onModeEvent = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] mode_event_update:', payload);
|
||||
window.dispatchEvent(new CustomEvent("ws:mode_event_update", { detail: payload }));
|
||||
};
|
||||
|
||||
const onAuthError = (payload: any) => {
|
||||
console.error('🔧 [GPT-5] auth_error:', payload);
|
||||
socket.on("auth_error", (payload: any) => {
|
||||
console.error('[WebSocket] Auth error');
|
||||
window.dispatchEvent(new CustomEvent("ws:auth_error", { detail: payload }));
|
||||
};
|
||||
|
||||
const onBulkExportProgress = (payload: any) => {
|
||||
console.log('🔧 [GPT-5] bulk_export_progress:', payload);
|
||||
window.dispatchEvent(new CustomEvent("ws:bulk_export_progress", { detail: payload }));
|
||||
};
|
||||
|
||||
// Bind all listeners
|
||||
console.log('🔧 [GPT-5] BINDING specific listeners to socket');
|
||||
socket.on("joined_focus_group", onJoined);
|
||||
socket.on("left_focus_group", onLeft);
|
||||
socket.on("message_update", onMsg);
|
||||
socket.on("ai_status_update", onAI);
|
||||
socket.on("moderator_status_update", onMod);
|
||||
socket.on("theme_update", onTheme);
|
||||
socket.on("focus_group_update", onFG);
|
||||
socket.on("mode_event_update", onModeEvent);
|
||||
socket.on("auth_error", onAuthError);
|
||||
socket.on("bulk_export_progress", onBulkExportProgress);
|
||||
console.log('🔧 [GPT-5] BOUND specific listeners to socket');
|
||||
|
||||
// GPT-5 DEBUG: Verify listeners are actually attached
|
||||
console.log('🔧 [GPT-5] Socket listeners after binding:', socket.listeners('message_update').length);
|
||||
console.log('🔧 [GPT-5] Socket hasListeners message_update:', socket.hasListeners('message_update'));
|
||||
|
||||
// GPT-5 TEST: Emit a test event to ourselves to verify binding
|
||||
setTimeout(() => {
|
||||
if (socket?.connected) {
|
||||
console.log('🔧 [GPT-5] SELF-TEST: Emitting test event');
|
||||
(socket as any).emit('message_update', { test: 'self-emit-test' });
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Handle connection events
|
||||
socket.on("connected", (payload: any) => {
|
||||
console.log('🔧 [GPT-5] connected:', payload);
|
||||
});
|
||||
|
||||
socket.on("error", (error: any) => {
|
||||
console.error('🔧 [GPT-5] socket error:', error);
|
||||
console.error('[WebSocket] Socket error:', error?.message || error);
|
||||
});
|
||||
|
||||
coreListenersBound = true;
|
||||
}
|
||||
|
||||
// Utility to get current socket (for debugging)
|
||||
export function getSocket() {
|
||||
return socket;
|
||||
}
|
||||
|
|
@ -341,4 +174,4 @@ export function getSocketId() {
|
|||
|
||||
export function isConnected() {
|
||||
return socket?.connected ?? false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,99 +15,55 @@ import { aiPersonasApi, personasApi, foldersApi } from "@/lib/api";
|
|||
* @param llmModel Optional LLM model to use for generation
|
||||
* @returns Array of generated personas
|
||||
*/
|
||||
/**
|
||||
* Start async persona generation. Returns task_id immediately (202).
|
||||
* Results are delivered via WebSocket task_completed event.
|
||||
*/
|
||||
export async function generateSyntheticPersonas(
|
||||
brief: string,
|
||||
brief: string,
|
||||
researchObjective?: string,
|
||||
count: number,
|
||||
count: number = 5,
|
||||
file?: FileList,
|
||||
targetFolderId?: string | null,
|
||||
llmModel?: string,
|
||||
onTaskIdReceived?: (taskId: string) => void,
|
||||
temperature?: number
|
||||
): Promise<{personas: Persona[], task_id?: string, partial_success?: boolean, errors?: any[]}> {
|
||||
// Debug logging for folder and model
|
||||
console.log(`generateSyntheticPersonas called with targetFolderId: ${targetFolderId || 'none'}`);
|
||||
console.log(`🔄 generateSyntheticPersonas using model: ${llmModel || 'gemini-3-pro-preview'}`);
|
||||
|
||||
try {
|
||||
// We'll use the two-stage approach
|
||||
console.log(`Generating ${count} synthetic personas using two-stage approach...`);
|
||||
|
||||
// First, check if the brief is too short to be useful
|
||||
if (brief.trim().length < 10) {
|
||||
throw new Error("Audience brief is too short. Please provide more context for better persona generation.");
|
||||
}
|
||||
|
||||
// Upload customer data if files provided
|
||||
let customerDataSessionId: string | undefined;
|
||||
if (file && file.length > 0) {
|
||||
console.log(`Uploading ${file.length} customer data files...`);
|
||||
try {
|
||||
const uploadResponse = await aiPersonasApi.uploadCustomerData(file);
|
||||
customerDataSessionId = uploadResponse.data.session_id;
|
||||
console.log(`Customer data uploaded with session ID: ${customerDataSessionId}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to upload customer data:', error);
|
||||
throw new Error("Failed to upload customer data files. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
// Use the new unified generation endpoint
|
||||
console.log('🔥 Calling generatePersonasFull...');
|
||||
console.log(`🌡️ Using temperature: ${temperature || 1.0}`);
|
||||
const response = await aiPersonasApi.generatePersonasFull(
|
||||
brief, // Audience brief
|
||||
researchObjective, // Research objective
|
||||
count, // Number of personas to generate
|
||||
temperature || 1.0, // Temperature (default to 1.0 if not specified)
|
||||
customerDataSessionId, // Customer data session ID
|
||||
llmModel, // LLM model
|
||||
targetFolderId // Target folder ID
|
||||
);
|
||||
|
||||
// Call the task ID callback immediately since unified endpoint returns task_id right away
|
||||
if (response.data.task_id && onTaskIdReceived) {
|
||||
console.log('🔥 Unified endpoint - calling onTaskIdReceived with:', response.data.task_id);
|
||||
onTaskIdReceived(response.data.task_id);
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
// Handle partial success (some personas succeeded, some failed)
|
||||
const hasPartialSuccess = response.data.partial_success === true;
|
||||
const hasPersonas = response.data.personas && response.data.personas.length > 0;
|
||||
const hasErrors = response.data.errors && response.data.errors.length > 0;
|
||||
|
||||
// Include task_id in response if available
|
||||
const result = {
|
||||
personas: response.data.personas,
|
||||
task_id: response.data.task_id,
|
||||
partial_success: hasPartialSuccess,
|
||||
errors: response.data.errors
|
||||
};
|
||||
|
||||
if (hasPersonas) {
|
||||
console.log(`Generated ${response.data.personas.length} personas with unified endpoint${hasErrors ? ` (${response.data.errors.length} failed)` : ''}`);
|
||||
|
||||
// Unified endpoint handles folder assignment and cleanup internally
|
||||
return result;
|
||||
} else if (hasErrors) {
|
||||
// If we have errors but no personas, throw an error
|
||||
throw new Error(`Failed to generate personas: ${response.data.errors.length} generation attempts failed.`);
|
||||
} else {
|
||||
throw new Error("No personas returned from API");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid response format from API");
|
||||
}
|
||||
} catch (error) {
|
||||
// Note: Cleanup is now handled by the unified backend endpoint
|
||||
|
||||
// Don't log 499 errors as they are successful cancellations
|
||||
if (error.response?.status !== 499) {
|
||||
console.error("Error generating AI personas:", error);
|
||||
}
|
||||
throw error;
|
||||
): Promise<{ task_id: string }> {
|
||||
if (brief.trim().length < 10) {
|
||||
throw new Error("Audience brief is too short. Please provide more context for better persona generation.");
|
||||
}
|
||||
|
||||
let customerDataSessionId: string | undefined;
|
||||
if (file && file.length > 0) {
|
||||
try {
|
||||
const uploadResponse = await aiPersonasApi.uploadCustomerData(file);
|
||||
customerDataSessionId = uploadResponse.data.session_id;
|
||||
} catch (error) {
|
||||
console.error('Failed to upload customer data:', error);
|
||||
throw new Error("Failed to upload customer data files. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
const response = await aiPersonasApi.generatePersonasFull(
|
||||
brief,
|
||||
researchObjective,
|
||||
count,
|
||||
temperature || 1.0,
|
||||
customerDataSessionId,
|
||||
llmModel,
|
||||
targetFolderId
|
||||
);
|
||||
|
||||
const task_id = response.data.task_id;
|
||||
if (!task_id) {
|
||||
throw new Error("No task_id returned from server");
|
||||
}
|
||||
|
||||
if (onTaskIdReceived) {
|
||||
onTaskIdReceived(task_id);
|
||||
}
|
||||
|
||||
return { task_id };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue