diff --git a/.gitignore b/.gitignore index 07e0701..2d784b1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ Thumbs.db # Build output dist/ build/ +.vercel diff --git a/Backend/controllers/Interview.controller.js b/Backend/controllers/Interview.controller.js index 7b23720..11e6d0f 100644 --- a/Backend/controllers/Interview.controller.js +++ b/Backend/controllers/Interview.controller.js @@ -673,6 +673,10 @@ import emotionAnalyzer from "../services/emotionAnalyzer.js"; import resultsGenerator from "../services/resultsGenerator.js"; import mongoose from "mongoose"; import groqService from "../services/groqService.js"; +import fs from "fs"; +import pdfParse from "pdf-parse"; +import { uploadToCloudinary } from "../config/cloudinary.js"; +import InterviewSession from "../models/InterviewSession.js"; // Helper function to safely get number function safeNumber(value, defaultValue = 0) { @@ -710,26 +714,14 @@ class InterviewController { const userId = req.user.id; - if (!domain || !subDomain || !interviewType) { - return res.status(400).json({ - success: false, - message: "Please provide all required fields (domain, subDomain, interviewType)", - }); - } + let role = req.body.role; + let resumeUrl = ""; + let resumeText = ""; + let scheduledAt = req.body.scheduledAt || new Date(); - const validTypes = [ - "behavioral", - "technical", - "system-design", - "coding", - "mixed", - ]; - if (!validTypes.includes(interviewType)) { - return res.status(400).json({ - success: false, - message: `Invalid interview type. Must be one of: ${validTypes.join(", ")}`, - }); - } + let finalDomain = domain; + let finalSubDomain = subDomain; + let finalInterviewType = interviewType || "mixed"; let parsedQuestions = []; if (customQuestions) { @@ -745,13 +737,96 @@ class InterviewController { } } + if (role) { + // Resume-based flow + const resumeFile = req.files && req.files.resume; + if (!resumeFile) { + return res.status(400).json({ + success: false, + message: "Resume file is required", + }); + } + + // 1. Parse resume text + try { + if (/\.pdf$/i.test(resumeFile.name)) { + const fileBuffer = await fs.promises.readFile(resumeFile.tempFilePath); + const pdfData = await pdfParse(fileBuffer); + resumeText = pdfData.text; + } else { + resumeText = `Resume content of ${resumeFile.name}`; + } + } catch (parseError) { + console.error("Error parsing resume:", parseError); + resumeText = "Error parsing resume content"; + } + + // 2. Upload resume to Cloudinary + try { + const uploadResult = await uploadToCloudinary(resumeFile, "resumes"); + resumeUrl = uploadResult.secure_url; + } catch (uploadError) { + console.error("Cloudinary upload error:", uploadError); + return res.status(500).json({ + success: false, + message: "Failed to upload resume to Cloudinary", + error: uploadError.message, + }); + } + + // 3. Infer domain, subDomain, and interviewType from role + const roleLower = role.toLowerCase(); + finalDomain = "Engineering"; + finalSubDomain = role; + + if (roleLower.includes("front")) { + finalDomain = "Frontend"; + } else if (roleLower.includes("back")) { + finalDomain = "Backend"; + } else if (roleLower.includes("full")) { + finalDomain = "Full Stack"; + } else if (roleLower.includes("data")) { + finalDomain = "Data Science"; + } else if (roleLower.includes("devops")) { + finalDomain = "DevOps"; + } else if (roleLower.includes("mobile") || roleLower.includes("android") || roleLower.includes("ios")) { + finalDomain = "Mobile"; + } + } else { + // Direct configuration flow + if (!finalDomain || !finalSubDomain || !finalInterviewType) { + return res.status(400).json({ + success: false, + message: "Please provide all required fields (domain, subDomain, interviewType)", + }); + } + + const validTypes = [ + "behavioral", + "technical", + "system-design", + "coding", + "mixed", + ]; + if (!validTypes.includes(finalInterviewType)) { + return res.status(400).json({ + success: false, + message: `Invalid interview type. Must be one of: ${validTypes.join(", ")}`, + }); + } + } + // Create interview const interview = new Interview({ userId, + role, + resumeUrl, + resumeText, + scheduledAt, config: { - domain, - subDomain, - interviewType, + domain: finalDomain, + subDomain: finalSubDomain, + interviewType: finalInterviewType, difficulty: difficulty || "medium", duration: duration || 30, targetCompany: targetCompany || "General", @@ -1999,6 +2074,64 @@ Be encouraging but honest. Provide specific, actionable feedback.`, }); } } + + // Get Interview by ID + async getInterviewById(req, res) { + try { + const { interviewId } = req.params; + const userId = req.user.id; + + const interview = await Interview.findOne({ _id: interviewId, userId }); + + if (!interview) { + return res.status(404).json({ + success: false, + message: "Interview not found", + }); + } + + res.status(200).json({ + success: true, + data: interview, + }); + } catch (error) { + console.error("Get interview by ID error:", error); + res.status(500).json({ + success: false, + message: "Failed to fetch interview details", + error: error.message, + }); + } + } + + // Get Interview Session + async getInterviewSession(req, res) { + try { + const { interviewId } = req.params; + const userId = req.user.id; + + const session = await InterviewSession.findOne({ interviewId, userId }); + + if (!session) { + return res.status(404).json({ + success: false, + message: "Interview session not found", + }); + } + + res.status(200).json({ + success: true, + data: session, + }); + } catch (error) { + console.error("Get interview session error:", error); + res.status(500).json({ + success: false, + message: "Failed to fetch interview session", + error: error.message, + }); + } + } } export default new InterviewController(); \ No newline at end of file diff --git a/Backend/models/Interview.model.js b/Backend/models/Interview.model.js index 68b0ffe..1ebf81f 100644 --- a/Backend/models/Interview.model.js +++ b/Backend/models/Interview.model.js @@ -7,13 +7,17 @@ const InterviewSchema = new mongoose.Schema({ ref: "User", required: true, }, + role: { type: String }, + resumeUrl: { type: String }, + resumeText: { type: String }, + scheduledAt: { type: Date }, config: { - domain: { type: String, required: true }, - subDomain: { type: String, required: true }, + domain: { type: String }, + subDomain: { type: String }, interviewType: { type: String, enum: ["behavioral", "technical", "system-design", "coding", "mixed"], - required: true, + default: "mixed", }, difficulty: { type: String, diff --git a/Backend/routes/interview.route.js b/Backend/routes/interview.route.js index a3975f6..0e0c02e 100644 --- a/Backend/routes/interview.route.js +++ b/Backend/routes/interview.route.js @@ -80,8 +80,13 @@ router.get( interviewController.getDetailedResults, ); -// ==================== HISTORY ==================== // Get interview history router.get("/history", interviewController.getInterviewHistory); +// Get single interview details +router.get("/:interviewId", interviewController.getInterviewById); + +// Get interview session +router.get("/:interviewId/session", interviewController.getInterviewSession); + export default router; diff --git a/Frontend/src/components/AiInterview/InterviewSetup.jsx b/Frontend/src/components/AiInterview/InterviewSetup.jsx index 6e76c4b..9d935db 100644 --- a/Frontend/src/components/AiInterview/InterviewSetup.jsx +++ b/Frontend/src/components/AiInterview/InterviewSetup.jsx @@ -91,7 +91,7 @@ const InterviewSetup = () => { try { const interview = await createInterview(data, setLoading, token); - navigate(`/interview/${interview._id}`); + navigate(`/interview/${interview.id || interview.interviewId}`); } catch (error) { console.error("Setup error:", error); } diff --git a/Frontend/src/services/operations/authAPI.js b/Frontend/src/services/operations/authAPI.js index cff41fd..c2b4e8c 100644 --- a/Frontend/src/services/operations/authAPI.js +++ b/Frontend/src/services/operations/authAPI.js @@ -171,9 +171,9 @@ export function logout(navigate) { localStorage.removeItem("token"); localStorage.removeItem("user"); navigate("/"); - } catch (e) { - console.error("Login Error:", error); - customToast.error(error.response?.data?.message || "Login failed"); + } catch (error) { + console.error("Logout Error:", error); + customToast.error(error.response?.data?.message || "Logout failed"); } finally { customToast.success("Logged out successfully"); customToast.dismiss(toastId); diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..8175065 --- /dev/null +++ b/vercel.json @@ -0,0 +1,28 @@ +{ + "services": { + "frontend": { + "root": "Frontend", + "framework": "vite" + }, + "backend": { + "root": "Backend", + "entrypoint": "index.js" + } + }, + "rewrites": [ + { + "source": "/api(/.*)?", + "destination": { + "type": "service", + "service": "backend" + } + }, + { + "source": "/(.*)", + "destination": { + "type": "service", + "service": "frontend" + } + } + ] +}