From 61cc5aed49b1c84d14524adefc766da9a365129c Mon Sep 17 00:00:00 2001 From: Deneth Pinsara Date: Mon, 18 Nov 2024 02:20:42 +0530 Subject: [PATCH 1/5] Revert "chore( model): update the ml-model with all changes" This reverts commit 94f912cd2e7297ffd5dfcb8252cbfa1b62a1b9e4. --- backend/controllers/authController.js | 82 ++++++++---- .../candidate/classificationController.js | 21 +++ .../candidate/predictionController.js | 18 +++ backend/controllers/predictController.js | 10 +- backend/models/candidate/Prediction.js | 10 ++ .../models/candidate/imageClassification.js | 22 ++++ backend/package-lock.json | 3 + backend/routes/auth.js | 3 +- .../routes/candidate/classificationRoutes.js | 22 ++++ backend/routes/candidate/predictionRoutes.js | 7 + backend/server.js | 9 +- frontend/AIPT/package-lock.json | 50 ++++++- frontend/AIPT/package.json | 6 +- frontend/AIPT/src/App.tsx | 73 ++++++----- .../components/Candidate/CandidateSidebar.tsx | 6 +- frontend/AIPT/src/components/Login.tsx | 14 +- .../AIPT/src/pages/Candidate/MockupTest.tsx | 123 ++++++++++++++++++ frontend/AIPT/src/pages/Candidate/Results.tsx | 34 +++++ 18 files changed, 445 insertions(+), 68 deletions(-) create mode 100644 backend/controllers/candidate/classificationController.js create mode 100644 backend/controllers/candidate/predictionController.js create mode 100644 backend/models/candidate/Prediction.js create mode 100644 backend/models/candidate/imageClassification.js create mode 100644 backend/routes/candidate/classificationRoutes.js create mode 100644 backend/routes/candidate/predictionRoutes.js create mode 100644 frontend/AIPT/src/pages/Candidate/MockupTest.tsx create mode 100644 frontend/AIPT/src/pages/Candidate/Results.tsx diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 928996a..5bd518d 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -1,6 +1,6 @@ -const User = require('../models/User'); -const bcrypt = require('bcryptjs'); -const jwt = require('jsonwebtoken'); +const User = require("../models/User"); +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); // Register User const register = async (req, res) => { @@ -10,7 +10,7 @@ const register = async (req, res) => { // Check if user already exists const existingUser = await User.findOne({ email }); if (existingUser) { - return res.status(400).json({ message: 'User already exists' }); + return res.status(400).json({ message: "User already exists" }); } // Create new user (hashing is done automatically in the User schema) @@ -18,7 +18,7 @@ const register = async (req, res) => { email, password, // No need to hash here, it's handled in the schema role, - name + name, }); await newUser.save(); @@ -27,65 +27,95 @@ const register = async (req, res) => { const token = jwt.sign( { id: newUser._id, role: newUser.role }, process.env.JWT_SECRET, - { expiresIn: '1h' } + { expiresIn: "1h" } ); // Return the token and user role res.status(201).json({ token, - role: newUser.role + role: newUser.role, }); - } catch (error) { - console.error('Error during registration:', error); - res.status(500).json({ message: 'Server error' }); + console.error("Error during registration:", error); + res.status(500).json({ message: "Server error" }); } }; - // Login User const login = async (req, res) => { const { email, password } = req.body; try { - console.log('Checking for user with email:', email); + console.log("Checking for user with email:", email); // Check if user exists const user = await User.findOne({ email }); if (!user) { - console.log('User not found'); - return res.status(404).json({ message: 'User not found' }); // Added return to stop further execution + console.log("User not found"); + return res.status(404).json({ message: "User not found" }); // Return response and stop further execution } - console.log('User found:'); + console.log("User found:"); // Check if password matches const isMatch = await user.comparePassword(password); if (!isMatch) { - console.log('Password mismatch'); - return res.status(400).json({ message: 'Invalid credentials' }); // Added return to stop further execution + console.log("Password mismatch"); + return res.status(400).json({ message: "Invalid credentials" }); // Return response and stop further execution } - console.log('Password match, generating token'); + console.log("Password match, generating token"); // Generate JWT token const token = jwt.sign( - { id: user._id, role: user.role }, + { id: user._id, role: user.role, email: user.email }, // Include email in JWT payload process.env.JWT_SECRET, - { expiresIn: '1h' } + { expiresIn: "1h" } // Token expiration time ); - // Send back the token and user role + // Send back the token, user role, and email return res.json({ token, - role: user.role - }); // Ensure this is the last response + role: user.role, + email: user.email, // Include email in the response + }); + } catch (error) { + console.error("Error during login:", error); + return res.status(500).json({ message: "Server error" }); // Return response and stop further execution + } +}; + + +// Get logged-in user data based on email query +const getUserData = async (req, res) => { + const { email } = req.query; // Retrieve the email from query parameters + try { + // Validate if email exists + if (!email) { + return res.status(400).json({ message: "Email is required" }); + } + + // Find user by email + const user = await User.findOne({ email }).select("-password"); // Exclude the password field + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + // Return user data + res.json({ + id: user._id, + name: user.name, + email: user.email, + role: user.role, + // Add any other fields you want to return + }); } catch (error) { - console.error('Error during login:', error); - return res.status(500).json({ message: 'Server error' }); // Added return to stop further execution + console.error("Error fetching user data:", error); + res.status(500).json({ message: "Server error" }); } }; + //--- -module.exports = { login , register }; +module.exports = { login, register, getUserData }; diff --git a/backend/controllers/candidate/classificationController.js b/backend/controllers/candidate/classificationController.js new file mode 100644 index 0000000..fcb4640 --- /dev/null +++ b/backend/controllers/candidate/classificationController.js @@ -0,0 +1,21 @@ +const ImageClassification = require('../../models/candidate/imageClassification'); + +// Function to store classification result +const storePrediction = async (email, prediction) => { + try { + const newClassification = new ImageClassification({ + email: email, + prediction: prediction, + }); + await newClassification.save(); + return { success: true, message: 'Prediction saved successfully' }; + } catch (error) { + console.error('Error saving prediction:', error); + return { success: false, message: 'Error saving prediction' }; + } +}; + + +module.exports = { + storePrediction, +}; diff --git a/backend/controllers/candidate/predictionController.js b/backend/controllers/candidate/predictionController.js new file mode 100644 index 0000000..28127f1 --- /dev/null +++ b/backend/controllers/candidate/predictionController.js @@ -0,0 +1,18 @@ +const Prediction = require("../../models/candidate/Prediction"); + +exports.savePrediction = async (req, res) => { + const { email, prediction } = req.body; + + if (!email || !prediction) { + return res.status(400).json({ error: "Email and prediction are required" }); + } + + try { + const newPrediction = new Prediction({ email, prediction }); + await newPrediction.save(); + return res.status(200).json({ message: "Prediction saved successfully" }); + } catch (error) { + console.error(error); + return res.status(500).json({ error: "Failed to save prediction" }); + } +}; diff --git a/backend/controllers/predictController.js b/backend/controllers/predictController.js index 6ad2e79..542ede3 100644 --- a/backend/controllers/predictController.js +++ b/backend/controllers/predictController.js @@ -3,6 +3,7 @@ const FormData = require('form-data'); const fs = require('fs'); const path = require('path'); const ffmpeg = require('fluent-ffmpeg'); // Import fluent-ffmpeg +const Prediction = require('../models/candidate/Prediction'); exports.predictConfidence = async (req, res) => { try { @@ -44,6 +45,13 @@ exports.predictConfidence = async (req, res) => { console.log('Flask response:', flaskResponse.data); // Log Flask response + // Save prediction in the database + const newPrediction = new Prediction({ + email: req.body.email, + prediction: flaskResponse.data.prediction + }); + await newPrediction.save(); + // Clean up the temporary converted audio file fs.unlink(tempFilePath, (err) => { if (err) { @@ -55,7 +63,7 @@ exports.predictConfidence = async (req, res) => { return res.status(200).json({ success: true, - prediction: flaskResponse.data + prediction: flaskResponse.data.prediction }); } catch (error) { console.error('Error in Flask response:', error.message); diff --git a/backend/models/candidate/Prediction.js b/backend/models/candidate/Prediction.js new file mode 100644 index 0000000..49652e8 --- /dev/null +++ b/backend/models/candidate/Prediction.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); + +const predictionSchema = new mongoose.Schema({ + email: { type: String, required: true }, + prediction: { type: String, required: true }, + timestamp: { type: Date, default: Date.now } +}); + +const Prediction = mongoose.model('Prediction', predictionSchema); +module.exports = Prediction; diff --git a/backend/models/candidate/imageClassification.js b/backend/models/candidate/imageClassification.js new file mode 100644 index 0000000..5f8f09d --- /dev/null +++ b/backend/models/candidate/imageClassification.js @@ -0,0 +1,22 @@ +const mongoose = require('mongoose'); + +// Define the schema for image classification results +const imageClassificationSchema = new mongoose.Schema({ + email: { + type: String, + required: true, + unique: true, + }, + prediction: { + type: [String], + required: true, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +const ImageClassification = mongoose.model('ImageClassification', imageClassificationSchema); + +module.exports = ImageClassification; diff --git a/backend/package-lock.json b/backend/package-lock.json index 3161ab4..b6e1bcd 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -464,6 +464,7 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -538,6 +539,7 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -677,6 +679,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "license": "MIT", "dependencies": { "async": "^0.2.9", "which": "^1.1.1" diff --git a/backend/routes/auth.js b/backend/routes/auth.js index bd4682e..b47d20b 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { register, login } = require('../controllers/authController'); +const { register, login, getUserData } = require('../controllers/authController'); // @route POST api/auth/register // @desc Register a new user @@ -11,5 +11,6 @@ router.post('/register', register); // @desc Login a user // @access Public router.post('/login', login); +router.get('/login', getUserData); module.exports = router; diff --git a/backend/routes/candidate/classificationRoutes.js b/backend/routes/candidate/classificationRoutes.js new file mode 100644 index 0000000..107e1b9 --- /dev/null +++ b/backend/routes/candidate/classificationRoutes.js @@ -0,0 +1,22 @@ +const express = require('express'); +const router = express.Router(); +const classificationController = require('../../controllers/candidate/classificationController'); + +// Route to store the prediction result +router.post('/store-prediction', async (req, res) => { + const { email, prediction } = req.body; + + if (!email || !prediction) { + return res.status(400).json({ error: 'Email and prediction are required' }); + } + + const result = await classificationController.storePrediction(email, prediction); + + if (result.success) { + return res.status(200).json({ message: result.message }); + } else { + return res.status(500).json({ error: result.message }); + } +}); + +module.exports = router; diff --git a/backend/routes/candidate/predictionRoutes.js b/backend/routes/candidate/predictionRoutes.js new file mode 100644 index 0000000..a7e0d0c --- /dev/null +++ b/backend/routes/candidate/predictionRoutes.js @@ -0,0 +1,7 @@ +const express = require('express'); +const router = express.Router(); +const predictionController = require('../../controllers/candidate/predictionController'); + +router.post('/savePrediction', predictionController.savePrediction); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index d01c39e..0ebd487 100644 --- a/backend/server.js +++ b/backend/server.js @@ -14,15 +14,14 @@ app.use('/api/auth', require('./routes/auth')); app.use('/api/jobs', require('./routes/employer/JobsRoutes')); app.use('/api/questions', require('./routes/employer/questionRoutes')); app.use('/api/skillGroups', require('./routes/employer/skillGroupRoutes')); -app.use('/api/audio', require('./routes/audioRoutes')); - - - +app.use('/api/classification', require('./routes/candidate/predictionRoutes')); // voice confidence routes app.use('/api', require('./routes/voiceConfidenceRoutes')); - +// image classification routes +app.use('/api', require('./routes/candidate/predictionRoutes')); +app.use('/api/classification', require('./routes/candidate/classificationRoutes')); // MongoDB Connection diff --git a/frontend/AIPT/package-lock.json b/frontend/AIPT/package-lock.json index ba49693..747ff49 100644 --- a/frontend/AIPT/package-lock.json +++ b/frontend/AIPT/package-lock.json @@ -8,6 +8,7 @@ "name": "aipt", "version": "0.0.0", "dependencies": { + "@types/axios": "^0.9.36", "@types/react-router-dom": "^5.3.3", "aipt": "file:", "axios": "^1.7.7", @@ -16,12 +17,15 @@ "react-icons": "^5.3.0", "react-media-recorder": "^1.7.1", "react-router-dom": "^6.26.2", - "react-toastify": "^10.0.5" + "react-toastify": "^10.0.5", + "react-webcam": "^7.2.0" }, "devDependencies": { "@eslint/js": "^9.9.0", + "@types/node": "^22.9.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/react-webcam": "^1.1.0", "@types/recorder-js": "^1.0.4", "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^9.9.0", @@ -1026,6 +1030,12 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@types/axios": { + "version": "0.9.36", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", + "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1043,6 +1053,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -1085,6 +1105,16 @@ "@types/react-router": "*" } }, + "node_modules/@types/react-webcam": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/react-webcam/-/react-webcam-1.1.0.tgz", + "integrity": "sha512-D0Vzo8FoBX5Htj0RpxAiXVaeuoT3UAw3XJUPe6t4aXrg+stFFNjRyZ0b573gahuaisMB5uPu80k548cBvw4+kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/recorder-js": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/recorder-js/-/recorder-js-1.0.4.tgz", @@ -1413,6 +1443,7 @@ "version": "1.7.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2625,6 +2656,16 @@ "react-dom": ">=18" } }, + "node_modules/react-webcam": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-webcam/-/react-webcam-7.2.0.tgz", + "integrity": "sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.2.0", + "react-dom": ">=16.2.0" + } + }, "node_modules/recorder-audio-worklet": { "version": "5.1.39", "resolved": "https://registry.npmjs.org/recorder-audio-worklet/-/recorder-audio-worklet-5.1.39.tgz", @@ -2954,6 +2995,13 @@ } } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/frontend/AIPT/package.json b/frontend/AIPT/package.json index f71e39a..88fdd95 100644 --- a/frontend/AIPT/package.json +++ b/frontend/AIPT/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@types/axios": "^0.9.36", "@types/react-router-dom": "^5.3.3", "aipt": "file:", "axios": "^1.7.7", @@ -18,12 +19,15 @@ "react-icons": "^5.3.0", "react-media-recorder": "^1.7.1", "react-router-dom": "^6.26.2", - "react-toastify": "^10.0.5" + "react-toastify": "^10.0.5", + "react-webcam": "^7.2.0" }, "devDependencies": { "@eslint/js": "^9.9.0", + "@types/node": "^22.9.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/react-webcam": "^1.1.0", "@types/recorder-js": "^1.0.4", "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^9.9.0", diff --git a/frontend/AIPT/src/App.tsx b/frontend/AIPT/src/App.tsx index 64e5089..799e8e1 100644 --- a/frontend/AIPT/src/App.tsx +++ b/frontend/AIPT/src/App.tsx @@ -1,19 +1,20 @@ -import React from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; -import Login from './components/Login'; -import Register from './components/Register'; -import InterviewerHome from './pages/Interviewer/InterviewerHome'; -import CandidateHome from './pages/Candidate/CandidateHome'; -import PrivateRoute from './components/PrivateRoute'; -import Home from './pages/Home'; -import Profile from './pages/Interviewer/Profile'; -import Interviews from './pages/Interviewer/Interviews'; -import Assignments from './pages/Candidate/Assignments'; -import CandidateProfile from './pages/Candidate/CandidateProfile'; -import Settings from './pages/Candidate/Settings'; +import React from "react"; +import { Routes, Route, Navigate } from "react-router-dom"; +import Login from "./components/Login"; +import Register from "./components/Register"; +import InterviewerHome from "./pages/Interviewer/InterviewerHome"; +import CandidateHome from "./pages/Candidate/CandidateHome"; +import PrivateRoute from "./components/PrivateRoute"; +import Home from "./pages/Home"; +import Profile from "./pages/Interviewer/Profile"; +import Interviews from "./pages/Interviewer/Interviews"; +import Assignments from "./pages/Candidate/Assignments"; +import CandidateProfile from "./pages/Candidate/CandidateProfile"; +import Settings from "./pages/Candidate/Settings"; +import MockupTest from "./pages/Candidate/MockupTest"; +import Results from "./pages/Candidate/Results"; const App: React.FC = () => { - return ( } /> @@ -25,7 +26,7 @@ const App: React.FC = () => { + } @@ -33,7 +34,7 @@ const App: React.FC = () => { + } @@ -41,19 +42,18 @@ const App: React.FC = () => { + } /> - {/* Routes for Candidates */} + } @@ -61,7 +61,7 @@ const App: React.FC = () => { + } @@ -69,15 +69,32 @@ const App: React.FC = () => { + } /> + + + + } + /> + + + + } + /> + + } @@ -86,19 +103,13 @@ const App: React.FC = () => { - + + } /> - - - - - - ); -} +}; export default App; diff --git a/frontend/AIPT/src/components/Candidate/CandidateSidebar.tsx b/frontend/AIPT/src/components/Candidate/CandidateSidebar.tsx index 35bcf27..228a433 100644 --- a/frontend/AIPT/src/components/Candidate/CandidateSidebar.tsx +++ b/frontend/AIPT/src/components/Candidate/CandidateSidebar.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { FaHome, FaTasks, FaUserAlt, FaCog, FaBars } from 'react-icons/fa'; +import { FaHome, FaTasks, FaUserAlt, FaCog, FaPen, FaBars } from 'react-icons/fa'; import { Link } from 'react-router-dom'; import './candidateSidebar.css'; @@ -24,6 +24,10 @@ const CandidateSidebar: React.FC = () => { {!isCollapsed && Assignments} +
  • + + {!isCollapsed && Mockup-Test} +
  • {!isCollapsed && Profile} diff --git a/frontend/AIPT/src/components/Login.tsx b/frontend/AIPT/src/components/Login.tsx index 7e054de..2a97cea 100644 --- a/frontend/AIPT/src/components/Login.tsx +++ b/frontend/AIPT/src/components/Login.tsx @@ -4,6 +4,13 @@ import { useNavigate } from 'react-router-dom'; import '../Auth.css'; import image from '../images/loginBG.jpg'; +// Define the response type for login +interface LoginResponse { + token: string; + role: string; + email: string; +} + const Login: React.FC = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -12,9 +19,14 @@ const Login: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - const response = await axios.post('http://localhost:5000/api/auth/login', { email, password }); + // Specify the response type as LoginResponse + const response = await axios.post('http://localhost:5000/api/auth/login', { email, password }); + + // Now TypeScript knows the structure of response.data localStorage.setItem('token', response.data.token); localStorage.setItem('role', response.data.role); + localStorage.setItem('email', response.data.email); + localStorage.setItem("authToken", response.data.token); // Fixed the error here if (response.data.role === 'interviewer') { navigate('/interviewer-home'); diff --git a/frontend/AIPT/src/pages/Candidate/MockupTest.tsx b/frontend/AIPT/src/pages/Candidate/MockupTest.tsx new file mode 100644 index 0000000..2ec3b7f --- /dev/null +++ b/frontend/AIPT/src/pages/Candidate/MockupTest.tsx @@ -0,0 +1,123 @@ +import React, { useEffect, useRef, useState } from "react"; +import Webcam from "react-webcam"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; + +// Define the PredictionResponse type +interface PredictionResponse { + prediction: string; +} + +const MockupTest: React.FC = () => { + const webcamRef = useRef(null); + const [isTesting, setIsTesting] = useState(false); // Track test state + const navigate = useNavigate(); + + // Retrieve the candidate's email from localStorage + const candidateEmail = localStorage.getItem("email"); + + // Redirect to login if no email is found + useEffect(() => { + if (!candidateEmail) { + console.error("No candidate email found in localStorage"); + navigate("/login"); + } + }, [candidateEmail, navigate]); + + // Capture a screenshot from the webcam + const captureScreenshot = async () => { + if (webcamRef.current) { + const imageSrc = webcamRef.current.getScreenshot(); + if (imageSrc) { + try { + // Convert base64 image to Blob + const blob = await fetch(imageSrc).then((res) => res.blob()); + const file = new File([blob], "screenshot.jpg", { + type: "image/jpeg", + }); + + // Create form data for Flask API + const formData = new FormData(); + formData.append("file", file); + formData.append("email", candidateEmail || ""); + + // Send the image to Flask for classification + const flaskResponse = await axios.post( + "http://127.0.0.1:5000/predict", + formData, + { + headers: { "Content-Type": "multipart/form-data" }, + } + ); + + console.log("Prediction received:", flaskResponse.data.prediction); + + // Store the prediction as a string in localStorage + localStorage.setItem( + "Prediction", + JSON.stringify(flaskResponse.data.prediction) + ); + + // Send the prediction as an array to Node.js backend + const predictionArray = Array.isArray(flaskResponse.data.prediction) + ? flaskResponse.data.prediction + : [flaskResponse.data.prediction]; // Ensure prediction is an array + + // Send the prediction array to Node.js backend + await axios.post( + "http://localhost:5000/api/classification/store-prediction", + { + email: candidateEmail, + prediction: predictionArray, + } + ); + } catch (error) { + console.error("Error during prediction or save:", error); + } + } + } + }; + + // Start the test and begin capturing screenshots + const startTest = () => { + setIsTesting(true); + }; + + // End the test and navigate to the results page + const endTest = () => { + setIsTesting(false); // Stop testing + navigate(`/candidate-mockup-results/${candidateEmail}`); // Redirect to results page with actual email + }; + + // Capture screenshots every 30 seconds while testing + useEffect(() => { + let interval: NodeJS.Timeout | null = null; + + if (isTesting) { + interval = setInterval(() => { + captureScreenshot(); + }, 30000); // 30 seconds interval + } + + // Cleanup the interval on component unmount or when testing stops + return () => { + if (interval) clearInterval(interval); + }; + }, [isTesting]); + + return ( +
    +

    Mockup Test

    +

    Email of Candidate: {candidateEmail}

    {" "} + {/* Display candidate's email */} + + {!isTesting ? ( + + ) : ( + + )} +
    + ); +}; + +export default MockupTest; diff --git a/frontend/AIPT/src/pages/Candidate/Results.tsx b/frontend/AIPT/src/pages/Candidate/Results.tsx new file mode 100644 index 0000000..89ff160 --- /dev/null +++ b/frontend/AIPT/src/pages/Candidate/Results.tsx @@ -0,0 +1,34 @@ +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; // Import useParams + +// Define the type for the response +interface PredictionResponse { + prediction: string; + error?: string; +} + +const Results = () => { + const { email } = useParams<{ email: string }>(); // Get email from route parameters + const [error, setError] = useState(null); + const [prediction, setPrediction] = useState(null); // State for prediction + const [candidateEmail, setCandidateEmail] = useState(null); // State for email + + // Use localStorage to retrieve email and prediction + useEffect(() => { + const storedEmail = localStorage.getItem("email"); + const storedPrediction = localStorage.getItem("Prediction"); + + if (storedEmail) setCandidateEmail(storedEmail); + if (storedPrediction) setPrediction(storedPrediction); + }, []); // Only run on component mount + + return ( +
    + {error &&

    {error}

    } + {prediction &&

    Prediction: {prediction}

    } + {candidateEmail &&

    Email: {candidateEmail}

    } +
    + ); +}; + +export default Results; From 0b2339647558846a454c0b70137f5d5c34f0db5b Mon Sep 17 00:00:00 2001 From: Deneth Pinsara Date: Mon, 18 Nov 2024 02:27:36 +0530 Subject: [PATCH 2/5] chore(microservice): implement the micro-service for model functioning --- .gitignore | 1 + .../candidate_classification_service/app.py | 76 +++++++++++++++++++ .../requirements.txt | 4 + 3 files changed, 81 insertions(+) create mode 100644 backend/candidate_classification_service/app.py create mode 100644 backend/candidate_classification_service/requirements.txt diff --git a/.gitignore b/.gitignore index 931c7c5..5bc5912 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ frontend/.env backend/.env backend/uploads +backend/candidate_classification_service/model/final_trained_model.pth backend/node_modules frontend/node_modules diff --git a/backend/candidate_classification_service/app.py b/backend/candidate_classification_service/app.py new file mode 100644 index 0000000..9d838ca --- /dev/null +++ b/backend/candidate_classification_service/app.py @@ -0,0 +1,76 @@ +import os +import torch +from flask import Flask, request, jsonify +from flask_cors import CORS +from PIL import Image +from torchvision import models, transforms +import io +import requests # To send the result to Node.js + +# Initialize Flask app +app = Flask(__name__) +CORS(app) + +# Define device and load the model +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# Load the trained model +model_path = './model/final_trained_model.pth' +model = models.resnet18(weights=None) # My model is initiating to learning architecture from this ResNet18 architecture +model.fc = torch.nn.Linear(model.fc.in_features, 4) # Adjust for 4 classes +model.load_state_dict(torch.load(model_path, map_location=device, weights_only=True)) +model = model.to(device) +model.eval() + +# Define class names +class_names = ["clean_casual", "clean_formal", "messy_casual", "messy_formal"] + +# Define image transformations (same as in training) +transform = transforms.Compose([ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) +]) + +# Prediction function +def predict_image(image_bytes): + image = Image.open(io.BytesIO(image_bytes)) # Load image from bytes + image = transform(image).unsqueeze(0).to(device) # Apply transformation and add batch dimension + with torch.no_grad(): + outputs = model(image) + _, predicted = torch.max(outputs, 1) # Get the predicted class + return class_names[predicted.item()] + +# Create route for prediction +@app.route('/predict', methods=['POST']) +def predict(): + if 'file' not in request.files: + return jsonify({"error": "No file part"}), 400 + file = request.files['file'] + if file.filename == '': + return jsonify({"error": "No selected file"}), 400 + + try: + image_bytes = file.read() # Read the file content as bytes + prediction = predict_image(image_bytes) # Get prediction + + # Send the result to the Node.js backend (assuming API expects prediction and email) + user_email = request.form.get("email") + if user_email: + response = requests.post( + 'http://localhost:5000/api/savePrediction', # Adjust the URL to your Node.js server + json={'prediction': prediction, 'email': user_email} + ) + if response.status_code == 200: + return jsonify({"prediction": prediction, "status": "Prediction saved"}) + else: + return jsonify({"error": "Failed to save prediction in database"}), 500 + else: + return jsonify({"error": "Email is required"}), 400 + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# Run the app +if __name__ == '__main__': + app.run(host='0.0.0.0', port=3000) diff --git a/backend/candidate_classification_service/requirements.txt b/backend/candidate_classification_service/requirements.txt new file mode 100644 index 0000000..fc8dfa4 --- /dev/null +++ b/backend/candidate_classification_service/requirements.txt @@ -0,0 +1,4 @@ +torch +torchvision +flask +pillow \ No newline at end of file From 6740b3ed2bf7d199db10032bec6a87b9c46dacc8 Mon Sep 17 00:00:00 2001 From: Deneth Pinsara Date: Mon, 18 Nov 2024 02:47:29 +0530 Subject: [PATCH 3/5] fix(app.py): change the port of python app --- backend/candidate_classification_service/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/candidate_classification_service/app.py b/backend/candidate_classification_service/app.py index 9d838ca..39912c0 100644 --- a/backend/candidate_classification_service/app.py +++ b/backend/candidate_classification_service/app.py @@ -73,4 +73,4 @@ def predict(): # Run the app if __name__ == '__main__': - app.run(host='0.0.0.0', port=3000) + app.run(host='0.0.0.0', port=5000, debug=True) From a6f8c756e3491b5dc5eb568809d43dbe4263086d Mon Sep 17 00:00:00 2001 From: Deneth Pinsara Date: Thu, 28 Nov 2024 23:19:04 +0530 Subject: [PATCH 4/5] fix(model): change the model port to 5001 --- backend/candidate_classification_service/app.py | 4 ++-- frontend/AIPT/package-lock.json | 15 +++++++++++++-- frontend/AIPT/src/App.tsx | 11 +++++------ frontend/AIPT/src/pages/Candidate/MockupTest.tsx | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/backend/candidate_classification_service/app.py b/backend/candidate_classification_service/app.py index 39912c0..9c60e37 100644 --- a/backend/candidate_classification_service/app.py +++ b/backend/candidate_classification_service/app.py @@ -42,7 +42,7 @@ def predict_image(image_bytes): return class_names[predicted.item()] # Create route for prediction -@app.route('/predict', methods=['POST']) +@app.route('/classification-predict', methods=['POST']) def predict(): if 'file' not in request.files: return jsonify({"error": "No file part"}), 400 @@ -73,4 +73,4 @@ def predict(): # Run the app if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=True) + app.run(host='0.0.0.0', port=5001, debug=False) diff --git a/frontend/AIPT/package-lock.json b/frontend/AIPT/package-lock.json index 4eb51b1..6b035b7 100644 --- a/frontend/AIPT/package-lock.json +++ b/frontend/AIPT/package-lock.json @@ -8,12 +8,12 @@ "name": "aipt", "version": "0.0.0", "dependencies": { - "@types/axios": "^0.9.36", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@shadcn/ui": "^0.0.4", + "@types/axios": "^0.9.36", "@types/react-router-dom": "^5.3.3", "aipt": "file:", "axios": "^1.7.7", @@ -26,12 +26,13 @@ "react-media-recorder": "^1.7.1", "react-router-dom": "^6.26.2", "react-toastify": "^10.0.5", + "react-webcam": "^7.2.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@eslint/js": "^9.9.0", - "@types/node": "^22.9.3", + "@types/node": "^22.9.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-webcam": "^1.1.0", @@ -4429,6 +4430,16 @@ "react-dom": ">=18" } }, + "node_modules/react-webcam": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-webcam/-/react-webcam-7.2.0.tgz", + "integrity": "sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.2.0", + "react-dom": ">=16.2.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/frontend/AIPT/src/App.tsx b/frontend/AIPT/src/App.tsx index 64cc257..6dac155 100644 --- a/frontend/AIPT/src/App.tsx +++ b/frontend/AIPT/src/App.tsx @@ -13,8 +13,8 @@ import CandidateProfile from "./pages/Candidate/CandidateProfile"; import Settings from "./pages/Candidate/Settings"; import MockupTest from "./pages/Candidate/MockupTest"; import Results from "./pages/Candidate/Results"; -import CandidateHome from './pages/Candidate/CandidateHome'; -import JobApplicationForm from './pages/Candidate/JobDetails'; +import CandidateHome from "./pages/Candidate/CandidateHome"; +import JobApplicationForm from "./pages/Candidate/JobDetails"; const App: React.FC = () => { return ( @@ -64,13 +64,12 @@ const App: React.FC = () => { + } /> - { + } @@ -126,7 +125,7 @@ const App: React.FC = () => { path="/settings" element={ - + } /> diff --git a/frontend/AIPT/src/pages/Candidate/MockupTest.tsx b/frontend/AIPT/src/pages/Candidate/MockupTest.tsx index 2ec3b7f..932444a 100644 --- a/frontend/AIPT/src/pages/Candidate/MockupTest.tsx +++ b/frontend/AIPT/src/pages/Candidate/MockupTest.tsx @@ -43,7 +43,7 @@ const MockupTest: React.FC = () => { // Send the image to Flask for classification const flaskResponse = await axios.post( - "http://127.0.0.1:5000/predict", + "http://127.0.0.1:5001/classification-predict", formData, { headers: { "Content-Type": "multipart/form-data" }, From 732d0533a28edc53f72bbd868ad8073d82648fb0 Mon Sep 17 00:00:00 2001 From: Deneth Pinsara Date: Thu, 28 Nov 2024 23:54:32 +0530 Subject: [PATCH 5/5] chore(docker): add docker and necessary file modifications done and change the python service port to 3003 --- .../.gitignore | 21 ++++++++++++++ .../Dockerfile | 28 +++++++++++++++++++ .../candidate_classification_service/app.py | 2 +- .../base/Dockerfile | 18 ++++++++++++ backend/docker-compose.yml | 17 +++++++---- .../AIPT/src/pages/Candidate/MockupTest.tsx | 2 +- 6 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 backend/candidate_classification_service/.gitignore create mode 100644 backend/candidate_classification_service/Dockerfile create mode 100644 backend/candidate_classification_service/base/Dockerfile diff --git a/backend/candidate_classification_service/.gitignore b/backend/candidate_classification_service/.gitignore new file mode 100644 index 0000000..35a063c --- /dev/null +++ b/backend/candidate_classification_service/.gitignore @@ -0,0 +1,21 @@ +# Ignore Python virtual environment +venv/ +# Ignore Python cache files +*.pyc +*.pyo +*.pyd +*.py[cod] +__pycache__/ + +# Ignore Jupyter Notebook checkpoints +.ipynb_checkpoints/ + +# Ignore IDE specific files +.idea/ +.vscode/ + +# Ignore local configuration files (optional) +.env +pyvenv.cfg + +models \ No newline at end of file diff --git a/backend/candidate_classification_service/Dockerfile b/backend/candidate_classification_service/Dockerfile new file mode 100644 index 0000000..06b278c --- /dev/null +++ b/backend/candidate_classification_service/Dockerfile @@ -0,0 +1,28 @@ +# prebuilt base image with large dependencies +FROM python-base:latest + +# Working directory +WORKDIR /app + +# Install system dependencies for PyTorch +RUN apt-get update && apt-get install -y \ + libgl1-mesa-glx \ + && rm -rf /var/lib/apt/lists/* + +# Create the uploads directory +RUN mkdir -p /app/uploads + +# Copy only the requirements.txt to leverage Docker caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the entire application code +COPY . . + +# Expose the application port +EXPOSE 3003 + +# Command to run the Flask app +CMD ["python", "app.py"] diff --git a/backend/candidate_classification_service/app.py b/backend/candidate_classification_service/app.py index 9c60e37..67a652b 100644 --- a/backend/candidate_classification_service/app.py +++ b/backend/candidate_classification_service/app.py @@ -73,4 +73,4 @@ def predict(): # Run the app if __name__ == '__main__': - app.run(host='0.0.0.0', port=5001, debug=False) + app.run(host='0.0.0.0', port=3003, debug=False) diff --git a/backend/candidate_classification_service/base/Dockerfile b/backend/candidate_classification_service/base/Dockerfile new file mode 100644 index 0000000..cc96454 --- /dev/null +++ b/backend/candidate_classification_service/base/Dockerfile @@ -0,0 +1,18 @@ +# Use the slim Python image as the base +FROM python:3.12-slim + +# Set the working directory +WORKDIR /app + +# Install necessary system packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libjpeg-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies that are common for the service +RUN pip install --no-cache-dir \ + torch \ + torchvision \ + Pillow diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index e2c6979..617da0e 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -39,10 +39,17 @@ services: # ports: # - "3002:3002" - # service-4: - # build: - # context: ./service-4 # Path to the service-4 folder - # ports: - # - "3003:3003" + candidate-classification: + build: + context: ./candidate_classification_service # Path to the candidate_classification_service folder + ports: + - "3003:3003" + volumes: + - ./candidate_classification_service/uploads:/app/uploads # Mount uploads folder + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3003"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/frontend/AIPT/src/pages/Candidate/MockupTest.tsx b/frontend/AIPT/src/pages/Candidate/MockupTest.tsx index 932444a..e921214 100644 --- a/frontend/AIPT/src/pages/Candidate/MockupTest.tsx +++ b/frontend/AIPT/src/pages/Candidate/MockupTest.tsx @@ -43,7 +43,7 @@ const MockupTest: React.FC = () => { // Send the image to Flask for classification const flaskResponse = await axios.post( - "http://127.0.0.1:5001/classification-predict", + "http://127.0.0.1:3003/classification-predict", formData, { headers: { "Content-Type": "multipart/form-data" },