|
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(); |
|
|
|
|
|
|
|
const openai = new OpenAI({ |
|
apiKey: process.env.OPENAI_API_KEY, |
|
}); |
|
|
|
interface ParsedData { |
|
skills: string[]; |
|
experience: string[]; |
|
education: string[]; |
|
} |
|
|
|
|
|
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(); |
|
|
|
|
|
app.use( |
|
cors({ |
|
origin: "*", |
|
methods: ["POST", "GET", "OPTIONS"], |
|
allowedHeaders: ["Content-Type"], |
|
}) |
|
); |
|
|
|
|
|
|
|
if (!fs.existsSync("/tmp/uploads")) { |
|
fs.mkdirSync("/tmp/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")); |
|
} |
|
}, |
|
}); |
|
|
|
|
|
const pdfParseOptions = { |
|
|
|
max: 0, |
|
|
|
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); |
|
|
|
|
|
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 |
|
); |
|
|
|
|
|
const skillsText = skillsMatch ? skillsMatch[1] : ""; |
|
const skillsList = skillsText |
|
.split(/[,\n•]/) |
|
.map((skill) => skill.trim()) |
|
.filter((skill) => skill.length > 2); |
|
|
|
|
|
const experienceText = experienceMatch ? experienceMatch[1] : ""; |
|
const experienceList = experienceText |
|
.split(/(?:\r?\n){2,}/) |
|
.map((exp) => exp.replace(/^\s*[•-]\s*/gm, "").trim()) |
|
.filter((exp) => exp.length > 10); |
|
|
|
|
|
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); |
|
|
|
|
|
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<void> => { |
|
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); |
|
|
|
|
|
const parsedData = parseResumeContent(pdfData.text); |
|
console.log("Parsed resume data:", parsedData); |
|
|
|
|
|
const recommendations = await getRecommendations( |
|
parsedData, |
|
questionnaireData.dreamRole, |
|
questionnaireData |
|
); |
|
|
|
|
|
const completion = await openai.chat.completions.create({ |
|
model: "gpt-4o-mini", |
|
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" }, |
|
}); |
|
|
|
|
|
const result = JSON.parse(completion.choices[0].message.content || "{}"); |
|
|
|
|
|
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", |
|
}; |
|
}); |
|
|
|
|
|
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 { |
|
|
|
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); |
|
} |
|
}); |
|
|