From 989d5da1deffe8991d6c6e57fe8d3808c35d2e5e Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 16:31:34 +0300 Subject: [PATCH 01/29] Add Resume Upload Controller Signed-off-by: Tal Jacob --- nextstep-backend/src/app.ts | 7 +- nextstep-backend/src/config/config.ts | 4 +- .../src/controllers/resume_controller.ts | 68 +++++++++++++++ nextstep-backend/src/routes/resume_routes.ts | 20 +++++ .../src/services/resume_service.ts | 82 +++++++++++++++++++ 5 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 nextstep-backend/src/controllers/resume_controller.ts create mode 100644 nextstep-backend/src/routes/resume_routes.ts create mode 100644 nextstep-backend/src/services/resume_service.ts diff --git a/nextstep-backend/src/app.ts b/nextstep-backend/src/app.ts index 2942509..371440b 100644 --- a/nextstep-backend/src/app.ts +++ b/nextstep-backend/src/app.ts @@ -14,7 +14,7 @@ import {config} from "./config/config"; import validateUser from "./middleware/validateUser"; import loadOpenApiFile from "./openapi/openapi_loader"; import resource_routes from './routes/resources_routes'; - +import resume_routes from './routes/resume_routes'; const specs = swaggerJsdoc(options); @@ -43,10 +43,8 @@ app.use(bodyParser.json()); app.use(removeUndefinedOrEmptyFields); app.use(bodyParser.urlencoded({ extended: true })); - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(loadOpenApiFile() as JsonObject)); - // Add Authentication for all routes except the ones listed below app.use(authenticateToken.unless({ path: [ @@ -68,8 +66,6 @@ app.use(authenticateToken.unless({ // To block queries without Authentication app.use(authenticateTokenForParams); - - app.use('/auth', authRoutes); app.use('/comment', commentsRoutes); app.use('/post', postsRoutes); @@ -77,5 +73,6 @@ app.use("/user/:id", validateUser); app.use('/user', usersRoutes); app.use('/resource', resource_routes); app.use('/room', roomsRoutes); +app.use('/resume', resume_routes); export { app, corsOptions }; diff --git a/nextstep-backend/src/config/config.ts b/nextstep-backend/src/config/config.ts index fb749a3..cda46c2 100644 --- a/nextstep-backend/src/config/config.ts +++ b/nextstep-backend/src/config/config.ts @@ -16,7 +16,9 @@ export const config = { }, resources: { imagesDirectoryPath: () => 'resources/images', - imageMaxSize: () => 10 * 1024 * 1024 // Max file size: 10MB + imageMaxSize: () => 10 * 1024 * 1024, // Max file size: 10MB + resumesDirectoryPath: () => 'resources/resumes', + resumeMaxSize: () => 5 * 1024 * 1024 // Max file size: 5MB }, chatAi: { api_url: () => process.env.CHAT_AI_API_URL || 'https://openrouter.ai/api/v1/chat/completions', diff --git a/nextstep-backend/src/controllers/resume_controller.ts b/nextstep-backend/src/controllers/resume_controller.ts new file mode 100644 index 0000000..ed363aa --- /dev/null +++ b/nextstep-backend/src/controllers/resume_controller.ts @@ -0,0 +1,68 @@ +import { Request, Response } from 'express'; +import { config } from '../config/config'; +import fs from 'fs'; +import path from 'path'; +import { uploadResume, scoreResume } from '../services/resume_service'; +import multer from 'multer'; +import { CustomRequest } from "types/customRequest"; +import { handleError } from "../utils/handle_error"; + +const uploadAndScoreResume = async (req: CustomRequest, res: Response) => { + try { + const resumeFilename = await uploadResume(req); + const resumePath = path.resolve(config.resources.resumesDirectoryPath(), resumeFilename); + const jobDescription = req.body.jobDescription; + + const score = await scoreResume(resumePath, jobDescription); + + return res.status(201).json({ + resumeFilename, + score, + message: 'Resume uploaded and scored successfully' + }); + } catch (error) { + if (error instanceof multer.MulterError || error instanceof TypeError) { + return res.status(400).send(error.message); + } else { + handleError(error, res); + } + } +}; + +const getResumeScore = async (req: Request, res: Response) => { + try { + const { filename } = req.params; + const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename); + const jobDescription = req.query.jobDescription as string; + + if (!fs.existsSync(resumePath)) { + return res.status(404).send('Resume not found'); + } + + const score = await scoreResume(resumePath, jobDescription); + return res.status(200).json({ score }); + } catch (error) { + handleError(error, res); + } +}; + +const getResumeFile = async (req: Request, res: Response) => { + try { + const { filename } = req.params; + const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename); + + if (!fs.existsSync(resumePath)) { + return res.status(404).send('Resume not found'); + } + + res.sendFile(resumePath); + } catch (error) { + handleError(error, res); + } +}; + +export default { + uploadAndScoreResume, + getResumeScore, + getResumeFile +}; \ No newline at end of file diff --git a/nextstep-backend/src/routes/resume_routes.ts b/nextstep-backend/src/routes/resume_routes.ts new file mode 100644 index 0000000..a74b98f --- /dev/null +++ b/nextstep-backend/src/routes/resume_routes.ts @@ -0,0 +1,20 @@ +import express, { Request, Response } from 'express'; +import ResumeController from '../controllers/resume_controller'; +import { CustomRequest } from "types/customRequest"; +import { authenticateToken } from "../middleware/auth"; + +const router = express.Router(); + +// Upload and score a resume +router.post('/upload', authenticateToken, (req: Request, res: Response) => + ResumeController.uploadAndScoreResume(req as CustomRequest, res)); + +// Get score for an existing resume +router.get('/score/:filename', authenticateToken, (req: Request, res: Response) => + ResumeController.getResumeScore(req, res)); + +// Get resume file +router.get('/file/:filename', authenticateToken, (req: Request, res: Response) => + ResumeController.getResumeFile(req, res)); + +export default router; \ No newline at end of file diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts new file mode 100644 index 0000000..32cb213 --- /dev/null +++ b/nextstep-backend/src/services/resume_service.ts @@ -0,0 +1,82 @@ +import { config } from '../config/config'; +import multer from 'multer'; +import path from 'path'; +import { randomUUID } from 'crypto'; +import fs from 'fs'; +import axios from 'axios'; + +const createResumesStorage = () => { + // Ensure the directory exists + const resumesDirectoryPath = config.resources.resumesDirectoryPath(); + if (!fs.existsSync(resumesDirectoryPath)) { + fs.mkdirSync(resumesDirectoryPath, { recursive: true }); + } + + const resumesStorage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, `${resumesDirectoryPath}/`); + }, + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + const id = randomUUID(); + cb(null, id + ext); + } + }); + + return multer({ + storage: resumesStorage, + limits: { + fileSize: config.resources.resumeMaxSize() + }, + fileFilter: (req, file, cb) => { + const allowedTypes = /pdf|doc|docx/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + + if (extname && mimetype) { + return cb(null, true); + } else { + return cb(new TypeError(`Invalid file type. Only PDF, DOC, and DOCX files are allowed.`)); + } + } + }); +}; + +// TODO - fix the type here +// @ts-ignore +const uploadResume = (req): Promise => { + return new Promise((resolve, reject) => { + // @ts-ignore + createResumesStorage().single('file')(req, {}, (error) => { + if (error) { + if (error instanceof multer.MulterError || error instanceof TypeError) { + return reject(error); + } else if (!req.file) { + return reject(new TypeError('No file uploaded.')); + } else { + return reject(new Error('Internal Server Error')); + } + } + resolve(req.file.filename); + }); + }); +}; + +const scoreResume = async (resumePath: string, jobDescription?: string): Promise => { + try { + // Here you would integrate with an actual ATS system + // For now, we'll return a mock score + // In a real implementation, you would: + // 1. Parse the resume text + // 2. Compare it with the job description + // 3. Calculate a score based on keywords, skills, experience, etc. + + // Mock implementation + const score = Math.floor(Math.random() * 100); + return score; + } catch (error) { + throw new Error('Failed to score resume'); + } +}; + +export { uploadResume, scoreResume }; \ No newline at end of file From 9f12d4712a599a0fd51188311fb81a5f8560cb76 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 18:45:17 +0300 Subject: [PATCH 02/29] Document ResumeController In `swagger.yaml` Signed-off-by: Tal Jacob --- nextstep-backend/src/openapi/swagger.yaml | 131 ++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index a02d165..123e5e1 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -17,6 +17,8 @@ tags: description: Operations related to uploading & downloading resources - name: Rooms description: Operations related to chat rooms + - name: Resume + description: Operations related to resume upload and ATS scoring paths: /post: @@ -841,6 +843,135 @@ paths: '400': description: Bad Request + /resume/upload: + post: + tags: + - Resume + summary: Upload and score a resume + security: + - BearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: The resume file to upload (PDF, DOC, DOCX) + jobDescription: + type: string + description: Optional job description for scoring + responses: + '201': + description: Resume uploaded and scored successfully + content: + application/json: + schema: + type: object + properties: + resumeFilename: + type: string + description: The filename of the uploaded resume + score: + type: number + description: The ATS score of the resume + message: + type: string + description: Status message + '400': + description: Bad request + content: + text/plain: + examples: + fileTooLarge: + value: "File too large" + invalidFileType: + value: "Invalid file type. Only PDF, DOC, and DOCX files are allowed" + noFileUploaded: + value: "No file uploaded" + '401': + description: Unauthorized + '500': + description: Internal server error + + /resume/score/{filename}: + get: + tags: + - Resume + summary: Get score for an existing resume + security: + - BearerAuth: [] + parameters: + - name: filename + in: path + required: true + schema: + type: string + description: The filename of the resume + - name: jobDescription + in: query + required: false + schema: + type: string + description: Optional job description for scoring + responses: + '200': + description: Resume score retrieved successfully + content: + application/json: + schema: + type: object + properties: + score: + type: number + description: The ATS score of the resume + '401': + description: Unauthorized + '404': + description: Resume not found + '500': + description: Internal server error + + /resume/file/{filename}: + get: + tags: + - Resume + summary: Get resume file + security: + - BearerAuth: [] + parameters: + - name: filename + in: path + required: true + schema: + type: string + description: The filename of the resume + responses: + '200': + description: Resume file retrieved successfully + content: + application/pdf: + schema: + type: string + format: binary + application/msword: + schema: + type: string + format: binary + application/vnd.openxmlformats-officedocument.wordprocessingml.document: + schema: + type: string + format: binary + '401': + description: Unauthorized + '404': + description: Resume not found + '500': + description: Internal server error + components: schemas: Post: From 357391b6fc482a29b5c8ac58d29d08a1a0ecfc68 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 18:53:57 +0300 Subject: [PATCH 03/29] Moved Resume Uploading Logic To `resources_service.ts` Signed-off-by: Tal Jacob --- .../src/controllers/resume_controller.ts | 3 +- .../src/services/resources_service.ts | 78 ++++++++++++++++--- .../src/services/resume_service.ts | 61 +-------------- 3 files changed, 71 insertions(+), 71 deletions(-) diff --git a/nextstep-backend/src/controllers/resume_controller.ts b/nextstep-backend/src/controllers/resume_controller.ts index ed363aa..2e5e4a3 100644 --- a/nextstep-backend/src/controllers/resume_controller.ts +++ b/nextstep-backend/src/controllers/resume_controller.ts @@ -2,7 +2,8 @@ import { Request, Response } from 'express'; import { config } from '../config/config'; import fs from 'fs'; import path from 'path'; -import { uploadResume, scoreResume } from '../services/resume_service'; +import { uploadResume } from '../services/resources_service'; +import { scoreResume } from '../services/resume_service'; import multer from 'multer'; import { CustomRequest } from "types/customRequest"; import { handleError } from "../utils/handle_error"; diff --git a/nextstep-backend/src/services/resources_service.ts b/nextstep-backend/src/services/resources_service.ts index 925bd61..26fb108 100644 --- a/nextstep-backend/src/services/resources_service.ts +++ b/nextstep-backend/src/services/resources_service.ts @@ -3,9 +3,13 @@ import multer from 'multer'; import path from 'path'; import { randomUUID } from 'crypto'; import fs from 'fs'; +import { Request } from 'express'; -const createImagesStorage = () => { +interface MulterRequest extends Request { + file?: Express.Multer.File; +} +const createImagesStorage = () => { // Ensure the directory exists const imagesResourcesDir = config.resources.imagesDirectoryPath(); if (!fs.existsSync(imagesResourcesDir)) { @@ -23,8 +27,7 @@ const createImagesStorage = () => { } }); - // TODO - consider to make a Promise - const uploadImage = multer({ + return multer({ storage: imagesStorage, limits: { fileSize: config.resources.imageMaxSize() @@ -41,16 +44,68 @@ const createImagesStorage = () => { } } }); +}; + +const createResumesStorage = () => { + // Ensure the directory exists + const resumesDirectoryPath = config.resources.resumesDirectoryPath(); + if (!fs.existsSync(resumesDirectoryPath)) { + fs.mkdirSync(resumesDirectoryPath, { recursive: true }); + } + + const resumesStorage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, `${resumesDirectoryPath}/`); + }, + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + const id = randomUUID(); + cb(null, id + ext); + } + }); + + return multer({ + storage: resumesStorage, + limits: { + fileSize: config.resources.resumeMaxSize() + }, + fileFilter: (req, file, cb) => { + const allowedTypes = /pdf|doc|docx/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + + if (extname && mimetype) { + return cb(null, true); + } else { + return cb(new TypeError(`Invalid file type. Only PDF, DOC, and DOCX files are allowed.`)); + } + } + }); +}; - return uploadImage; +const uploadImage = (req: MulterRequest): Promise => { + return new Promise((resolve, reject) => { + createImagesStorage().single('file')(req, {} as any, (error) => { + if (error) { + if (error instanceof multer.MulterError || error instanceof TypeError) { + return reject(error); + } else if (!req.file) { + return reject(new TypeError('No file uploaded.')); + } else { + return reject(new Error('Internal Server Error')); + } + } + if (!req.file) { + return reject(new TypeError('No file uploaded.')); + } + resolve(req.file.filename); + }); + }); }; -// TODO - fix the type here -// @ts-ignore -const uploadImage = (req) : Promise => { +const uploadResume = (req: MulterRequest): Promise => { return new Promise((resolve, reject) => { - // @ts-ignore - createImagesStorage().single('file')(req, {}, (error) => { + createResumesStorage().single('file')(req, {} as any, (error) => { if (error) { if (error instanceof multer.MulterError || error instanceof TypeError) { return reject(error); @@ -60,9 +115,12 @@ const uploadImage = (req) : Promise => { return reject(new Error('Internal Server Error')); } } + if (!req.file) { + return reject(new TypeError('No file uploaded.')); + } resolve(req.file.filename); }); }); }; -export { uploadImage }; \ No newline at end of file +export { uploadImage, uploadResume }; \ No newline at end of file diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts index 32cb213..03efde2 100644 --- a/nextstep-backend/src/services/resume_service.ts +++ b/nextstep-backend/src/services/resume_service.ts @@ -1,67 +1,8 @@ import { config } from '../config/config'; -import multer from 'multer'; import path from 'path'; -import { randomUUID } from 'crypto'; import fs from 'fs'; import axios from 'axios'; -const createResumesStorage = () => { - // Ensure the directory exists - const resumesDirectoryPath = config.resources.resumesDirectoryPath(); - if (!fs.existsSync(resumesDirectoryPath)) { - fs.mkdirSync(resumesDirectoryPath, { recursive: true }); - } - - const resumesStorage = multer.diskStorage({ - destination: (req, file, cb) => { - cb(null, `${resumesDirectoryPath}/`); - }, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname); - const id = randomUUID(); - cb(null, id + ext); - } - }); - - return multer({ - storage: resumesStorage, - limits: { - fileSize: config.resources.resumeMaxSize() - }, - fileFilter: (req, file, cb) => { - const allowedTypes = /pdf|doc|docx/; - const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); - const mimetype = allowedTypes.test(file.mimetype); - - if (extname && mimetype) { - return cb(null, true); - } else { - return cb(new TypeError(`Invalid file type. Only PDF, DOC, and DOCX files are allowed.`)); - } - } - }); -}; - -// TODO - fix the type here -// @ts-ignore -const uploadResume = (req): Promise => { - return new Promise((resolve, reject) => { - // @ts-ignore - createResumesStorage().single('file')(req, {}, (error) => { - if (error) { - if (error instanceof multer.MulterError || error instanceof TypeError) { - return reject(error); - } else if (!req.file) { - return reject(new TypeError('No file uploaded.')); - } else { - return reject(new Error('Internal Server Error')); - } - } - resolve(req.file.filename); - }); - }); -}; - const scoreResume = async (resumePath: string, jobDescription?: string): Promise => { try { // Here you would integrate with an actual ATS system @@ -79,4 +20,4 @@ const scoreResume = async (resumePath: string, jobDescription?: string): Promise } }; -export { uploadResume, scoreResume }; \ No newline at end of file +export { scoreResume }; \ No newline at end of file From d65c385de4b7a319aceeb32f03fcde9358fcb365 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 19:28:43 +0300 Subject: [PATCH 04/29] Remove Redundant Comments From `swagger.yaml` Signed-off-by: Tal Jacob --- nextstep-backend/src/openapi/swagger.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index 123e5e1..b70e6ac 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -249,7 +249,6 @@ paths: '401': description: Unauthorized - Missing token - # Comment Routes /comment: get: tags: @@ -411,7 +410,6 @@ paths: '404': $ref: '#/components/responses/PostNotFound' - # Authentication Routes /auth/register: post: tags: @@ -579,6 +577,7 @@ paths: $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + /user/{userId}: get: tags: @@ -666,7 +665,6 @@ paths: '404': $ref: '#/components/responses/UserNotFound' - /resource/image/user: post: tags: From 1832700534c37ff02c488344c60f806b8cc0347f Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 19:46:36 +0300 Subject: [PATCH 05/29] Reorder Resume Uploading And Downloading To Be In Resources Controller Signed-off-by: Tal Jacob --- .../src/controllers/resources_controller.ts | 49 +++++- .../src/controllers/resume_controller.ts | 38 ---- nextstep-backend/src/openapi/swagger.yaml | 166 +++++++++--------- .../src/routes/resources_routes.ts | 4 + nextstep-backend/src/routes/resume_routes.ts | 8 - 5 files changed, 133 insertions(+), 132 deletions(-) diff --git a/nextstep-backend/src/controllers/resources_controller.ts b/nextstep-backend/src/controllers/resources_controller.ts index a178a98..6d0bab4 100644 --- a/nextstep-backend/src/controllers/resources_controller.ts +++ b/nextstep-backend/src/controllers/resources_controller.ts @@ -2,12 +2,12 @@ import { Request, Response } from 'express'; import { config } from '../config/config'; import fs from 'fs'; import path from 'path'; -import { uploadImage } from '../services/resources_service'; +import { uploadResume, uploadImage } from '../services/resources_service'; import multer from 'multer'; import {CustomRequest} from "types/customRequest"; import {updateUserById} from "../services/users_service"; import {handleError} from "../utils/handle_error"; - +import { scoreResume } from '../services/resume_service'; const createUserImageResource = async (req: CustomRequest, res: Response) => { try { @@ -49,4 +49,47 @@ const getImageResource = async (req: Request, res: Response) => { } }; -export default { createUserImageResource, createImageResource, getImageResource }; \ No newline at end of file +const createAndScoreResumeResource = async (req: Request, res: Response) => { + try { + const resumeFilename = await uploadResume(req); + const resumePath = path.resolve(config.resources.resumesDirectoryPath(), resumeFilename); + const jobDescription = req.body.jobDescription; + + const score = await scoreResume(resumePath, jobDescription); + + return res.status(201).json({ + resumeFilename, + score, + message: 'Resume uploaded and scored successfully' + }); + } catch (error) { + if (error instanceof multer.MulterError || error instanceof TypeError) { + return res.status(400).send(error.message); + } else { + handleError(error, res); + } + } +}; + +const getResumeResource = async (req: Request, res: Response) => { + try { + const { filename } = req.params; + const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename); + + if (!fs.existsSync(resumePath)) { + return res.status(404).send('Resume not found'); + } + + res.sendFile(resumePath); + } catch (error) { + handleError(error, res); + } +}; + +export default { + createUserImageResource, + createImageResource, + getImageResource, + getResumeResource, + createAndScoreResumeResource +}; \ No newline at end of file diff --git a/nextstep-backend/src/controllers/resume_controller.ts b/nextstep-backend/src/controllers/resume_controller.ts index 2e5e4a3..23feaea 100644 --- a/nextstep-backend/src/controllers/resume_controller.ts +++ b/nextstep-backend/src/controllers/resume_controller.ts @@ -2,34 +2,11 @@ import { Request, Response } from 'express'; import { config } from '../config/config'; import fs from 'fs'; import path from 'path'; -import { uploadResume } from '../services/resources_service'; import { scoreResume } from '../services/resume_service'; import multer from 'multer'; import { CustomRequest } from "types/customRequest"; import { handleError } from "../utils/handle_error"; -const uploadAndScoreResume = async (req: CustomRequest, res: Response) => { - try { - const resumeFilename = await uploadResume(req); - const resumePath = path.resolve(config.resources.resumesDirectoryPath(), resumeFilename); - const jobDescription = req.body.jobDescription; - - const score = await scoreResume(resumePath, jobDescription); - - return res.status(201).json({ - resumeFilename, - score, - message: 'Resume uploaded and scored successfully' - }); - } catch (error) { - if (error instanceof multer.MulterError || error instanceof TypeError) { - return res.status(400).send(error.message); - } else { - handleError(error, res); - } - } -}; - const getResumeScore = async (req: Request, res: Response) => { try { const { filename } = req.params; @@ -47,23 +24,8 @@ const getResumeScore = async (req: Request, res: Response) => { } }; -const getResumeFile = async (req: Request, res: Response) => { - try { - const { filename } = req.params; - const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename); - - if (!fs.existsSync(resumePath)) { - return res.status(404).send('Resume not found'); - } - res.sendFile(resumePath); - } catch (error) { - handleError(error, res); - } -}; export default { - uploadAndScoreResume, getResumeScore, - getResumeFile }; \ No newline at end of file diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index b70e6ac..0bd2852 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -18,7 +18,7 @@ tags: - name: Rooms description: Operations related to chat rooms - name: Resume - description: Operations related to resume upload and ATS scoring + description: Operations related to resume ATS scoring paths: /post: @@ -783,68 +783,10 @@ paths: '500': description: Internal server error - /room/user/{receiverUserId}: - get: - tags: - - Rooms - security: - - BearerAuth: [] - summary: Get or create a room by user IDs - parameters: - - name: receiverUserId - in: path - required: true - description: The ID of the receiver user - schema: - type: string - responses: - '200': - description: Room found and returned successfully - content: - application/json: - schema: - type: object - properties: - _id: - type: string - example: "67d5d49a9757556bd7e30939" - userIds: - type: array - items: - type: string - example: ["67afa72968f736f112ae1d4f", "67afa589118b00ef7c04bbee"] - messages: - type: array - items: - type: string - example: [] - '201': - description: Room created successfully - content: - application/json: - schema: - type: object - properties: - _id: - type: string - example: "67d5d49a9757556bd7e30939" - userIds: - type: array - items: - type: string - example: ["67afa72968f736f112ae1d4f", "67afa589118b00ef7c04bbee"] - messages: - type: array - items: - type: string - example: [] - '400': - description: Bad Request - - /resume/upload: + /resource/resume: post: tags: - - Resume + - Resources summary: Upload and score a resume security: - BearerAuth: [] @@ -895,6 +837,43 @@ paths: '500': description: Internal server error + /resource/resume/{filename}: + get: + tags: + - Resources + summary: Get resume file + security: + - BearerAuth: [] + parameters: + - name: filename + in: path + required: true + schema: + type: string + description: The filename of the resume + responses: + '200': + description: Resume file retrieved successfully + content: + application/pdf: + schema: + type: string + format: binary + application/msword: + schema: + type: string + format: binary + application/vnd.openxmlformats-officedocument.wordprocessingml.document: + schema: + type: string + format: binary + '401': + description: Unauthorized + '404': + description: Resume not found + '500': + description: Internal server error + /resume/score/{filename}: get: tags: @@ -933,42 +912,63 @@ paths: '500': description: Internal server error - /resume/file/{filename}: + /room/user/{receiverUserId}: get: tags: - - Resume - summary: Get resume file + - Rooms security: - BearerAuth: [] + summary: Get or create a room by user IDs parameters: - - name: filename + - name: receiverUserId in: path required: true + description: The ID of the receiver user schema: type: string - description: The filename of the resume responses: '200': - description: Resume file retrieved successfully + description: Room found and returned successfully content: - application/pdf: - schema: - type: string - format: binary - application/msword: + application/json: schema: - type: string - format: binary - application/vnd.openxmlformats-officedocument.wordprocessingml.document: + type: object + properties: + _id: + type: string + example: "67d5d49a9757556bd7e30939" + userIds: + type: array + items: + type: string + example: ["67afa72968f736f112ae1d4f", "67afa589118b00ef7c04bbee"] + messages: + type: array + items: + type: string + example: [] + '201': + description: Room created successfully + content: + application/json: schema: - type: string - format: binary - '401': - description: Unauthorized - '404': - description: Resume not found - '500': - description: Internal server error + type: object + properties: + _id: + type: string + example: "67d5d49a9757556bd7e30939" + userIds: + type: array + items: + type: string + example: ["67afa72968f736f112ae1d4f", "67afa589118b00ef7c04bbee"] + messages: + type: array + items: + type: string + example: [] + '400': + description: Bad Request components: schemas: diff --git a/nextstep-backend/src/routes/resources_routes.ts b/nextstep-backend/src/routes/resources_routes.ts index 4a24532..885f9fb 100644 --- a/nextstep-backend/src/routes/resources_routes.ts +++ b/nextstep-backend/src/routes/resources_routes.ts @@ -11,4 +11,8 @@ router.post('/image', Resource.createImageResource); router.get('/image/:filename', Resource.getImageResource); +router.post('/resume', Resource.createAndScoreResumeResource); + +router.get('/resume/:filename', Resource.getResumeResource); + export default router; \ No newline at end of file diff --git a/nextstep-backend/src/routes/resume_routes.ts b/nextstep-backend/src/routes/resume_routes.ts index a74b98f..b04d84b 100644 --- a/nextstep-backend/src/routes/resume_routes.ts +++ b/nextstep-backend/src/routes/resume_routes.ts @@ -5,16 +5,8 @@ import { authenticateToken } from "../middleware/auth"; const router = express.Router(); -// Upload and score a resume -router.post('/upload', authenticateToken, (req: Request, res: Response) => - ResumeController.uploadAndScoreResume(req as CustomRequest, res)); - // Get score for an existing resume router.get('/score/:filename', authenticateToken, (req: Request, res: Response) => ResumeController.getResumeScore(req, res)); -// Get resume file -router.get('/file/:filename', authenticateToken, (req: Request, res: Response) => - ResumeController.getResumeFile(req, res)); - export default router; \ No newline at end of file From c07223ce1d438d4dd82a27d7169d39f6fd096a21 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 19:58:07 +0300 Subject: [PATCH 06/29] Reformat Resume Upload Response To Text Only Signed-off-by: Tal Jacob --- .../src/controllers/resources_controller.ts | 17 ++++------------- nextstep-backend/src/openapi/swagger.yaml | 19 +++++-------------- .../src/routes/resources_routes.ts | 2 +- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/nextstep-backend/src/controllers/resources_controller.ts b/nextstep-backend/src/controllers/resources_controller.ts index 6d0bab4..f17cff3 100644 --- a/nextstep-backend/src/controllers/resources_controller.ts +++ b/nextstep-backend/src/controllers/resources_controller.ts @@ -49,19 +49,10 @@ const getImageResource = async (req: Request, res: Response) => { } }; -const createAndScoreResumeResource = async (req: Request, res: Response) => { +const createResumeResource = async (req: Request, res: Response) => { try { - const resumeFilename = await uploadResume(req); - const resumePath = path.resolve(config.resources.resumesDirectoryPath(), resumeFilename); - const jobDescription = req.body.jobDescription; - - const score = await scoreResume(resumePath, jobDescription); - - return res.status(201).json({ - resumeFilename, - score, - message: 'Resume uploaded and scored successfully' - }); + const resumeFilename = await uploadResume(req); + return res.status(201).send(resumeFilename); } catch (error) { if (error instanceof multer.MulterError || error instanceof TypeError) { return res.status(400).send(error.message); @@ -91,5 +82,5 @@ export default { createImageResource, getImageResource, getResumeResource, - createAndScoreResumeResource + createResumeResource }; \ No newline at end of file diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index 0bd2852..00a6d20 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -787,7 +787,7 @@ paths: post: tags: - Resources - summary: Upload and score a resume + summary: Upload a resume security: - BearerAuth: [] requestBody: @@ -806,21 +806,12 @@ paths: description: Optional job description for scoring responses: '201': - description: Resume uploaded and scored successfully + description: Resume uploaded successfully content: - application/json: + text/plain: schema: - type: object - properties: - resumeFilename: - type: string - description: The filename of the uploaded resume - score: - type: number - description: The ATS score of the resume - message: - type: string - description: Status message + description: The name of the uploaded file + type: string '400': description: Bad request content: diff --git a/nextstep-backend/src/routes/resources_routes.ts b/nextstep-backend/src/routes/resources_routes.ts index 885f9fb..4f8e5fe 100644 --- a/nextstep-backend/src/routes/resources_routes.ts +++ b/nextstep-backend/src/routes/resources_routes.ts @@ -11,7 +11,7 @@ router.post('/image', Resource.createImageResource); router.get('/image/:filename', Resource.getImageResource); -router.post('/resume', Resource.createAndScoreResumeResource); +router.post('/resume', Resource.createResumeResource); router.get('/resume/:filename', Resource.getResumeResource); From 4189a6e7ac852a4a868727792ffd9c88dd2fa65d Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 20:03:15 +0300 Subject: [PATCH 07/29] Reorder Route Imports Signed-off-by: Tal Jacob --- nextstep-backend/src/routes/resources_routes.ts | 1 - nextstep-backend/src/routes/resume_routes.ts | 7 ++----- nextstep-backend/src/routes/rooms_routes.ts | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/nextstep-backend/src/routes/resources_routes.ts b/nextstep-backend/src/routes/resources_routes.ts index 4f8e5fe..016d79b 100644 --- a/nextstep-backend/src/routes/resources_routes.ts +++ b/nextstep-backend/src/routes/resources_routes.ts @@ -1,4 +1,3 @@ - import express, {Request, Response} from 'express'; import Resource from '../controllers/resources_controller'; import {CustomRequest} from "types/customRequest"; diff --git a/nextstep-backend/src/routes/resume_routes.ts b/nextstep-backend/src/routes/resume_routes.ts index b04d84b..eb1f8ba 100644 --- a/nextstep-backend/src/routes/resume_routes.ts +++ b/nextstep-backend/src/routes/resume_routes.ts @@ -1,12 +1,9 @@ import express, { Request, Response } from 'express'; -import ResumeController from '../controllers/resume_controller'; +import Resume from '../controllers/resume_controller'; import { CustomRequest } from "types/customRequest"; -import { authenticateToken } from "../middleware/auth"; const router = express.Router(); -// Get score for an existing resume -router.get('/score/:filename', authenticateToken, (req: Request, res: Response) => - ResumeController.getResumeScore(req, res)); +router.get('/score/:filename', Resume.getResumeScore); export default router; \ No newline at end of file diff --git a/nextstep-backend/src/routes/rooms_routes.ts b/nextstep-backend/src/routes/rooms_routes.ts index 7805a99..61818d4 100644 --- a/nextstep-backend/src/routes/rooms_routes.ts +++ b/nextstep-backend/src/routes/rooms_routes.ts @@ -1,4 +1,3 @@ - import express, {Request, Response} from 'express'; import * as roomsController from '../controllers/rooms_controller'; import { CustomRequest } from 'types/customRequest'; From 4ef80799921d7dd64676cc0af6a9e5b0546afc31 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 20:09:32 +0300 Subject: [PATCH 08/29] Add Resume Tests Signed-off-by: Tal Jacob --- nextstep-backend/src/tests/resumes.test.ts | 105 +++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 nextstep-backend/src/tests/resumes.test.ts diff --git a/nextstep-backend/src/tests/resumes.test.ts b/nextstep-backend/src/tests/resumes.test.ts new file mode 100644 index 0000000..2b3108b --- /dev/null +++ b/nextstep-backend/src/tests/resumes.test.ts @@ -0,0 +1,105 @@ +import request from 'supertest'; +import express from 'express'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import { config } from '../config/config'; +import resumeRoutes from '../routes/resume_routes'; +import { scoreResume } from '../services/resume_service'; + +// Mock the resume service +jest.mock('../services/resume_service'); + +describe('Resume API Tests', () => { + let app: express.Application; + const testResumePath = path.join(process.cwd(), config.resources.resumesDirectoryPath(), 'test-resume.pdf'); + const testJobDescription = 'Software Engineer with 5 years of experience'; + + beforeAll(() => { + // Create the resumes directory if it doesn't exist + const resumesDir = path.join(process.cwd(), config.resources.resumesDirectoryPath()); + if (!fs.existsSync(resumesDir)) { + fs.mkdirSync(resumesDir, { recursive: true }); + } + + // Create a test resume file + fs.writeFileSync(testResumePath, 'Test resume content'); + }); + + afterAll(() => { + // Clean up test files + if (fs.existsSync(testResumePath)) { + fs.unlinkSync(testResumePath); + } + }); + + beforeEach(() => { + app = express(); + app.use(express.json()); + app.use('/resume', resumeRoutes); + }); + + describe('GET /resume/score/:filename', () => { + it('should return a score for a valid resume', async () => { + // Mock the scoreResume function + (scoreResume as jest.Mock).mockResolvedValue(85); + + const response = await request(app) + .get(`/resume/score/test-resume.pdf?jobDescription=${encodeURIComponent(testJobDescription)}`) + .expect(200); + + expect(response.body).toHaveProperty('score'); + expect(typeof response.body.score).toBe('number'); + expect(scoreResume).toHaveBeenCalledWith( + expect.stringContaining('test-resume.pdf'), + testJobDescription + ); + }); + + it('should return 404 for non-existent resume', async () => { + const response = await request(app) + .get('/resume/score/nonexistent.pdf') + .expect(404); + + expect(response.text).toBe('Resume not found'); + }); + + it('should handle errors gracefully', async () => { + // Mock the scoreResume function to throw an error + (scoreResume as jest.Mock).mockRejectedValue(new Error('Scoring failed')); + + const response = await request(app) + .get(`/resume/score/test-resume.pdf?jobDescription=${encodeURIComponent(testJobDescription)}`) + .expect(500); + + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toBe('Scoring failed'); + }); + }); + + // Note: Since the actual upload/download endpoints are not implemented yet, + // these tests are placeholders for when those features are added + describe('POST /resume/upload', () => { + it('should upload a resume successfully', async () => { + // This test will be implemented when the upload endpoint is added + expect(true).toBe(true); + }); + + it('should validate file type', async () => { + // This test will be implemented when the upload endpoint is added + expect(true).toBe(true); + }); + }); + + describe('GET /resume/download/:filename', () => { + it('should download a resume successfully', async () => { + // This test will be implemented when the download endpoint is added + expect(true).toBe(true); + }); + + it('should return 404 for non-existent resume', async () => { + // This test will be implemented when the download endpoint is added + expect(true).toBe(true); + }); + }); +}); \ No newline at end of file From 8abf5a636d6c0ce1a8912b251244924360350767 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 20:17:38 +0300 Subject: [PATCH 09/29] Rename Resources Tests Signed-off-by: Tal Jacob --- .../tests/{resources_service.test.ts => resources_images.test.ts} | 0 .../src/tests/{resumes.test.ts => resources_resumes.test.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename nextstep-backend/src/tests/{resources_service.test.ts => resources_images.test.ts} (100%) rename nextstep-backend/src/tests/{resumes.test.ts => resources_resumes.test.ts} (100%) diff --git a/nextstep-backend/src/tests/resources_service.test.ts b/nextstep-backend/src/tests/resources_images.test.ts similarity index 100% rename from nextstep-backend/src/tests/resources_service.test.ts rename to nextstep-backend/src/tests/resources_images.test.ts diff --git a/nextstep-backend/src/tests/resumes.test.ts b/nextstep-backend/src/tests/resources_resumes.test.ts similarity index 100% rename from nextstep-backend/src/tests/resumes.test.ts rename to nextstep-backend/src/tests/resources_resumes.test.ts From df6d457ca4bce42a4a3211f26406aeb0172ab421 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 12 Apr 2025 20:22:08 +0300 Subject: [PATCH 10/29] Add Upload & Download Resume Tests Signed-off-by: Tal Jacob --- .../src/tests/resources_resumes.test.ts | 104 +++++++++++++++--- 1 file changed, 87 insertions(+), 17 deletions(-) diff --git a/nextstep-backend/src/tests/resources_resumes.test.ts b/nextstep-backend/src/tests/resources_resumes.test.ts index 2b3108b..7703517 100644 --- a/nextstep-backend/src/tests/resources_resumes.test.ts +++ b/nextstep-backend/src/tests/resources_resumes.test.ts @@ -6,13 +6,15 @@ import fs from 'fs'; import { config } from '../config/config'; import resumeRoutes from '../routes/resume_routes'; import { scoreResume } from '../services/resume_service'; +import resourcesRoutes from '../routes/resources_routes'; // Mock the resume service jest.mock('../services/resume_service'); describe('Resume API Tests', () => { let app: express.Application; - const testResumePath = path.join(process.cwd(), config.resources.resumesDirectoryPath(), 'test-resume.pdf'); + const testResumePath = path.join(process.cwd(), 'test-resume.pdf'); + const testResumeContent = 'Test resume content'; const testJobDescription = 'Software Engineer with 5 years of experience'; beforeAll(() => { @@ -23,7 +25,7 @@ describe('Resume API Tests', () => { } // Create a test resume file - fs.writeFileSync(testResumePath, 'Test resume content'); + fs.writeFileSync(testResumePath, testResumeContent); }); afterAll(() => { @@ -36,24 +38,38 @@ describe('Resume API Tests', () => { beforeEach(() => { app = express(); app.use(express.json()); + app.use('/resource', resourcesRoutes); app.use('/resume', resumeRoutes); }); describe('GET /resume/score/:filename', () => { it('should return a score for a valid resume', async () => { + // First upload a resume + const uploadResponse = await request(app) + .post('/resource/resume') + .attach('file', testResumePath); + + const filename = uploadResponse.text; + // Mock the scoreResume function (scoreResume as jest.Mock).mockResolvedValue(85); const response = await request(app) - .get(`/resume/score/test-resume.pdf?jobDescription=${encodeURIComponent(testJobDescription)}`) + .get(`/resume/score/${filename}?jobDescription=${encodeURIComponent(testJobDescription)}`) .expect(200); expect(response.body).toHaveProperty('score'); expect(typeof response.body.score).toBe('number'); expect(scoreResume).toHaveBeenCalledWith( - expect.stringContaining('test-resume.pdf'), + expect.stringContaining(filename), testJobDescription ); + + // Clean up the uploaded file + const uploadedFilePath = path.join(config.resources.resumesDirectoryPath(), filename); + if (fs.existsSync(uploadedFilePath)) { + fs.unlinkSync(uploadedFilePath); + } }); it('should return 404 for non-existent resume', async () => { @@ -65,41 +81,95 @@ describe('Resume API Tests', () => { }); it('should handle errors gracefully', async () => { + // First upload a resume + const uploadResponse = await request(app) + .post('/resource/resume') + .attach('file', testResumePath); + + const filename = uploadResponse.text; + // Mock the scoreResume function to throw an error (scoreResume as jest.Mock).mockRejectedValue(new Error('Scoring failed')); const response = await request(app) - .get(`/resume/score/test-resume.pdf?jobDescription=${encodeURIComponent(testJobDescription)}`) + .get(`/resume/score/${filename}?jobDescription=${encodeURIComponent(testJobDescription)}`) .expect(500); expect(response.body).toHaveProperty('message'); expect(response.body.message).toBe('Scoring failed'); + + // Clean up the uploaded file + const uploadedFilePath = path.join(config.resources.resumesDirectoryPath(), filename); + if (fs.existsSync(uploadedFilePath)) { + fs.unlinkSync(uploadedFilePath); + } }); }); - // Note: Since the actual upload/download endpoints are not implemented yet, - // these tests are placeholders for when those features are added - describe('POST /resume/upload', () => { + describe('POST /resource/resume', () => { it('should upload a resume successfully', async () => { - // This test will be implemented when the upload endpoint is added - expect(true).toBe(true); + const response = await request(app) + .post('/resource/resume') + .attach('file', testResumePath) + .expect(201); + + expect(response.text).toBeDefined(); + expect(response.text).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.pdf$/); + + // Clean up the uploaded file + const uploadedFilePath = path.join(config.resources.resumesDirectoryPath(), response.text); + if (fs.existsSync(uploadedFilePath)) { + fs.unlinkSync(uploadedFilePath); + } }); it('should validate file type', async () => { - // This test will be implemented when the upload endpoint is added - expect(true).toBe(true); + const response = await request(app) + .post('/resource/resume') + .attach('file', Buffer.from('invalid content'), { filename: 'test.txt' }) + .expect(400); + + expect(response.text).toBe('Invalid file type. Only PDF, DOC, and DOCX files are allowed.'); + }); + + it('should handle missing file', async () => { + const response = await request(app) + .post('/resource/resume') + .expect(400); + + expect(response.text).toBe('No file uploaded.'); }); }); - describe('GET /resume/download/:filename', () => { + describe('GET /resource/resume/:filename', () => { it('should download a resume successfully', async () => { - // This test will be implemented when the download endpoint is added - expect(true).toBe(true); + // First upload a resume + const uploadResponse = await request(app) + .post('/resource/resume') + .attach('file', testResumePath); + + const filename = uploadResponse.text; + + // Then try to download it + const response = await request(app) + .get(`/resource/resume/${filename}`) + .expect(200); + + expect(response.body).toBeDefined(); + + // Clean up the uploaded file + const uploadedFilePath = path.join(config.resources.resumesDirectoryPath(), filename); + if (fs.existsSync(uploadedFilePath)) { + fs.unlinkSync(uploadedFilePath); + } }); it('should return 404 for non-existent resume', async () => { - // This test will be implemented when the download endpoint is added - expect(true).toBe(true); + const response = await request(app) + .get('/resource/resume/nonexistent.pdf') + .expect(404); + + expect(response.text).toBe('Resume not found'); }); }); }); \ No newline at end of file From fad6780cd6258a951b3550f4be6d950ab082d051 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 10:21:17 +0300 Subject: [PATCH 11/29] Add Prompt To AI For Resume Scoring Signed-off-by: Tal Jacob --- .../src/controllers/resources_controller.ts | 1 - .../src/controllers/resume_controller.ts | 5 +- .../src/services/chat_api_service.ts | 6 +- .../src/services/posts_service.ts | 2 +- .../src/services/resume_service.ts | 73 ++++++++++++++++--- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/nextstep-backend/src/controllers/resources_controller.ts b/nextstep-backend/src/controllers/resources_controller.ts index f17cff3..c59a52b 100644 --- a/nextstep-backend/src/controllers/resources_controller.ts +++ b/nextstep-backend/src/controllers/resources_controller.ts @@ -7,7 +7,6 @@ import multer from 'multer'; import {CustomRequest} from "types/customRequest"; import {updateUserById} from "../services/users_service"; import {handleError} from "../utils/handle_error"; -import { scoreResume } from '../services/resume_service'; const createUserImageResource = async (req: CustomRequest, res: Response) => { try { diff --git a/nextstep-backend/src/controllers/resume_controller.ts b/nextstep-backend/src/controllers/resume_controller.ts index 23feaea..e272e8e 100644 --- a/nextstep-backend/src/controllers/resume_controller.ts +++ b/nextstep-backend/src/controllers/resume_controller.ts @@ -17,15 +17,14 @@ const getResumeScore = async (req: Request, res: Response) => { return res.status(404).send('Resume not found'); } - const score = await scoreResume(resumePath, jobDescription); - return res.status(200).json({ score }); + const scoreAndFeedback = await scoreResume(resumePath, jobDescription); + return res.status(200).send(scoreAndFeedback); } catch (error) { handleError(error, res); } }; - export default { getResumeScore, }; \ No newline at end of file diff --git a/nextstep-backend/src/services/chat_api_service.ts b/nextstep-backend/src/services/chat_api_service.ts index fc5edb3..8284d0d 100644 --- a/nextstep-backend/src/services/chat_api_service.ts +++ b/nextstep-backend/src/services/chat_api_service.ts @@ -7,7 +7,7 @@ const cleanResponse = (response: string): string => { -export const chatWithAI = async (inputUserMessage: string)=> { +export const chatWithAI = async (userMessageContent: string, systemMessageContent: string)=> { try { const API_URL = config.chatAi.api_url(); const API_KEY = config.chatAi.api_key(); @@ -15,12 +15,12 @@ export const chatWithAI = async (inputUserMessage: string)=> { const systemMessage = { role: 'system', - content: 'You are an AI assistant tasked with providing the first comment on forum posts. Your responses should be relevant, engaging, and encourage further discussion, also must be short, and you must answer if you know the answer. Ensure your comments are appropriate for the content and tone of the post. Also must answer in the language of the user post. answer short answers. dont ask questions to follow up' + content: systemMessageContent }; const userMessage = { role: 'user', - content: inputUserMessage + content: userMessageContent }; diff --git a/nextstep-backend/src/services/posts_service.ts b/nextstep-backend/src/services/posts_service.ts index 65ac591..643cb97 100644 --- a/nextstep-backend/src/services/posts_service.ts +++ b/nextstep-backend/src/services/posts_service.ts @@ -32,7 +32,7 @@ const addMisterAIComment = async (postId: string, postContent: string) => { misterAI = await usersService.addUser('misterai', 'securepassword', 'misterai@example.com', 'local'); } - const comment = await chatService.chatWithAI(postContent); + const comment = await chatService.chatWithAI(postContent, 'You are an AI assistant tasked with providing the first comment on forum posts. Your responses should be relevant, engaging, and encourage further discussion, also must be short, and you must answer if you know the answer. Ensure your comments are appropriate for the content and tone of the post. Also must answer in the language of the user post. answer short answers. dont ask questions to follow up'); const commentData: CommentData = { postId, owner: misterAI.id, content: comment }; const savedComment = await commentsService.addComment(commentData); return savedComment; diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts index 03efde2..4283dae 100644 --- a/nextstep-backend/src/services/resume_service.ts +++ b/nextstep-backend/src/services/resume_service.ts @@ -2,20 +2,73 @@ import { config } from '../config/config'; import path from 'path'; import fs from 'fs'; import axios from 'axios'; +import { chatWithAI } from './chat_api_service'; -const scoreResume = async (resumePath: string, jobDescription?: string): Promise => { +const systemTemplate = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named Bob the Resume builder. +You will review resumes with or without job descriptions. +You are an expert in resume evaluation and provide constructive feedback with dynamic evaluation. +You should also provide an improvement table, taking into account: +- Content (Medium priority) +- Keyword matching (High priority) +- Hard skills (High priority) +- Soft skills (High priority) +- Overall presentation (Low priority)`; + +const feedbackTemplate = (resumeText: string, jdText: string) => ` +Resume Feedback Report +Here is the resume you provided: +${resumeText} +And the job description: +${jdText} + +Create the Improvement Table in relevance to the resume and give the consideration and suggestion for each section strictly following +the pattern as below and don't just out this guided pattern : +| Area | Consideration | Status | Suggestions | +| ------------- | --------------------------------------------------------------- | ------ | ----------- | +| Content | Measurable Results: At least 5 specific achievements or impact. | Low | | +| | Words to avoid: Negative phrases or clichés. | | | +| Keywords | Hard Skills: Presence and frequency of hard skills. | High | | +| | Soft Skills: Presence and frequency of soft skills. | | | +| Presentation | Education Match: Does the resume list a degree that matches the job requirements? | High | | + +Strengths: +List the strengths of the resume here. + +Detailed Feedback: +Provide detailed feedback on the resume's content, structure, grammar, and relevance to the job description. + +Suggestions: +Provide actionable suggestions for improvement, including specific keywords to include and skills to highlight. + +Based on your analysis, provide a numerical score between 0-100 that represents the overall quality and match of the resume. +The score should be provided at the end of your response in the format: "SCORE: X" where X is the numerical score. +`; + +const scoreResume = async (resumePath: string, jobDescription?: string): Promise<{ score: number; feedback: string }> => { try { - // Here you would integrate with an actual ATS system - // For now, we'll return a mock score - // In a real implementation, you would: - // 1. Parse the resume text - // 2. Compare it with the job description - // 3. Calculate a score based on keywords, skills, experience, etc. + // Read the resume file + const resumeText = fs.readFileSync(resumePath, 'utf-8'); + + // Prepare the prompt for the AI + const prompt = feedbackTemplate(resumeText, jobDescription || 'No job description provided.'); + + let feedback = 'The Chat AI feature is turned off. Could not score your resume.'; + + if (config.chatAi.turned_on()) { + // Get feedback from the AI + feedback = await chatWithAI(prompt, systemTemplate); + } + + // Extract the score from the feedback + const scoreMatch = feedback.match(/SCORE: (\d+)/); + const score = scoreMatch ? parseInt(scoreMatch[1]) : 0; - // Mock implementation - const score = Math.floor(Math.random() * 100); - return score; + return { + score, + feedback + }; } catch (error) { + console.error('Error scoring resume:', error); throw new Error('Failed to score resume'); } }; From 441874ebc83687c9f4f3e4a404a9aadaaff21c97 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 11:43:40 +0300 Subject: [PATCH 12/29] Add Streaming Resume Feedback Score Signed-off-by: Tal Jacob --- .../src/controllers/resume_controller.ts | 41 +++++++- nextstep-backend/src/openapi/swagger.yaml | 38 ++++++++ nextstep-backend/src/routes/resume_routes.ts | 2 + .../src/services/chat_api_service.ts | 95 ++++++++++++++++++- .../src/services/resume_service.ts | 62 +++++++++--- 5 files changed, 219 insertions(+), 19 deletions(-) diff --git a/nextstep-backend/src/controllers/resume_controller.ts b/nextstep-backend/src/controllers/resume_controller.ts index e272e8e..925f0f0 100644 --- a/nextstep-backend/src/controllers/resume_controller.ts +++ b/nextstep-backend/src/controllers/resume_controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { config } from '../config/config'; import fs from 'fs'; import path from 'path'; -import { scoreResume } from '../services/resume_service'; +import { scoreResume, streamScoreResume } from '../services/resume_service'; import multer from 'multer'; import { CustomRequest } from "types/customRequest"; import { handleError } from "../utils/handle_error"; @@ -24,7 +24,44 @@ const getResumeScore = async (req: Request, res: Response) => { } }; +const getStreamResumeScore = async (req: Request, res: Response) => { + try { + const { filename } = req.params; + const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename); + const jobDescription = req.query.jobDescription as string; + + if (!fs.existsSync(resumePath)) { + return res.status(404).send('Resume not found'); + } + + // Set headers for SSE + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + // Handle client disconnect + req.on('close', () => { + res.end(); + }); + + // Stream the response + const score = await streamScoreResume( + resumePath, + jobDescription, + (chunk) => { + res.write(`data: ${JSON.stringify({ chunk })}\n\n`); + } + ); + + // Send the final score + res.write(`data: ${JSON.stringify({ score, done: true })}\n\n`); + res.end(); + } catch (error) { + handleError(error, res); + } +}; export default { getResumeScore, -}; \ No newline at end of file + getStreamResumeScore +}; \ No newline at end of file diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index 00a6d20..5b64fc2 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -903,6 +903,44 @@ paths: '500': description: Internal server error + /resume/streamScore/{filename}: + get: + tags: + - Resume + summary: Get stream score for an existing resume + security: + - BearerAuth: [] + parameters: + - name: filename + in: path + required: true + schema: + type: string + description: The filename of the resume + - name: jobDescription + in: query + required: false + schema: + type: string + description: Optional job description for scoring + responses: + '200': + description: Stream resume score retrieved successfully + content: + application/json: + schema: + type: object + properties: + score: + type: number + description: The stream ATS score of the resume + '401': + description: Unauthorized + '404': + description: Resume not found + '500': + description: Internal server error + /room/user/{receiverUserId}: get: tags: diff --git a/nextstep-backend/src/routes/resume_routes.ts b/nextstep-backend/src/routes/resume_routes.ts index eb1f8ba..92b4611 100644 --- a/nextstep-backend/src/routes/resume_routes.ts +++ b/nextstep-backend/src/routes/resume_routes.ts @@ -6,4 +6,6 @@ const router = express.Router(); router.get('/score/:filename', Resume.getResumeScore); +router.get('/streamScore/:filename', Resume.getStreamResumeScore); + export default router; \ No newline at end of file diff --git a/nextstep-backend/src/services/chat_api_service.ts b/nextstep-backend/src/services/chat_api_service.ts index 8284d0d..a21eae8 100644 --- a/nextstep-backend/src/services/chat_api_service.ts +++ b/nextstep-backend/src/services/chat_api_service.ts @@ -5,9 +5,101 @@ const cleanResponse = (response: string): string => { return response.replace(/\\boxed{(.*?)}/g, "$1"); // Removes \boxed{} } +/** + * @example + * await streamChatWithAI( + * "How would you build the tallest building ever?", + * "You are a helpful assistant.", + * (chunk) => { + * // Handle each chunk of the response + * console.log(chunk); + * // Or update your UI with the chunk + * } + * ); + */ +export const streamChatWithAI = async ( + userMessageContent: string, + systemMessageContent: string, + onChunk: (chunk: string) => void +): Promise => { + try { + const API_URL = config.chatAi.api_url(); + const API_KEY = config.chatAi.api_key(); + const MODEL_NAME = config.chatAi.model_name(); + const systemMessage = { + role: 'system', + content: systemMessageContent + }; -export const chatWithAI = async (userMessageContent: string, systemMessageContent: string)=> { + const userMessage = { + role: 'user', + content: userMessageContent + }; + + const response = await fetch(API_URL, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: MODEL_NAME, + messages: [systemMessage, userMessage], + stream: true, + }), + }); + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('Response body is not readable'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // Append new chunk to buffer + buffer += decoder.decode(value, { stream: true }); + + // Process complete lines from buffer + while (true) { + const lineEnd = buffer.indexOf('\n'); + if (lineEnd === -1) break; + + const line = buffer.slice(0, lineEnd).trim(); + buffer = buffer.slice(lineEnd + 1); + + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') break; + + try { + const parsed = JSON.parse(data); + const content = parsed.choices[0].delta.content; + if (content) { + onChunk(content); + } + } catch (e) { + // Ignore invalid JSON + } + } + } + } + } finally { + reader.cancel(); + } + } catch (error) { + console.error("Error streaming with OpenRouter:", error); + throw error; + } +} + +export const chatWithAI = async (userMessageContent: string, systemMessageContent: string): Promise => { try { const API_URL = config.chatAi.api_url(); const API_KEY = config.chatAi.api_key(); @@ -23,7 +115,6 @@ export const chatWithAI = async (userMessageContent: string, systemMessageConten content: userMessageContent }; - const response = await axios.post( API_URL, { diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts index 4283dae..657005f 100644 --- a/nextstep-backend/src/services/resume_service.ts +++ b/nextstep-backend/src/services/resume_service.ts @@ -2,9 +2,9 @@ import { config } from '../config/config'; import path from 'path'; import fs from 'fs'; import axios from 'axios'; -import { chatWithAI } from './chat_api_service'; +import { chatWithAI, streamChatWithAI } from './chat_api_service'; -const systemTemplate = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named Bob the Resume builder. +const SYSTEM_TEMPLATE = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named BOb the Resume builder. You will review resumes with or without job descriptions. You are an expert in resume evaluation and provide constructive feedback with dynamic evaluation. You should also provide an improvement table, taking into account: @@ -44,33 +44,65 @@ Based on your analysis, provide a numerical score between 0-100 that represents The score should be provided at the end of your response in the format: "SCORE: X" where X is the numerical score. `; +const FEEDBACK_ERROR_MESSAGE = 'The Chat AI feature is turned off. Could not score your resume.'; + const scoreResume = async (resumePath: string, jobDescription?: string): Promise<{ score: number; feedback: string }> => { try { - // Read the resume file const resumeText = fs.readFileSync(resumePath, 'utf-8'); - - // Prepare the prompt for the AI const prompt = feedbackTemplate(resumeText, jobDescription || 'No job description provided.'); - - let feedback = 'The Chat AI feature is turned off. Could not score your resume.'; + let feedback = FEEDBACK_ERROR_MESSAGE; if (config.chatAi.turned_on()) { // Get feedback from the AI - feedback = await chatWithAI(prompt, systemTemplate); + feedback = await chatWithAI(prompt, SYSTEM_TEMPLATE); } - + // Extract the score from the feedback const scoreMatch = feedback.match(/SCORE: (\d+)/); const score = scoreMatch ? parseInt(scoreMatch[1]) : 0; - - return { - score, - feedback - }; + return { score, feedback }; } catch (error) { console.error('Error scoring resume:', error); throw new Error('Failed to score resume'); } }; -export { scoreResume }; \ No newline at end of file +const streamScoreResume = async ( + resumePath: string, + jobDescription: string | undefined, + onChunk: (chunk: string) => void +): Promise => { + try { + const resumeText = fs.readFileSync(resumePath, 'utf-8'); + const prompt = feedbackTemplate(resumeText, jobDescription || 'No job description provided.'); + + let fullResponse = ''; + let finalScore = 0; + + if (config.chatAi.turned_on()) { + await streamChatWithAI( + prompt, + SYSTEM_TEMPLATE, + (chunk) => { + fullResponse += chunk; + onChunk(chunk); + + // Try to extract score from the accumulated response + const scoreMatch = fullResponse.match(/SCORE: (\d+)/); + if (scoreMatch) { + finalScore = parseInt(scoreMatch[1]); + } + } + ); + } else { + onChunk(FEEDBACK_ERROR_MESSAGE); + } + + return finalScore; + } catch (error) { + console.error('Error streaming resume score:', error); + throw new Error('Failed to stream resume score'); + } +}; + +export { scoreResume, streamScoreResume }; \ No newline at end of file From fffa215432d0f6ae3a17413996fff3638851e1d2 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 12:11:27 +0300 Subject: [PATCH 13/29] Fix Parsing From Pdf, Docx, Doc And Txt Signed-off-by: Tal Jacob --- nextstep-backend/package.json | 8 ++- .../src/services/resume_service.ts | 52 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/nextstep-backend/package.json b/nextstep-backend/package.json index a1e3ccb..67d1f63 100644 --- a/nextstep-backend/package.json +++ b/nextstep-backend/package.json @@ -20,6 +20,8 @@ }, "homepage": "https://github.com/NextStepFinalProject/NextStep#readme", "dependencies": { + "@types/pdf-parse": "^1.1.5", + "axios": "^1.8.3", "bcrypt": "^5.1.1", "body-parser": "^1.20.3", "cors": "^2.8.5", @@ -34,13 +36,15 @@ "jest-junit": "^16.0.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", + "mammoth": "^1.9.0", "mongoose": "^8.8.2", "multer": "^1.4.5-lts.1", + "office-text-extractor": "^3.0.3", + "pdf-lib": "^1.17.1", "socket.io": "^4.8.1", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", - "axios": "^1.8.3" + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@babel/preset-env": "^7.26.0", diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts index 657005f..ddcecc6 100644 --- a/nextstep-backend/src/services/resume_service.ts +++ b/nextstep-backend/src/services/resume_service.ts @@ -3,6 +3,8 @@ import path from 'path'; import fs from 'fs'; import axios from 'axios'; import { chatWithAI, streamChatWithAI } from './chat_api_service'; +import mammoth from 'mammoth'; +import pdfParse from 'pdf-parse'; const SYSTEM_TEMPLATE = `You are a very experienced ATS (Application Tracking System) bot with a deep understanding named BOb the Resume builder. You will review resumes with or without job descriptions. @@ -46,9 +48,51 @@ The score should be provided at the end of your response in the format: "SCORE: const FEEDBACK_ERROR_MESSAGE = 'The Chat AI feature is turned off. Could not score your resume.'; +const parseDocument = async (filePath: string): Promise => { + const ext = path.extname(filePath).toLowerCase(); + + try { + switch (ext) { + case '.pdf': + return await parsePdf(filePath); + case '.docx': + case '.doc': + return await parseWord(filePath); + case '.txt': + return fs.readFileSync(filePath, 'utf-8'); + default: + throw new Error(`Unsupported file format: ${ext}`); + } + } catch (error: any) { + console.error(`Error parsing document ${filePath}:`, error); + throw new Error(`Failed to parse document: ${error.message}`); + } +}; + +const parsePdf = async (filePath: string): Promise => { + try { + const dataBuffer = fs.readFileSync(filePath); + const data = await pdfParse(dataBuffer); + return data.text; + } catch (error: any) { + console.error('Error parsing PDF:', error); + throw new Error('Failed to parse PDF document'); + } +}; + +const parseWord = async (filePath: string): Promise => { + try { + const result = await mammoth.extractRawText({ path: filePath }); + return result.value; + } catch (error: any) { + console.error('Error parsing Word document:', error); + throw new Error('Failed to parse Word document'); + } +}; + const scoreResume = async (resumePath: string, jobDescription?: string): Promise<{ score: number; feedback: string }> => { try { - const resumeText = fs.readFileSync(resumePath, 'utf-8'); + const resumeText = await parseDocument(resumePath); const prompt = feedbackTemplate(resumeText, jobDescription || 'No job description provided.'); let feedback = FEEDBACK_ERROR_MESSAGE; @@ -61,7 +105,7 @@ const scoreResume = async (resumePath: string, jobDescription?: string): Promise const scoreMatch = feedback.match(/SCORE: (\d+)/); const score = scoreMatch ? parseInt(scoreMatch[1]) : 0; return { score, feedback }; - } catch (error) { + } catch (error: any) { console.error('Error scoring resume:', error); throw new Error('Failed to score resume'); } @@ -73,7 +117,7 @@ const streamScoreResume = async ( onChunk: (chunk: string) => void ): Promise => { try { - const resumeText = fs.readFileSync(resumePath, 'utf-8'); + const resumeText = await parseDocument(resumePath); const prompt = feedbackTemplate(resumeText, jobDescription || 'No job description provided.'); let fullResponse = ''; @@ -99,7 +143,7 @@ const streamScoreResume = async ( } return finalScore; - } catch (error) { + } catch (error: any) { console.error('Error streaming resume score:', error); throw new Error('Failed to stream resume score'); } From 5b3dc1e7d4b20c85fbdb636c18d866590f367bad Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 12:14:18 +0300 Subject: [PATCH 14/29] Support Uploading Resume As `.txt`/`.text` Signed-off-by: Tal Jacob --- nextstep-backend/src/openapi/swagger.yaml | 4 ++-- nextstep-backend/src/services/resources_service.ts | 4 ++-- nextstep-backend/src/services/resume_service.ts | 1 + nextstep-backend/src/tests/resources_resumes.test.ts | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index 5b64fc2..fe8d40d 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -800,7 +800,7 @@ paths: file: type: string format: binary - description: The resume file to upload (PDF, DOC, DOCX) + description: The resume file to upload (PDF, DOC, DOCX, TXT/TEXT) jobDescription: type: string description: Optional job description for scoring @@ -820,7 +820,7 @@ paths: fileTooLarge: value: "File too large" invalidFileType: - value: "Invalid file type. Only PDF, DOC, and DOCX files are allowed" + value: "Invalid file type. Only PDF, DOC, DOCX and TXT/TEXT files are allowed" noFileUploaded: value: "No file uploaded" '401': diff --git a/nextstep-backend/src/services/resources_service.ts b/nextstep-backend/src/services/resources_service.ts index 26fb108..8370c26 100644 --- a/nextstep-backend/src/services/resources_service.ts +++ b/nextstep-backend/src/services/resources_service.ts @@ -70,14 +70,14 @@ const createResumesStorage = () => { fileSize: config.resources.resumeMaxSize() }, fileFilter: (req, file, cb) => { - const allowedTypes = /pdf|doc|docx/; + const allowedTypes = /pdf|doc|docx|txt|text/; const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); const mimetype = allowedTypes.test(file.mimetype); if (extname && mimetype) { return cb(null, true); } else { - return cb(new TypeError(`Invalid file type. Only PDF, DOC, and DOCX files are allowed.`)); + return cb(new TypeError(`Invalid file type. Only PDF, DOC, DOCX and TXT/TEXT files are allowed.`)); } } }); diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts index ddcecc6..456d40c 100644 --- a/nextstep-backend/src/services/resume_service.ts +++ b/nextstep-backend/src/services/resume_service.ts @@ -59,6 +59,7 @@ const parseDocument = async (filePath: string): Promise => { case '.doc': return await parseWord(filePath); case '.txt': + case '.text': return fs.readFileSync(filePath, 'utf-8'); default: throw new Error(`Unsupported file format: ${ext}`); diff --git a/nextstep-backend/src/tests/resources_resumes.test.ts b/nextstep-backend/src/tests/resources_resumes.test.ts index 7703517..3ad3b2d 100644 --- a/nextstep-backend/src/tests/resources_resumes.test.ts +++ b/nextstep-backend/src/tests/resources_resumes.test.ts @@ -126,10 +126,10 @@ describe('Resume API Tests', () => { it('should validate file type', async () => { const response = await request(app) .post('/resource/resume') - .attach('file', Buffer.from('invalid content'), { filename: 'test.txt' }) + .attach('file', Buffer.from('invalid content'), { filename: 'test.png' }) .expect(400); - expect(response.text).toBe('Invalid file type. Only PDF, DOC, and DOCX files are allowed.'); + expect(response.text).toBe('Invalid file type. Only PDF, DOC, DOCX and TXT/TEXT files are allowed.'); }); it('should handle missing file', async () => { From 5d6ad0cb9d2faf28a3e13c00a24df1688e8111cd Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 12:56:50 +0300 Subject: [PATCH 15/29] Remove Redundant jobDescription From `swagger.yaml` Of `/resource/resume` Signed-off-by: Tal Jacob --- nextstep-backend/src/openapi/swagger.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index fe8d40d..f7325e9 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -801,9 +801,6 @@ paths: type: string format: binary description: The resume file to upload (PDF, DOC, DOCX, TXT/TEXT) - jobDescription: - type: string - description: Optional job description for scoring responses: '201': description: Resume uploaded successfully From a2e1be5cddc25a08be64d947263bdf58ac0e06b8 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 13:14:58 +0300 Subject: [PATCH 16/29] Throw Error If We Cannot Parse Resume File Signed-off-by: Tal Jacob --- .../src/controllers/resume_controller.ts | 12 ++++++++-- .../src/services/resume_service.ts | 24 +++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/nextstep-backend/src/controllers/resume_controller.ts b/nextstep-backend/src/controllers/resume_controller.ts index 925f0f0..3b6d70f 100644 --- a/nextstep-backend/src/controllers/resume_controller.ts +++ b/nextstep-backend/src/controllers/resume_controller.ts @@ -20,7 +20,11 @@ const getResumeScore = async (req: Request, res: Response) => { const scoreAndFeedback = await scoreResume(resumePath, jobDescription); return res.status(200).send(scoreAndFeedback); } catch (error) { - handleError(error, res); + if (error instanceof TypeError) { + return res.status(400).send(error.message); + } else { + handleError(error, res); + } } }; @@ -57,7 +61,11 @@ const getStreamResumeScore = async (req: Request, res: Response) => { res.write(`data: ${JSON.stringify({ score, done: true })}\n\n`); res.end(); } catch (error) { - handleError(error, res); + if (error instanceof TypeError) { + return res.status(400).send(error.message); + } else { + handleError(error, res); + } } }; diff --git a/nextstep-backend/src/services/resume_service.ts b/nextstep-backend/src/services/resume_service.ts index 456d40c..5fbbdfd 100644 --- a/nextstep-backend/src/services/resume_service.ts +++ b/nextstep-backend/src/services/resume_service.ts @@ -94,6 +94,9 @@ const parseWord = async (filePath: string): Promise => { const scoreResume = async (resumePath: string, jobDescription?: string): Promise<{ score: number; feedback: string }> => { try { const resumeText = await parseDocument(resumePath); + if (resumeText.trim() == '') { + throw new TypeError('Could not parse the resume file'); + } const prompt = feedbackTemplate(resumeText, jobDescription || 'No job description provided.'); let feedback = FEEDBACK_ERROR_MESSAGE; @@ -107,8 +110,13 @@ const scoreResume = async (resumePath: string, jobDescription?: string): Promise const score = scoreMatch ? parseInt(scoreMatch[1]) : 0; return { score, feedback }; } catch (error: any) { - console.error('Error scoring resume:', error); - throw new Error('Failed to score resume'); + if (error instanceof TypeError) { + console.error('TypeError while scoring resume:', error); + throw error; + } else { + console.error('Unexpected error while scoring resume:', error); + throw new Error('Failed to score resume'); + } } }; @@ -119,6 +127,9 @@ const streamScoreResume = async ( ): Promise => { try { const resumeText = await parseDocument(resumePath); + if (resumeText.trim() == '') { + throw new TypeError('Could not parse the resume file'); + } const prompt = feedbackTemplate(resumeText, jobDescription || 'No job description provided.'); let fullResponse = ''; @@ -145,8 +156,13 @@ const streamScoreResume = async ( return finalScore; } catch (error: any) { - console.error('Error streaming resume score:', error); - throw new Error('Failed to stream resume score'); + if (error instanceof TypeError) { + console.error('TypeError while streaming resume score:', error); + throw error; + } else { + console.error('Unexpected error while streaming resume score:', error); + throw new Error('Failed to stream resume score'); + } } }; From 59c037b4b0063d67d7039dbfd0961543e1b71dd6 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 13:28:01 +0300 Subject: [PATCH 17/29] Document 400 HttpStatus In `swagger.yaml` when could not parse the resume file Signed-off-by: Tal Jacob --- nextstep-backend/src/openapi/swagger.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nextstep-backend/src/openapi/swagger.yaml b/nextstep-backend/src/openapi/swagger.yaml index f7325e9..5b51507 100644 --- a/nextstep-backend/src/openapi/swagger.yaml +++ b/nextstep-backend/src/openapi/swagger.yaml @@ -893,6 +893,13 @@ paths: score: type: number description: The ATS score of the resume + '400': + description: Bad request + content: + text/plain: + examples: + couldNotParseResumeFile: + value: "Could not parse the resume file" '401': description: Unauthorized '404': @@ -931,6 +938,13 @@ paths: score: type: number description: The stream ATS score of the resume + '400': + description: Bad request + content: + text/plain: + examples: + couldNotParseResumeFile: + value: "Could not parse the resume file" '401': description: Unauthorized '404': From 3d7a3131de2dce027d123824068d1884c5e8fdec Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 14:01:32 +0300 Subject: [PATCH 18/29] Add Initial ResumePage For Scoring Resume Signed-off-by: Tal Jacob --- nextstep-frontend/src/App.tsx | 3 +- nextstep-frontend/src/pages/ResumePage.tsx | 220 +++++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 nextstep-frontend/src/pages/ResumePage.tsx diff --git a/nextstep-frontend/src/App.tsx b/nextstep-frontend/src/App.tsx index 7d9e2f6..e273da6 100644 --- a/nextstep-frontend/src/App.tsx +++ b/nextstep-frontend/src/App.tsx @@ -10,7 +10,7 @@ import RequireAuth from './hoc/RequireAuth'; import NewPost from './pages/NewPost'; import PostDetails from './pages/PostDetails'; import Chat from './pages/Chat'; - +import ResumePage from './pages/ResumePage'; const App: React.FC = () => { return ( @@ -25,6 +25,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> diff --git a/nextstep-frontend/src/pages/ResumePage.tsx b/nextstep-frontend/src/pages/ResumePage.tsx new file mode 100644 index 0000000..436682e --- /dev/null +++ b/nextstep-frontend/src/pages/ResumePage.tsx @@ -0,0 +1,220 @@ +import React, { useState, useRef } from 'react'; +import { Box, Button, CircularProgress, Typography, TextField } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { useNavigate } from 'react-router-dom'; +import { config } from '../config'; +import api from '../serverApi'; +import TopBar from '../components/TopBar'; + +const UploadBox = styled(Box)(({ theme }) => ({ + border: '2px dashed #ccc', + borderRadius: '8px', + padding: '20px', + textAlign: 'center', + cursor: 'pointer', + '&:hover': { + borderColor: theme.palette.primary.main, + }, +})); + +const ScoreGauge = styled(Box)<{ score: number }>(({ theme, score }) => ({ + width: '200px', + height: '200px', + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: `conic-gradient( + ${theme.palette.error.main} 0% 33%, + ${theme.palette.warning.main} 33% 66%, + ${theme.palette.success.main} 66% 100% + )`, + position: 'relative', + '&::before': { + content: '""', + position: 'absolute', + width: '180px', + height: '180px', + borderRadius: '50%', + background: theme.palette.background.paper, + }, +})); + +const ScoreText = styled(Typography)({ + position: 'absolute', + fontSize: '2.5rem', + fontWeight: 'bold', +}); + +const ResumePage: React.FC = () => { + const [file, setFile] = useState(null); + const [jobDescription, setJobDescription] = useState(''); + const [feedback, setFeedback] = useState(''); + const [score, setScore] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const fileInputRef = useRef(null); + const navigate = useNavigate(); + + const uploadResume = async (formData: FormData) => { + // First, upload the resume file to get the filename + const response = await api.post('/resource/resume', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return response.data; + }; + + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files[0]) { + setFile(event.target.files[0]); + setError(''); + } + }; + + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; + + const handleSubmit = async () => { + if (!file) { + setError('Please select a file'); + return; + } + + setLoading(true); + setFeedback(''); + setScore(null); + setError(''); + + try { + const formData = new FormData(); + formData.append('file', file); + + // First upload the file + const filename = await uploadResume(formData); + + // Then set up the EventSource for streaming with authentication + const token = localStorage.getItem(config.localStorageKeys.userAuth) + ? JSON.parse(localStorage.getItem(config.localStorageKeys.userAuth)!).accessToken + : ''; + + const eventSource = new EventSource( + `${config.app.backend_url()}/resume/streamScore/${filename}?jobDescription=${encodeURIComponent(jobDescription)}&token=${encodeURIComponent(token)}` + ); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + if (data.done) { + // Final score received + setScore(data.score); + eventSource.close(); + setLoading(false); + } else if (data.chunk) { + // Streamed chunk received + setFeedback(prev => prev + data.chunk); + } + } catch (e) { + console.error('Error parsing event data:', e); + } + }; + + eventSource.onerror = (error) => { + console.error('EventSource failed:', error); + setError('Failed to analyze resume'); + eventSource.close(); + setLoading(false); + }; + + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + setLoading(false); + } + }; + + return ( + + + + Resume Score Analyzer + + + + setJobDescription(e.target.value)} + sx={{ mb: 2 }} + /> + + + + + {file ? ( + {file.name} + ) : ( + Click to upload your resume + )} + + + {error && ( + + {error} + + )} + + + + + {loading && } + + {feedback && ( + + + Analysis Feedback: + + + {feedback} + + + )} + + {score !== null && ( + + + {score} + + + )} + + ); +}; + +export default ResumePage; \ No newline at end of file From d0e3d8e06c63c6e70825973add9210c97e766cfc Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 14:45:11 +0300 Subject: [PATCH 19/29] Setup Token In Query Params For Streaming Scoring Resume Signed-off-by: Tal Jacob --- nextstep-backend/src/middleware/auth.ts | 10 +++++++++- nextstep-frontend/src/pages/ResumePage.tsx | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/nextstep-backend/src/middleware/auth.ts b/nextstep-backend/src/middleware/auth.ts index 56c8ddc..38b1dda 100644 --- a/nextstep-backend/src/middleware/auth.ts +++ b/nextstep-backend/src/middleware/auth.ts @@ -11,8 +11,16 @@ const getTokenFromHeader = (req: CustomRequest): string | undefined => { return authHeader?.split(' ')[1]; } +const getTokenFromQueryParams = (req: CustomRequest): string | undefined => { + return req.query.accessToken as string; +} + const authenticateTokenHandler: any & { unless: typeof unless } = async (req: CustomRequest, res: Response, next: NextFunction, ignoreExpiration = false): Promise => { - const token = getTokenFromHeader(req); + let token = getTokenFromHeader(req); + if (!token) { + // If the token was not found in the headers, fall back to the query params. + token = getTokenFromQueryParams(req); + } if (!token) { res.status(401).json({ message: 'Access token required' }); diff --git a/nextstep-frontend/src/pages/ResumePage.tsx b/nextstep-frontend/src/pages/ResumePage.tsx index 436682e..b26ca76 100644 --- a/nextstep-frontend/src/pages/ResumePage.tsx +++ b/nextstep-frontend/src/pages/ResumePage.tsx @@ -102,7 +102,7 @@ const ResumePage: React.FC = () => { : ''; const eventSource = new EventSource( - `${config.app.backend_url()}/resume/streamScore/${filename}?jobDescription=${encodeURIComponent(jobDescription)}&token=${encodeURIComponent(token)}` + `${config.app.backend_url()}/resume/streamScore/${filename}?jobDescription=${encodeURIComponent(jobDescription)}&accessToken=${encodeURIComponent(token)}` ); eventSource.onmessage = (event) => { From dadad0acc34c73bff11262d2f9578aa7b463805c Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 14:49:13 +0300 Subject: [PATCH 20/29] Update UI With Markdown Formatting And AutoScroll Signed-off-by: Tal Jacob --- nextstep-frontend/package.json | 2 + nextstep-frontend/src/pages/ResumePage.tsx | 55 +++++++++++++++++----- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/nextstep-frontend/package.json b/nextstep-frontend/package.json index e2f4d35..a2838ff 100644 --- a/nextstep-frontend/package.json +++ b/nextstep-frontend/package.json @@ -25,7 +25,9 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-froala-wysiwyg": "^4.5.0", + "react-markdown": "^10.1.0", "react-router-dom": "^7.1.5", + "remark-gfm": "^4.0.1", "socket.io-client": "^4.8.1" }, "devDependencies": { diff --git a/nextstep-frontend/src/pages/ResumePage.tsx b/nextstep-frontend/src/pages/ResumePage.tsx index b26ca76..933fa6c 100644 --- a/nextstep-frontend/src/pages/ResumePage.tsx +++ b/nextstep-frontend/src/pages/ResumePage.tsx @@ -1,10 +1,12 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { Box, Button, CircularProgress, Typography, TextField } from '@mui/material'; import { styled } from '@mui/material/styles'; import { useNavigate } from 'react-router-dom'; import { config } from '../config'; import api from '../serverApi'; import TopBar from '../components/TopBar'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; const UploadBox = styled(Box)(({ theme }) => ({ border: '2px dashed #ccc', @@ -46,6 +48,33 @@ const ScoreText = styled(Typography)({ fontWeight: 'bold', }); +const FeedbackContainer = styled(Box)(({ theme }) => ({ + maxHeight: '60vh', + overflowY: 'auto', + padding: theme.spacing(2), + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + '& pre': { + backgroundColor: theme.palette.grey[100], + padding: theme.spacing(1), + borderRadius: theme.shape.borderRadius, + overflowX: 'auto', + }, + '& table': { + borderCollapse: 'collapse', + width: '100%', + marginBottom: theme.spacing(2), + }, + '& th, & td': { + border: `1px solid ${theme.palette.divider}`, + padding: theme.spacing(1), + textAlign: 'left', + }, + '& th': { + backgroundColor: theme.palette.grey[100], + }, +})); + const ResumePage: React.FC = () => { const [file, setFile] = useState(null); const [jobDescription, setJobDescription] = useState(''); @@ -54,8 +83,16 @@ const ResumePage: React.FC = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const fileInputRef = useRef(null); + const feedbackEndRef = useRef(null); const navigate = useNavigate(); + // Auto-scroll to bottom when feedback updates + useEffect(() => { + if (feedbackEndRef.current) { + feedbackEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [feedback]); + const uploadResume = async (formData: FormData) => { // First, upload the resume file to get the filename const response = await api.post('/resource/resume', formData, { @@ -193,16 +230,12 @@ const ResumePage: React.FC = () => { Analysis Feedback: - - {feedback} - + + + {feedback} + +
+ )} From 3714195808fc8a37f67ebae3915a62db52ae8fec Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 14:50:48 +0300 Subject: [PATCH 21/29] Align The Response Text To The Left Signed-off-by: Tal Jacob --- nextstep-frontend/src/pages/ResumePage.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nextstep-frontend/src/pages/ResumePage.tsx b/nextstep-frontend/src/pages/ResumePage.tsx index 933fa6c..e38fdde 100644 --- a/nextstep-frontend/src/pages/ResumePage.tsx +++ b/nextstep-frontend/src/pages/ResumePage.tsx @@ -54,6 +54,7 @@ const FeedbackContainer = styled(Box)(({ theme }) => ({ padding: theme.spacing(2), backgroundColor: theme.palette.background.paper, borderRadius: theme.shape.borderRadius, + textAlign: 'left', '& pre': { backgroundColor: theme.palette.grey[100], padding: theme.spacing(1), @@ -73,6 +74,12 @@ const FeedbackContainer = styled(Box)(({ theme }) => ({ '& th': { backgroundColor: theme.palette.grey[100], }, + '& h1, & h2, & h3, & h4, & h5, & h6': { + textAlign: 'left', + }, + '& p': { + textAlign: 'left', + }, })); const ResumePage: React.FC = () => { From 3d7f38f335aca2f25695d3085acf0f4d8c3e661c Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 16:28:36 +0300 Subject: [PATCH 22/29] Working On Refactoring CSS Signed-off-by: Tal Jacob --- nextstep-frontend/src/App.css | 8 +- nextstep-frontend/src/App.tsx | 11 +- nextstep-frontend/src/components/TopBar.css | 3 + nextstep-frontend/src/components/TopBar.tsx | 3 +- nextstep-frontend/src/index.css | 352 ++++++++++++++++++++ nextstep-frontend/src/pages/Chat.css | 6 +- nextstep-frontend/src/pages/Chat.tsx | 2 - nextstep-frontend/src/pages/Dashboard.tsx | 245 +++++++------- nextstep-frontend/src/pages/Login.tsx | 2 +- nextstep-frontend/src/pages/NewPost.tsx | 1 - nextstep-frontend/src/pages/PostDetails.tsx | 2 - nextstep-frontend/src/pages/Profile.tsx | 6 +- nextstep-frontend/src/pages/Register.tsx | 2 +- 13 files changed, 500 insertions(+), 143 deletions(-) create mode 100644 nextstep-frontend/src/components/TopBar.css diff --git a/nextstep-frontend/src/App.css b/nextstep-frontend/src/App.css index 7a9fb29..b93828f 100644 --- a/nextstep-frontend/src/App.css +++ b/nextstep-frontend/src/App.css @@ -9,9 +9,13 @@ } #root { - max-width: 1280px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + height: 100vh; + width: 100%; margin: 0 auto; - padding: 2rem; text-align: center; } diff --git a/nextstep-frontend/src/App.tsx b/nextstep-frontend/src/App.tsx index e273da6..ef95b06 100644 --- a/nextstep-frontend/src/App.tsx +++ b/nextstep-frontend/src/App.tsx @@ -11,6 +11,7 @@ import NewPost from './pages/NewPost'; import PostDetails from './pages/PostDetails'; import Chat from './pages/Chat'; import ResumePage from './pages/ResumePage'; +import TopBar from './components/TopBar'; const App: React.FC = () => { return ( @@ -20,11 +21,11 @@ const App: React.FC = () => { } /> } /> } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/nextstep-frontend/src/components/TopBar.css b/nextstep-frontend/src/components/TopBar.css new file mode 100644 index 0000000..3c0e317 --- /dev/null +++ b/nextstep-frontend/src/components/TopBar.css @@ -0,0 +1,3 @@ +.top-bar { + margin-bottom: 10vh; +} \ No newline at end of file diff --git a/nextstep-frontend/src/components/TopBar.tsx b/nextstep-frontend/src/components/TopBar.tsx index 23e1333..80bb681 100644 --- a/nextstep-frontend/src/components/TopBar.tsx +++ b/nextstep-frontend/src/components/TopBar.tsx @@ -4,6 +4,7 @@ import { AppBar, Toolbar, IconButton, Tooltip, Box } from '@mui/material'; import { Home, Person, Message, Logout } from '@mui/icons-material'; import {getUserAuth, removeUserAuth} from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; +import './TopBar.css'; const TopBar: React.FC = () => { const userAuthRef = useRef(getUserAuth()); @@ -20,7 +21,7 @@ const TopBar: React.FC = () => { }; return ( - + navigate('/dashboard')} sx={{ mx: 1 }}> diff --git a/nextstep-frontend/src/index.css b/nextstep-frontend/src/index.css index 6119ad9..1857355 100644 --- a/nextstep-frontend/src/index.css +++ b/nextstep-frontend/src/index.css @@ -66,3 +66,355 @@ button:focus-visible { background-color: #f9f9f9; } } + +/* ------------------------------------ Normalize.css --------------------------------- */ + +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + + html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} \ No newline at end of file diff --git a/nextstep-frontend/src/pages/Chat.css b/nextstep-frontend/src/pages/Chat.css index 26853f5..977cb5d 100644 --- a/nextstep-frontend/src/pages/Chat.css +++ b/nextstep-frontend/src/pages/Chat.css @@ -1,14 +1,16 @@ .chat-container { - margin-top: 70px; display: flex; flex-direction: column; - height: 100%; padding: 20px; background-color: #f9f9f9; border: 1px solid #ccc; border-radius: 8px; } +.online-users { + overflow-y: auto; +} + .message-input { display: flex; margin-bottom: 10px; diff --git a/nextstep-frontend/src/pages/Chat.tsx b/nextstep-frontend/src/pages/Chat.tsx index 15ceb7b..c35e9b2 100644 --- a/nextstep-frontend/src/pages/Chat.tsx +++ b/nextstep-frontend/src/pages/Chat.tsx @@ -6,7 +6,6 @@ import { LoginResponse } from '../models/LoginResponse'; import DividedList from '../components/DividedList'; import { Room } from '../models/Room'; import axios from 'axios'; -import TopBar from '../components/TopBar'; const Chat: React.FC = () => { const [messageContent, setMessageContent] = useState(''); @@ -81,7 +80,6 @@ const Chat: React.FC = () => { return (
-
{ }; return ( - - - - - - - setFilterByUser(!filterByUser)} - color="primary" - /> - } - label="Show My Posts" - /> - - - {isLoading ? ( - - - - ) : error ? ( - {error} - ) : ( - <> - - {posts.map((post) => ( - - navigate(`/post/${post.id}`)}> - - - - {post.ownerUsername || post.owner} - - - {post.title} - - - - - - - handleLikePost(e, post.id)} - color={isLikedByUser[post.id] ? 'primary' : 'default'} - > - - - - - - - - - - {post.owner === auth.userId && ( - handleOpenDialog(e, post.id)}> - + + + + + + + setFilterByUser(!filterByUser)} + color="primary" + /> + } + label="Show My Posts" + /> + + + {isLoading ? ( + + + + ) : error ? ( + {error} + ) : ( + <> + + {posts.map((post) => ( + + navigate(`/post/${post.id}`)}> + + + + {post.ownerUsername || post.owner} + + + {post.title} + + + + + + + handleLikePost(e, post.id)} + color={isLikedByUser[post.id] ? 'primary' : 'default'} + > + + + - )} - - - - ))} - - - )} + + + + + + {post.owner === auth.userId && ( + handleOpenDialog(e, post.id)}> + + + )} + + + + ))} + + + )} + + + + {`Page ${currentPage} of ${totalPages}`} + + - - - {`Page ${currentPage} of ${totalPages}`} - - - + + - - - {"Are you sure you want to delete this post?"} - - - This action cannot be undone. - - - - - - - - + ); }; diff --git a/nextstep-frontend/src/pages/Login.tsx b/nextstep-frontend/src/pages/Login.tsx index c37c95d..fec6550 100644 --- a/nextstep-frontend/src/pages/Login.tsx +++ b/nextstep-frontend/src/pages/Login.tsx @@ -67,7 +67,7 @@ const Login: React.FC = () => { return (
- + Next Step diff --git a/nextstep-frontend/src/pages/NewPost.tsx b/nextstep-frontend/src/pages/NewPost.tsx index 1f773ef..c855470 100644 --- a/nextstep-frontend/src/pages/NewPost.tsx +++ b/nextstep-frontend/src/pages/NewPost.tsx @@ -59,7 +59,6 @@ const NewPost: React.FC = () => { return ( - Create New Post diff --git a/nextstep-frontend/src/pages/PostDetails.tsx b/nextstep-frontend/src/pages/PostDetails.tsx index 82d6517..3a38d2f 100644 --- a/nextstep-frontend/src/pages/PostDetails.tsx +++ b/nextstep-frontend/src/pages/PostDetails.tsx @@ -8,7 +8,6 @@ import 'froala-editor/css/froala_editor.pkgd.min.css'; import 'froala-editor/js/plugins/image.min.js'; import { Post as PostModel } from '../models/Post'; import { Comment } from '../models/Comment'; -import TopBar from '../components/TopBar'; import { getUserAuth } from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; import defaultProfileImage from '../assets/defaultProfileImage.jpg'; @@ -196,7 +195,6 @@ const PostDetails: React.FC = () => { return ( - navigate('/dashboard')}> diff --git a/nextstep-frontend/src/pages/Profile.tsx b/nextstep-frontend/src/pages/Profile.tsx index 2a0217d..533cf4b 100644 --- a/nextstep-frontend/src/pages/Profile.tsx +++ b/nextstep-frontend/src/pages/Profile.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; import { Container, Typography, Box, Button, Alert, TextField, Collapse } from '@mui/material'; import { ExpandMore, ExpandLess } from '@mui/icons-material'; -import TopBar from '../components/TopBar'; import {getUserAuth} from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; import {UserProfile} from "../models/UserProfile.ts"; @@ -105,9 +104,8 @@ const Profile: React.FC = () => { }; return ( - - - + + Profile diff --git a/nextstep-frontend/src/pages/Register.tsx b/nextstep-frontend/src/pages/Register.tsx index 5c07b43..9fdaf48 100644 --- a/nextstep-frontend/src/pages/Register.tsx +++ b/nextstep-frontend/src/pages/Register.tsx @@ -41,7 +41,7 @@ const Register: React.FC = () => { return (
- + Next Step From 273af0015b104912afa7aafd39f88eb7dd436207 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 17:29:19 +0300 Subject: [PATCH 23/29] Add Common Layout For All Pages To Have Constant MarginTop Signed-off-by: Tal Jacob --- nextstep-frontend/src/App.tsx | 19 ++++++++++--------- nextstep-frontend/src/components/Layout.tsx | 13 +++++++++++++ nextstep-frontend/src/components/TopBar.css | 3 --- nextstep-frontend/src/components/TopBar.tsx | 1 - 4 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 nextstep-frontend/src/components/Layout.tsx delete mode 100644 nextstep-frontend/src/components/TopBar.css diff --git a/nextstep-frontend/src/App.tsx b/nextstep-frontend/src/App.tsx index ef95b06..a5b20ff 100644 --- a/nextstep-frontend/src/App.tsx +++ b/nextstep-frontend/src/App.tsx @@ -12,21 +12,22 @@ import PostDetails from './pages/PostDetails'; import Chat from './pages/Chat'; import ResumePage from './pages/ResumePage'; import TopBar from './components/TopBar'; +import Layout from './components/Layout'; const App: React.FC = () => { return ( <> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/nextstep-frontend/src/components/Layout.tsx b/nextstep-frontend/src/components/Layout.tsx new file mode 100644 index 0000000..6e91566 --- /dev/null +++ b/nextstep-frontend/src/components/Layout.tsx @@ -0,0 +1,13 @@ +import { Container } from '@mui/material'; +import React from 'react'; + + +const Layout: React.FC<{ children: React.ReactNode }> = (props: any) => { + return ( + + { props.children } + + ); +}; + +export default Layout; \ No newline at end of file diff --git a/nextstep-frontend/src/components/TopBar.css b/nextstep-frontend/src/components/TopBar.css deleted file mode 100644 index 3c0e317..0000000 --- a/nextstep-frontend/src/components/TopBar.css +++ /dev/null @@ -1,3 +0,0 @@ -.top-bar { - margin-bottom: 10vh; -} \ No newline at end of file diff --git a/nextstep-frontend/src/components/TopBar.tsx b/nextstep-frontend/src/components/TopBar.tsx index 80bb681..a0b5f5c 100644 --- a/nextstep-frontend/src/components/TopBar.tsx +++ b/nextstep-frontend/src/components/TopBar.tsx @@ -4,7 +4,6 @@ import { AppBar, Toolbar, IconButton, Tooltip, Box } from '@mui/material'; import { Home, Person, Message, Logout } from '@mui/icons-material'; import {getUserAuth, removeUserAuth} from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; -import './TopBar.css'; const TopBar: React.FC = () => { const userAuthRef = useRef(getUserAuth()); From 957d22a6903378ac64c5cf8caecc88ee8e512d5b Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 18:53:32 +0300 Subject: [PATCH 24/29] Add Resume To TopBar Signed-off-by: Tal Jacob --- nextstep-frontend/src/App.tsx | 2 +- nextstep-frontend/src/components/TopBar.tsx | 7 ++++++- nextstep-frontend/src/pages/ResumePage.tsx | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/nextstep-frontend/src/App.tsx b/nextstep-frontend/src/App.tsx index a5b20ff..351d6e6 100644 --- a/nextstep-frontend/src/App.tsx +++ b/nextstep-frontend/src/App.tsx @@ -27,7 +27,7 @@ const App: React.FC = () => { } /> } /> } /> - } /> + } /> } /> diff --git a/nextstep-frontend/src/components/TopBar.tsx b/nextstep-frontend/src/components/TopBar.tsx index a0b5f5c..2a3350d 100644 --- a/nextstep-frontend/src/components/TopBar.tsx +++ b/nextstep-frontend/src/components/TopBar.tsx @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { AppBar, Toolbar, IconButton, Tooltip, Box } from '@mui/material'; -import { Home, Person, Message, Logout } from '@mui/icons-material'; +import { Home, Person, Message, Logout, DocumentScannerTwoTone } from '@mui/icons-material'; import {getUserAuth, removeUserAuth} from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; @@ -37,6 +37,11 @@ const TopBar: React.FC = () => { + + navigate('/resume')} sx={{ mx: 1 }}> + + + diff --git a/nextstep-frontend/src/pages/ResumePage.tsx b/nextstep-frontend/src/pages/ResumePage.tsx index e38fdde..9d44daf 100644 --- a/nextstep-frontend/src/pages/ResumePage.tsx +++ b/nextstep-frontend/src/pages/ResumePage.tsx @@ -182,7 +182,6 @@ const ResumePage: React.FC = () => { return ( - Resume Score Analyzer From 2cfbd4458d286ba247e8ae149cd1552761d4eae2 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 19:42:04 +0300 Subject: [PATCH 25/29] Reorder Layout CSS And Index CSS Signed-off-by: Tal Jacob --- nextstep-frontend/src/components/Layout.css | 4 + nextstep-frontend/src/components/Layout.tsx | 3 +- nextstep-frontend/src/index.css | 142 ++++++++++---------- nextstep-frontend/src/pages/Login.css | 7 - nextstep-frontend/src/pages/Register.css | 10 -- nextstep-frontend/src/pages/Register.tsx | 1 - 6 files changed, 78 insertions(+), 89 deletions(-) create mode 100644 nextstep-frontend/src/components/Layout.css delete mode 100644 nextstep-frontend/src/pages/Register.css diff --git a/nextstep-frontend/src/components/Layout.css b/nextstep-frontend/src/components/Layout.css new file mode 100644 index 0000000..2e5e800 --- /dev/null +++ b/nextstep-frontend/src/components/Layout.css @@ -0,0 +1,4 @@ +.layout-container { + margin-top: 10vh; + overflow-y: auto; +} diff --git a/nextstep-frontend/src/components/Layout.tsx b/nextstep-frontend/src/components/Layout.tsx index 6e91566..55264e4 100644 --- a/nextstep-frontend/src/components/Layout.tsx +++ b/nextstep-frontend/src/components/Layout.tsx @@ -1,10 +1,11 @@ import { Container } from '@mui/material'; import React from 'react'; +import './Layout.css'; const Layout: React.FC<{ children: React.ReactNode }> = (props: any) => { return ( - + { props.children } ); diff --git a/nextstep-frontend/src/index.css b/nextstep-frontend/src/index.css index 1857355..2514a69 100644 --- a/nextstep-frontend/src/index.css +++ b/nextstep-frontend/src/index.css @@ -1,72 +1,3 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} - /* ------------------------------------ Normalize.css --------------------------------- */ /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ @@ -417,4 +348,75 @@ template { [hidden] { display: none; -} \ No newline at end of file +} + +/* ------------------------------------ Normalize.css End --------------------------------- */ + +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/nextstep-frontend/src/pages/Login.css b/nextstep-frontend/src/pages/Login.css index 77b40e2..ebf6be9 100644 --- a/nextstep-frontend/src/pages/Login.css +++ b/nextstep-frontend/src/pages/Login.css @@ -1,10 +1,3 @@ -body { - margin: 0; - font-family: Arial, sans-serif; - background-color: var(--color-2); - display: flex; -} - .login-container { display: flex; justify-content: center; diff --git a/nextstep-frontend/src/pages/Register.css b/nextstep-frontend/src/pages/Register.css deleted file mode 100644 index 307a063..0000000 --- a/nextstep-frontend/src/pages/Register.css +++ /dev/null @@ -1,10 +0,0 @@ -body { - margin: 0; - font-family: Arial, sans-serif; - background-color: var(--color-2); - display: flex; - justify-content: center; - align-items: center; - text-align: center; - overflow: hidden; -} diff --git a/nextstep-frontend/src/pages/Register.tsx b/nextstep-frontend/src/pages/Register.tsx index 9fdaf48..7ac8cf0 100644 --- a/nextstep-frontend/src/pages/Register.tsx +++ b/nextstep-frontend/src/pages/Register.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import axios from 'axios'; import { Container, TextField, Button, Typography, Box, Paper, Link, Alert } from '@mui/material'; import { Link as RouterLink, useNavigate } from 'react-router-dom'; -import './Register.css'; import { config } from '../config'; const Register: React.FC = () => { From 7a75de6bfc2bc8f4417e712c629ea0125485c0ec Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 19:48:03 +0300 Subject: [PATCH 26/29] Fix Footer Position To To Be Relative, While Keeping Top Margin 10vh Signed-off-by: Tal Jacob --- nextstep-frontend/src/App.css | 2 +- nextstep-frontend/src/components/Footer.css | 2 -- nextstep-frontend/src/components/Layout.css | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/nextstep-frontend/src/App.css b/nextstep-frontend/src/App.css index b93828f..683a2cf 100644 --- a/nextstep-frontend/src/App.css +++ b/nextstep-frontend/src/App.css @@ -11,7 +11,7 @@ #root { display: flex; flex-direction: column; - justify-content: flex-start; + justify-content: space-between; align-items: center; height: 100vh; width: 100%; diff --git a/nextstep-frontend/src/components/Footer.css b/nextstep-frontend/src/components/Footer.css index db365e7..404eb5f 100644 --- a/nextstep-frontend/src/components/Footer.css +++ b/nextstep-frontend/src/components/Footer.css @@ -4,7 +4,5 @@ padding: 10px 20px; text-align: center; width: 100%; - position: absolute; - bottom: 0; justify-self: anchor-center; } \ No newline at end of file diff --git a/nextstep-frontend/src/components/Layout.css b/nextstep-frontend/src/components/Layout.css index 2e5e800..2a137bd 100644 --- a/nextstep-frontend/src/components/Layout.css +++ b/nextstep-frontend/src/components/Layout.css @@ -1,4 +1,5 @@ .layout-container { + height: 100%; margin-top: 10vh; overflow-y: auto; } From ae157254f4b4558ebb6a2ca5573b19819d028d52 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 20:12:35 +0300 Subject: [PATCH 27/29] Extend Y Scroll Signed-off-by: Tal Jacob --- nextstep-frontend/src/components/Layout.css | 4 ++-- nextstep-frontend/src/components/Layout.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/nextstep-frontend/src/components/Layout.css b/nextstep-frontend/src/components/Layout.css index 2a137bd..1eb91d5 100644 --- a/nextstep-frontend/src/components/Layout.css +++ b/nextstep-frontend/src/components/Layout.css @@ -1,5 +1,5 @@ .layout-container { height: 100%; - margin-top: 10vh; + padding-top: 10vh; overflow-y: auto; -} +} \ No newline at end of file diff --git a/nextstep-frontend/src/components/Layout.tsx b/nextstep-frontend/src/components/Layout.tsx index 55264e4..7835387 100644 --- a/nextstep-frontend/src/components/Layout.tsx +++ b/nextstep-frontend/src/components/Layout.tsx @@ -5,7 +5,13 @@ import './Layout.css'; const Layout: React.FC<{ children: React.ReactNode }> = (props: any) => { return ( - + { props.children } ); From 6abad9a16d1f5b9e89fcde885d94876657fe1a0b Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 20:52:25 +0300 Subject: [PATCH 28/29] Fix Resume Tests With Authentication Bearer Signed-off-by: Tal Jacob --- .../src/tests/resources_resumes.test.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/nextstep-backend/src/tests/resources_resumes.test.ts b/nextstep-backend/src/tests/resources_resumes.test.ts index 3ad3b2d..6af14f4 100644 --- a/nextstep-backend/src/tests/resources_resumes.test.ts +++ b/nextstep-backend/src/tests/resources_resumes.test.ts @@ -7,15 +7,22 @@ import { config } from '../config/config'; import resumeRoutes from '../routes/resume_routes'; import { scoreResume } from '../services/resume_service'; import resourcesRoutes from '../routes/resources_routes'; +import { authenticateToken } from '../middleware/auth'; // Mock the resume service jest.mock('../services/resume_service'); +// Mock the authentication middleware +jest.mock('../middleware/auth', () => ({ + authenticateToken: jest.fn((req, res, next) => next()) +})); + describe('Resume API Tests', () => { let app: express.Application; const testResumePath = path.join(process.cwd(), 'test-resume.pdf'); const testResumeContent = 'Test resume content'; const testJobDescription = 'Software Engineer with 5 years of experience'; + const testToken = 'test-token'; beforeAll(() => { // Create the resumes directory if it doesn't exist @@ -47,15 +54,17 @@ describe('Resume API Tests', () => { // First upload a resume const uploadResponse = await request(app) .post('/resource/resume') + .set('Authorization', `Bearer ${testToken}`) .attach('file', testResumePath); const filename = uploadResponse.text; // Mock the scoreResume function - (scoreResume as jest.Mock).mockResolvedValue(85); + (scoreResume as jest.Mock).mockResolvedValue({ score: 85 }); const response = await request(app) .get(`/resume/score/${filename}?jobDescription=${encodeURIComponent(testJobDescription)}`) + .set('Authorization', `Bearer ${testToken}`) .expect(200); expect(response.body).toHaveProperty('score'); @@ -75,6 +84,7 @@ describe('Resume API Tests', () => { it('should return 404 for non-existent resume', async () => { const response = await request(app) .get('/resume/score/nonexistent.pdf') + .set('Authorization', `Bearer ${testToken}`) .expect(404); expect(response.text).toBe('Resume not found'); @@ -84,6 +94,7 @@ describe('Resume API Tests', () => { // First upload a resume const uploadResponse = await request(app) .post('/resource/resume') + .set('Authorization', `Bearer ${testToken}`) .attach('file', testResumePath); const filename = uploadResponse.text; @@ -93,6 +104,7 @@ describe('Resume API Tests', () => { const response = await request(app) .get(`/resume/score/${filename}?jobDescription=${encodeURIComponent(testJobDescription)}`) + .set('Authorization', `Bearer ${testToken}`) .expect(500); expect(response.body).toHaveProperty('message'); @@ -110,6 +122,7 @@ describe('Resume API Tests', () => { it('should upload a resume successfully', async () => { const response = await request(app) .post('/resource/resume') + .set('Authorization', `Bearer ${testToken}`) .attach('file', testResumePath) .expect(201); @@ -126,6 +139,7 @@ describe('Resume API Tests', () => { it('should validate file type', async () => { const response = await request(app) .post('/resource/resume') + .set('Authorization', `Bearer ${testToken}`) .attach('file', Buffer.from('invalid content'), { filename: 'test.png' }) .expect(400); @@ -135,6 +149,7 @@ describe('Resume API Tests', () => { it('should handle missing file', async () => { const response = await request(app) .post('/resource/resume') + .set('Authorization', `Bearer ${testToken}`) .expect(400); expect(response.text).toBe('No file uploaded.'); @@ -146,6 +161,7 @@ describe('Resume API Tests', () => { // First upload a resume const uploadResponse = await request(app) .post('/resource/resume') + .set('Authorization', `Bearer ${testToken}`) .attach('file', testResumePath); const filename = uploadResponse.text; @@ -153,6 +169,7 @@ describe('Resume API Tests', () => { // Then try to download it const response = await request(app) .get(`/resource/resume/${filename}`) + .set('Authorization', `Bearer ${testToken}`) .expect(200); expect(response.body).toBeDefined(); @@ -167,6 +184,7 @@ describe('Resume API Tests', () => { it('should return 404 for non-existent resume', async () => { const response = await request(app) .get('/resource/resume/nonexistent.pdf') + .set('Authorization', `Bearer ${testToken}`) .expect(404); expect(response.text).toBe('Resume not found'); From 46d64b993babc7f3ead258cf139de036ffe08b67 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sun, 13 Apr 2025 21:02:54 +0300 Subject: [PATCH 29/29] Fix frontend Build, And Rename ResumePage To Resume Signed-off-by: Tal Jacob --- nextstep-frontend/src/App.tsx | 4 ++-- nextstep-frontend/src/pages/NewPost.tsx | 1 - .../src/pages/{ResumePage.tsx => Resume.tsx} | 9 +++------ 3 files changed, 5 insertions(+), 9 deletions(-) rename nextstep-frontend/src/pages/{ResumePage.tsx => Resume.tsx} (96%) diff --git a/nextstep-frontend/src/App.tsx b/nextstep-frontend/src/App.tsx index 351d6e6..58f988e 100644 --- a/nextstep-frontend/src/App.tsx +++ b/nextstep-frontend/src/App.tsx @@ -10,7 +10,7 @@ import RequireAuth from './hoc/RequireAuth'; import NewPost from './pages/NewPost'; import PostDetails from './pages/PostDetails'; import Chat from './pages/Chat'; -import ResumePage from './pages/ResumePage'; +import Resume from './pages/Resume'; import TopBar from './components/TopBar'; import Layout from './components/Layout'; @@ -27,7 +27,7 @@ const App: React.FC = () => { } /> } /> } /> - } /> + } /> } /> diff --git a/nextstep-frontend/src/pages/NewPost.tsx b/nextstep-frontend/src/pages/NewPost.tsx index c855470..1e50e14 100644 --- a/nextstep-frontend/src/pages/NewPost.tsx +++ b/nextstep-frontend/src/pages/NewPost.tsx @@ -6,7 +6,6 @@ import FroalaEditor from 'react-froala-wysiwyg'; import 'froala-editor/css/froala_style.min.css'; import 'froala-editor/css/froala_editor.pkgd.min.css'; import 'froala-editor/js/plugins/image.min.js'; -import TopBar from '../components/TopBar'; import api from "../serverApi.ts"; import { getUserAuth } from '../handlers/userAuth.ts'; diff --git a/nextstep-frontend/src/pages/ResumePage.tsx b/nextstep-frontend/src/pages/Resume.tsx similarity index 96% rename from nextstep-frontend/src/pages/ResumePage.tsx rename to nextstep-frontend/src/pages/Resume.tsx index 9d44daf..c51db44 100644 --- a/nextstep-frontend/src/pages/ResumePage.tsx +++ b/nextstep-frontend/src/pages/Resume.tsx @@ -1,10 +1,8 @@ import React, { useState, useRef, useEffect } from 'react'; import { Box, Button, CircularProgress, Typography, TextField } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { useNavigate } from 'react-router-dom'; import { config } from '../config'; import api from '../serverApi'; -import TopBar from '../components/TopBar'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -19,7 +17,7 @@ const UploadBox = styled(Box)(({ theme }) => ({ }, })); -const ScoreGauge = styled(Box)<{ score: number }>(({ theme, score }) => ({ +const ScoreGauge = styled(Box)<{ score: number }>(({ theme }) => ({ width: '200px', height: '200px', borderRadius: '50%', @@ -82,7 +80,7 @@ const FeedbackContainer = styled(Box)(({ theme }) => ({ }, })); -const ResumePage: React.FC = () => { +const Resume: React.FC = () => { const [file, setFile] = useState(null); const [jobDescription, setJobDescription] = useState(''); const [feedback, setFeedback] = useState(''); @@ -91,7 +89,6 @@ const ResumePage: React.FC = () => { const [error, setError] = useState(''); const fileInputRef = useRef(null); const feedbackEndRef = useRef(null); - const navigate = useNavigate(); // Auto-scroll to bottom when feedback updates useEffect(() => { @@ -256,4 +253,4 @@ const ResumePage: React.FC = () => { ); }; -export default ResumePage; \ No newline at end of file +export default Resume; \ No newline at end of file