🎯 Features implemented: - Multi-language speech recognition (EN, FR, ES, DE) - CV upload and parsing with regex-escaped skills extraction - Authelia authentication integration for n8n webhook - Complete n8n workflow for AI question generation - Real-time language switching with enhanced UI - Professional authentication modal with dual login options 🔧 Technical stack: - Angular 18 with standalone components and signals - TypeScript with strict typing and interfaces - Authelia session-based authentication - n8n workflow automation with OpenAI integration - PDF.js for CV text extraction - Web Speech API for voice recognition 🛠️ Infrastructure: - Secure authentication flow with proper error handling - Environment-based configuration for dev/prod - Comprehensive documentation and workflow templates - Clean project structure with proper git ignore rules 🔒 Security features: - Cookie-based session management with CORS - Protected n8n webhooks via Authelia - Graceful fallback to local processing - Secure redirect handling and session persistence 🚀 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
189 lines
9.4 KiB
JSON
189 lines
9.4 KiB
JSON
{
|
|
"name": "CV Interview Assistant",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "cv-analysis",
|
|
"responseMode": "responseNode",
|
|
"options": {}
|
|
},
|
|
"id": "f8b4c5d6-7890-1234-5678-90abcdef1234",
|
|
"name": "Webhook",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 1.1,
|
|
"position": [240, 300],
|
|
"webhookId": "cv-analysis-webhook"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Extract and structure CV data for AI processing\nconst input = $input.first().json;\nconst cvProfile = input.cvProfile || {};\nconst personalInfo = cvProfile.personalInfo || {};\nconst skills = cvProfile.skills || [];\nconst experience = cvProfile.experience || [];\nconst education = cvProfile.education || [];\n\n// Extract key information\nconst candidateName = personalInfo.fullName || 'Candidate';\nconst skillNames = skills.map(s => s.name).filter(Boolean);\nconst technicalSkills = skills.filter(s => s.category === 'technical').map(s => s.name);\nconst yearsExperience = Math.max(...skills.map(s => s.yearsOfExperience || 0), 0);\nconst totalPositions = experience.length;\nconst educationLevel = education.length > 0 ? education[0].degree : 'Not specified';\n\n// Create structured prompt data\nconst promptData = {\n analysisId: input.analysisId,\n candidateName: candidateName,\n email: personalInfo.email || '',\n skills: skillNames,\n technicalSkills: technicalSkills,\n yearsExperience: yearsExperience,\n totalPositions: totalPositions,\n educationLevel: educationLevel,\n fullCvText: cvProfile.parsedText || '',\n experienceSummary: experience.map(exp => `${exp.jobTitle} at ${exp.company} (${exp.duration})`).join('; '),\n skillsSummary: skills.map(s => `${s.name}${s.yearsOfExperience ? ` (${s.yearsOfExperience} years)` : ''}`).join(', ')\n};\n\nreturn [{ json: promptData }];"
|
|
},
|
|
"id": "a1b2c3d4-5678-9012-3456-789abcdef012",
|
|
"name": "Extract CV Data",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [460, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "chat",
|
|
"operation": "create",
|
|
"model": "gpt-4",
|
|
"messages": {
|
|
"values": [
|
|
{
|
|
"role": "system",
|
|
"content": "You are an expert interview question generator. Create relevant, insightful interview questions based on the candidate's CV profile."
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": "Generate 10 diverse interview questions for this candidate profile:\n\n**Candidate:** {{$json.candidateName}}\n**Email:** {{$json.email}}\n**Experience:** {{$json.yearsExperience}} years total, {{$json.totalPositions}} positions\n**Education:** {{$json.educationLevel}}\n**Technical Skills:** {{$json.technicalSkills.join(', ')}}\n**All Skills:** {{$json.skillsSummary}}\n**Experience Summary:** {{$json.experienceSummary}}\n\n**Full CV Content:**\n{{$json.fullCvText}}\n\n---\n\nGenerate exactly 10 questions covering:\n- 4 Technical questions (specific to their skills/experience)\n- 3 Behavioral questions (leadership, teamwork, problem-solving)\n- 2 Scenario-based questions (hypothetical situations)\n- 1 Career/Growth question\n\nReturn ONLY a valid JSON array with this exact format:\n```json\n[\n {\n \"id\": 1,\n \"question\": \"Detailed question text here\",\n \"category\": \"technical\",\n \"difficulty\": \"medium\",\n \"expectedSkills\": [\"JavaScript\", \"Problem Solving\"],\n \"reasoning\": \"Why this question is relevant to the candidate\"\n }\n]\n```\n\nEnsure questions are:\n- Specific to the candidate's actual experience\n- Appropriate difficulty level\n- Clear and actionable\n- Professional and respectful"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"temperature": 0.7,
|
|
"maxTokens": 2000
|
|
}
|
|
},
|
|
"id": "b2c3d4e5-6789-0123-4567-89abcdef0123",
|
|
"name": "Generate Questions (OpenAI)",
|
|
"type": "n8n-nodes-base.openAi",
|
|
"typeVersion": 1.3,
|
|
"position": [680, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse and validate AI response\nconst aiResponse = $input.first().json.choices[0].message.content;\nconst extractData = $('Extract CV Data').first().json;\n\ntry {\n // Extract JSON from AI response (handle markdown code blocks)\n let jsonStr = aiResponse;\n if (aiResponse.includes('```json')) {\n const start = aiResponse.indexOf('```json') + 7;\n const end = aiResponse.lastIndexOf('```');\n jsonStr = aiResponse.substring(start, end).trim();\n }\n \n const questions = JSON.parse(jsonStr);\n \n // Validate and enhance questions\n const validatedQuestions = questions.map((q, index) => ({\n id: q.id || (index + 1),\n question: q.question || 'Question not generated',\n category: q.category || 'general',\n difficulty: q.difficulty || 'medium',\n expectedSkills: Array.isArray(q.expectedSkills) ? q.expectedSkills : [],\n reasoning: q.reasoning || 'Standard interview question',\n generatedAt: new Date().toISOString()\n }));\n \n // Generate question bank ID\n const questionBankId = `qb_${extractData.analysisId}_${Date.now()}`;\n \n return [{\n json: {\n status: 'completed',\n analysisId: extractData.analysisId,\n questionBankId: questionBankId,\n questionsGenerated: validatedQuestions.length,\n candidateName: extractData.candidateName,\n questions: validatedQuestions,\n metadata: {\n skillsAnalyzed: extractData.skills.length,\n experienceYears: extractData.yearsExperience,\n processingTime: new Date().toISOString()\n }\n }\n }];\n \n} catch (error) {\n // Handle parsing errors gracefully\n return [{\n json: {\n status: 'failed',\n analysisId: extractData.analysisId,\n error: 'Failed to generate questions',\n errorDetails: error.message,\n fallbackQuestions: [\n {\n id: 1,\n question: `Tell me about your experience with ${extractData.technicalSkills[0] || 'your main technology stack'}.`,\n category: 'technical',\n difficulty: 'medium',\n expectedSkills: extractData.technicalSkills.slice(0, 2),\n reasoning: 'Fallback technical question'\n },\n {\n id: 2,\n question: 'Describe a challenging project you worked on and how you overcame obstacles.',\n category: 'behavioral',\n difficulty: 'medium',\n expectedSkills: ['Problem Solving', 'Communication'],\n reasoning: 'Fallback behavioral question'\n }\n ]\n }\n }];\n}"
|
|
},
|
|
"id": "c3d4e5f6-7890-1234-5678-9abcdef01234",
|
|
"name": "Process AI Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [900, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "json",
|
|
"responseBody": "={{$json}}",
|
|
"options": {
|
|
"responseHeaders": {
|
|
"entries": [
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "Access-Control-Allow-Origin",
|
|
"value": "*"
|
|
},
|
|
{
|
|
"name": "Access-Control-Allow-Methods",
|
|
"value": "POST, OPTIONS"
|
|
},
|
|
{
|
|
"name": "Access-Control-Allow-Headers",
|
|
"value": "Content-Type, Authorization"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"id": "d4e5f6g7-8901-2345-6789-abcdef012345",
|
|
"name": "Return Response",
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1.1,
|
|
"position": [1120, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "error-condition",
|
|
"leftValue": "={{$json.status}}",
|
|
"rightValue": "failed",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "e5f6g7h8-9012-3456-7890-bcdef0123456",
|
|
"name": "Check for Errors",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [1120, 480]
|
|
}
|
|
],
|
|
"connections": {
|
|
"Webhook": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extract CV Data",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract CV Data": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Generate Questions (OpenAI)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Generate Questions (OpenAI)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Process AI Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Process AI Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Return Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"pinData": {},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"staticData": null,
|
|
"tags": [
|
|
{
|
|
"id": "interview-assistant",
|
|
"name": "Interview Assistant"
|
|
}
|
|
],
|
|
"triggerCount": 1,
|
|
"updatedAt": "2025-09-16T22:00:00.000Z",
|
|
"versionId": "1"
|
|
} |