Crayonics / api /index.ts
Nattyboi's picture
reverted stuff
6eaaedd
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<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);
// 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);
}
});