import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; import { logger } from '@/lib/logger'; /** * POST /api/missions/[missionId]/generate-plan * * Generates an action plan by calling N8N webhook which uses an LLM. * Saves the generated plan to the mission. */ export async function POST( request: Request, props: { params: Promise<{ missionId: string }> } ) { const params = await props.params; try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { missionId } = params; if (!missionId) { return NextResponse.json({ error: 'Mission ID is required' }, { status: 400 }); } // Get the mission const mission = await prisma.mission.findUnique({ where: { id: missionId } }); if (!mission) { return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); } // Check if user has permission (creator or admin) const isCreator = mission.creatorId === session.user.id; const userRoles = Array.isArray(session.user.role) ? session.user.role : []; const isAdmin = userRoles.includes('admin') || userRoles.includes('ADMIN'); if (!isCreator && !isAdmin) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } // Prepare data for N8N webhook - must be nested under "mission" key const webhookData = { mission: { name: mission.name, oddScope: mission.oddScope, niveau: mission.niveau, intention: mission.intention, missionType: mission.missionType, donneurDOrdre: mission.donneurDOrdre, projection: mission.projection, services: mission.services, participation: mission.participation, profils: mission.profils, } }; logger.debug('Calling N8N GeneratePlan webhook', { missionId, missionName: mission.name, webhookData }); // Call N8N webhook const webhookUrl = process.env.N8N_GENERATE_PLAN_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/GeneratePlan'; const apiKey = process.env.N8N_API_KEY || ''; const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, body: JSON.stringify(webhookData), }); if (!response.ok) { const errorText = await response.text(); logger.error('N8N GeneratePlan webhook error', { status: response.status, error: errorText.substring(0, 200) }); throw new Error(`Failed to generate plan: ${response.status}`); } // Parse the response const responseText = await response.text(); let actionPlan: string; try { let result = JSON.parse(responseText); // N8N might return an array like [{"response":"..."}] when responseData is "allEntries" if (Array.isArray(result) && result.length > 0) { result = result[0]; logger.debug('N8N returned array, using first element'); } // N8N returns { "response": "..." } based on the workflow actionPlan = result.response || result.plan || result.actionPlan || result.content || result.text || responseText; logger.debug('Parsed N8N response', { hasResponse: !!result.response, responseLength: actionPlan?.length || 0 }); } catch { // If not JSON, use the raw text actionPlan = responseText; logger.debug('Using raw response text', { length: actionPlan.length }); } logger.debug('Received action plan from N8N', { missionId, planLength: actionPlan.length }); // Save the action plan to the mission // Note: Using 'as any' until prisma generate is run to update types const updatedMission = await (prisma.mission as any).update({ where: { id: missionId }, data: { actionPlan: actionPlan, actionPlanGeneratedAt: new Date() } }); logger.debug('Action plan saved successfully', { missionId, generatedAt: updatedMission.actionPlanGeneratedAt }); return NextResponse.json({ success: true, actionPlan: updatedMission.actionPlan, generatedAt: updatedMission.actionPlanGeneratedAt }); } catch (error) { logger.error('Error generating action plan', { error: error instanceof Error ? error.message : String(error), missionId: params.missionId }); return NextResponse.json( { error: 'Failed to generate action plan', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } /** * PUT /api/missions/[missionId]/generate-plan * * Updates the action plan (allows manual editing by creator) */ export async function PUT( request: Request, props: { params: Promise<{ missionId: string }> } ) { const params = await props.params; try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { missionId } = params; if (!missionId) { return NextResponse.json({ error: 'Mission ID is required' }, { status: 400 }); } const body = await request.json(); const { actionPlan } = body; if (typeof actionPlan !== 'string') { return NextResponse.json({ error: 'actionPlan must be a string' }, { status: 400 }); } // Get the mission const mission = await prisma.mission.findUnique({ where: { id: missionId } }); if (!mission) { return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); } // Check if user has permission (creator or admin) const isCreator = mission.creatorId === session.user.id; const userRoles = Array.isArray(session.user.role) ? session.user.role : []; const isAdmin = userRoles.includes('admin') || userRoles.includes('ADMIN'); if (!isCreator && !isAdmin) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } // Update the action plan // Note: Using 'as any' until prisma generate is run to update types const updatedMission = await (prisma.mission as any).update({ where: { id: missionId }, data: { actionPlan: actionPlan } }); logger.debug('Action plan updated successfully', { missionId }); return NextResponse.json({ success: true, actionPlan: updatedMission.actionPlan }); } catch (error) { logger.error('Error updating action plan', { error: error instanceof Error ? error.message : String(error), missionId: params.missionId }); return NextResponse.json( { error: 'Failed to update action plan', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } }