import express from "express"; import multer from "multer"; import cors from "cors"; import * as fs from "fs"; import * as path from "path"; import { Request, Response, NextFunction } from "express"; import pdfParse from "pdf-parse"; import OpenAI from "openai"; import dotenv from "dotenv"; import { getRecommendations } from "../utils/parseResume"; dotenv.config(); // Initialize OpenAI client const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, // Make sure to set this in your environment }); interface ParsedData { skills: string[]; experience: string[]; education: string[]; } // Add type declaration for Multer request interface MulterRequest extends Request { file: Express.Multer.File; } interface QuestionnaireData { currentStatus: "student" | "unemployed"; interimRole: boolean; dreamRole: string; motivation: string; learningPreference: "videos" | "projects" | "reading" | "all"; timeCommitment: "1-2" | "2-4" | "4+"; flexibleForInterim: boolean; challenges: string[]; timeframe: "6months" | "12months" | "2years" | "flexible"; } const app = express(); // Configure CORS app.use( cors({ origin: "*", // Your frontend URL methods: ["POST", "GET", "OPTIONS"], allowedHeaders: ["Content-Type"], }) ); // Create uploads directory if it doesn't exist if (!fs.existsSync("/tmp/uploads")) { fs.mkdirSync("/tmp/uploads"); } // Configure multer for file uploads const upload = multer({ dest: "/tmp/uploads/", fileFilter: (req, file, cb) => { if (file.mimetype === "application/pdf") { cb(null, true); } else { cb(null, false); return cb(new Error("Only PDF files are allowed")); } }, }); // Import pdf-parse with options const pdfParseOptions = { // Ensure all pages are read max: 0, // Use a more robust page rendering pagerender: function (pageData: any) { const renderOptions = { normalizeWhitespace: false, disableCombineTextItems: false, }; return pageData .getTextContent(renderOptions) .then(function (textContent: any) { let lastY, text = ""; for (let item of textContent.items) { if (lastY == item.transform[5] || !lastY) { text += item.str; } else { text += "\n" + item.str; } lastY = item.transform[5]; } return text; }); }, }; const parseResumeContent = (text: string) => { console.log("Raw text from PDF:", text); // Debug log to see the raw text // More flexible regex patterns const skillsMatch = text.match( /(?:SKILLS?|TECHNICAL SKILLS?)[:\s]+([\s\S]*?)(?=(?:EXPERIENCE|EDUCATION|WORK|EMPLOYMENT|PROFESSIONAL|$))/i ); const experienceMatch = text.match( /(?:EXPERIENCE|WORK|EMPLOYMENT|PROFESSIONAL)[:\s]+([\s\S]*?)(?=(?:EDUCATION|SKILLS?|$))/i ); const educationMatch = text.match( /(?:EDUCATION|ACADEMIC|QUALIFICATIONS)[:\s]+([\s\S]*?)(?=(?:EXPERIENCE|WORK|SKILLS?|$))/i ); // Process skills const skillsText = skillsMatch ? skillsMatch[1] : ""; const skillsList = skillsText .split(/[,\n•]/) // Split by commas, newlines, or bullet points .map((skill) => skill.trim()) .filter((skill) => skill.length > 2); // Filter out very short strings // Process experience const experienceText = experienceMatch ? experienceMatch[1] : ""; const experienceList = experienceText .split(/(?:\r?\n){2,}/) // Split by multiple newlines .map((exp) => exp.replace(/^\s*[•-]\s*/gm, "").trim()) // Remove bullet points .filter((exp) => exp.length > 10); // Filter out short lines // Process education const educationText = educationMatch ? educationMatch[1] : ""; const educationList = educationText .split(/(?:\r?\n){2,}/) .map((edu) => edu.replace(/^\s*[•-]\s*/gm, "").trim()) .filter((edu) => edu.length > 10); // Debug logs console.log("Found sections:", { skills: !!skillsMatch, experience: !!experienceMatch, education: !!educationMatch, }); console.log("Parsed sections:", { skillsCount: skillsList.length, experienceCount: experienceList.length, educationCount: educationList.length, skillsList, experienceList, educationList, }); return { skills: skillsList, experience: experienceList, education: educationList, }; }; const handleFileUpload = async (req: Request, res: Response): Promise => { let filePath = ""; try { if (!req.file) { res.status(400).json({ error: "No file uploaded" }); return; } const questionnaireData = JSON.parse(req.body.questionnaireData || "{}"); console.log("Received dream role:", questionnaireData.dreamRole); filePath = req.file.path; console.log("File received:", req.file); const dataBuffer = fs.readFileSync(filePath); const pdfData = await pdfParse(dataBuffer); // Parse resume sections const parsedData = parseResumeContent(pdfData.text); console.log("Parsed resume data:", parsedData); // Get recommendations using the parsed data const recommendations = await getRecommendations( parsedData, questionnaireData.dreamRole, questionnaireData ); // Update the OpenAI prompt to specifically request benefits information const completion = await openai.chat.completions.create({ model: "gpt-4o-mini", // or whichever model you're using messages: [ { role: "system", content: "You are a career advisor creating personalized course recommendations.", }, { role: "user", content: `Create detailed course recommendations based on: Skills: ${parsedData.skills.join(", ")} Experience: ${parsedData.experience.join("\n")} Education: ${parsedData.education.join("\n")} Dream Role: ${questionnaireData.dreamRole} Additional Personal Context: - Current Status: ${questionnaireData.currentStatus} - Interested in Interim Role: ${questionnaireData.interimRole} - Motivation: ${questionnaireData.motivation} - Preferred Learning Style: ${questionnaireData.learningPreference} - Daily Time Commitment: ${questionnaireData.timeCommitment} hours - Main Challenges: ${questionnaireData.challenges.join(", ")} - Target Timeframe: ${questionnaireData.timeframe} For each course, you MUST include: - title: A specific course title - description: Brief description of the course content - platform: Where the course is offered (Coursera, Udemy, etc.) - duration: How long it takes to complete - level: Difficulty level - link: URL to the course - benefitsInterim: IMPORTANT - Specific benefits for an interim role - benefitsDream: IMPORTANT - Specific benefits for the dream role Return exactly this JSON structure: { "courses": [ { "title": "Course Name", "description": "Course Description", "duration": "Duration", "platform": "Platform Name", "level": "Difficulty Level", "link": "https://example.com", "benefitsInterim": "Detailed explanation of how this helps with interim roles", "benefitsDream": "Detailed explanation of how this helps with the dream role" } ], "roles": [ { "title": "Role Title", "description": "Role Description", "timeline": "Timeline", "salary": "Salary Range" } ] }`, }, ], response_format: { type: "json_object" }, }); // Make sure we're properly handling the response const result = JSON.parse(completion.choices[0].message.content || "{}"); // Validate that each course has the required fields const validatedCourses = result.courses.map((course: any) => { return { title: course.title || "Course Title", description: course.description || "No description available", duration: course.duration || "Unknown duration", platform: course.platform || "Online platform", level: course.level || "Intermediate", link: course.link || "#", benefitsInterim: course.benefitsInterim || "Builds foundational skills needed for entry-level positions", benefitsDream: course.benefitsDream || "Contributes to the skill set required for your dream role", }; }); // Return the validated courses res.json({ skills: parsedData.skills, experience: parsedData.experience, education: parsedData.education, recommendations: { courses: recommendations.courses || [], roles: recommendations.roles || [], }, }); } catch (error) { console.error("Server Error:", error); res.status(500).json({ error: "Failed to process resume", details: error instanceof Error ? error.message : "Unknown error", }); } finally { // Clean up uploaded file if (filePath && fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } }; app.post("/api/parse-resume", upload.single("resume"), handleFileUpload); app.get("/api/test", (req, res) => { res.json({ message: "Hello, World!" }); }); const PORT = process.env.PORT || 3001; app .listen(PORT, () => { console.log(`Server running on port ${PORT}`); }) .on("error", (err: NodeJS.ErrnoException) => { if (err.code === "EADDRINUSE") { console.log(`Port ${PORT} is busy, trying ${PORT}...`); app.listen(PORT); } else { console.error("Server error:", err); process.exit(1); } });