From a2a55e4d043ee29e463b7d914089642a2fdf4912 Mon Sep 17 00:00:00 2001 From: Shashank070105 Date: Mon, 25 Aug 2025 15:03:34 +0530 Subject: [PATCH 001/198] Final Commit befor Deployement --- backend/controllers/forumController.js | 376 ------------- backend/controllers/mentorshipController.js | 398 -------------- backend/controllers/peerReviewController.js | 302 ----------- backend/controllers/studyGroupController.js | 429 --------------- backend/models/Forum.js | 30 -- backend/models/Mentorship.js | 25 - backend/models/PeerReview.js | 16 - backend/models/StudyGroup.js | 31 -- backend/package-lock.json | 7 + backend/package.json | 1 + backend/routes/forumRoutes.js | 44 -- backend/routes/mentorshipRoutes.js | 36 -- backend/routes/peerReviewRoutes.js | 33 -- backend/routes/studyGroupRoutes.js | 42 -- backend/server.js | 11 - frontend/interview-perp-ai/src/App.jsx | 34 -- .../Collaborative/CollaborativeNav.jsx | 34 -- .../src/pages/Collaborative/Forum.jsx | 303 ----------- .../src/pages/Collaborative/ForumDetail.jsx | 376 ------------- .../src/pages/Collaborative/Mentorship.jsx | 383 ------------- .../src/pages/Collaborative/PeerReview.jsx | 414 -------------- .../pages/Collaborative/StudyGroupDetail.jsx | 507 ------------------ .../src/pages/Collaborative/StudyGroups.jsx | 299 ----------- .../src/pages/Home/Dashboard.jsx | 5 - .../interview-perp-ai/src/utils/apiPaths.js | 53 -- 25 files changed, 8 insertions(+), 4181 deletions(-) delete mode 100644 backend/controllers/forumController.js delete mode 100644 backend/controllers/mentorshipController.js delete mode 100644 backend/controllers/peerReviewController.js delete mode 100644 backend/controllers/studyGroupController.js delete mode 100644 backend/models/Forum.js delete mode 100644 backend/models/Mentorship.js delete mode 100644 backend/models/PeerReview.js delete mode 100644 backend/models/StudyGroup.js delete mode 100644 backend/routes/forumRoutes.js delete mode 100644 backend/routes/mentorshipRoutes.js delete mode 100644 backend/routes/peerReviewRoutes.js delete mode 100644 backend/routes/studyGroupRoutes.js delete mode 100644 frontend/interview-perp-ai/src/components/Collaborative/CollaborativeNav.jsx delete mode 100644 frontend/interview-perp-ai/src/pages/Collaborative/Forum.jsx delete mode 100644 frontend/interview-perp-ai/src/pages/Collaborative/ForumDetail.jsx delete mode 100644 frontend/interview-perp-ai/src/pages/Collaborative/Mentorship.jsx delete mode 100644 frontend/interview-perp-ai/src/pages/Collaborative/PeerReview.jsx delete mode 100644 frontend/interview-perp-ai/src/pages/Collaborative/StudyGroupDetail.jsx delete mode 100644 frontend/interview-perp-ai/src/pages/Collaborative/StudyGroups.jsx diff --git a/backend/controllers/forumController.js b/backend/controllers/forumController.js deleted file mode 100644 index 770599f..0000000 --- a/backend/controllers/forumController.js +++ /dev/null @@ -1,376 +0,0 @@ -const { Forum, Post } = require('../models/Forum'); -const User = require('../models/User'); - -// Create a new forum -exports.createForum = async (req, res) => { - try { - const { title, description, category, tags } = req.body; - const userId = req.user.id; - - const newForum = new Forum({ - title, - description, - category, - tags: tags || [], - creator: userId, - isActive: true - }); - - await newForum.save(); - res.status(201).json(newForum); - } catch (error) { - console.error('Error creating forum:', error); - res.status(500).json({ message: 'Failed to create forum' }); - } -}; - -// Get all forums (with filtering options) -exports.getAllForums = async (req, res) => { - try { - const { category, tag, search, sort } = req.query; - const query = {}; - - // Apply filters if provided - if (category) query.category = category; - if (tag) query.tags = { $in: [tag] }; - if (search) query.title = { $regex: search, $options: 'i' }; - - // Default to active forums only - query.isActive = true; - - // Determine sort order - let sortOption = { createdAt: -1 }; // Default: newest first - if (sort === 'popular') sortOption = { viewCount: -1 }; - if (sort === 'active') sortOption = { lastActivity: -1 }; - - const forums = await Forum.find(query) - .populate('creator', 'name email profileImageUrl') - .sort(sortOption); - - res.status(200).json(forums); - } catch (error) { - console.error('Error fetching forums:', error); - res.status(500).json({ message: 'Failed to fetch forums' }); - } -}; - -// Get a specific forum by ID with its posts -exports.getForumById = async (req, res) => { - try { - const forumId = req.params.id; - - const forum = await Forum.findById(forumId) - .populate('creator', 'name email profileImageUrl'); - - if (!forum) { - return res.status(404).json({ message: 'Forum not found' }); - } - - // Increment view count - forum.viewCount += 1; - await forum.save(); - - // Get the posts for this forum - const posts = await Post.find({ - forum: forumId, - parentPost: { $exists: false } // Only get top-level posts, not comments - }) - .populate('author', 'name email profileImageUrl') - .sort({ createdAt: -1 }); - - res.status(200).json({ - forum, - posts - }); - } catch (error) { - console.error('Error fetching forum:', error); - res.status(500).json({ message: 'Failed to fetch forum' }); - } -}; - -// Create a new post in a forum -exports.createPost = async (req, res) => { - try { - const { content, attachments } = req.body; - const forumId = req.params.id; - const userId = req.user.id; - - // Check if the forum exists - const forum = await Forum.findById(forumId); - if (!forum) { - return res.status(404).json({ message: 'Forum not found' }); - } - - // Check if the forum is active - if (!forum.isActive) { - return res.status(400).json({ message: 'This forum is no longer active' }); - } - - // Create the post - const newPost = new Post({ - content, - author: userId, - forum: forumId, - attachments: attachments || [] - }); - - await newPost.save(); - - // Update the forum's lastActivity and add the post to its posts array - forum.lastActivity = new Date(); - forum.posts.push(newPost._id); - await forum.save(); - - // Populate author details before sending response - await newPost.populate('author', 'name email profileImageUrl'); - - res.status(201).json(newPost); - } catch (error) { - console.error('Error creating post:', error); - res.status(500).json({ message: 'Failed to create post' }); - } -}; - -// Get a specific post with its comments -exports.getPostWithComments = async (req, res) => { - try { - const postId = req.params.postId; - - const post = await Post.findById(postId) - .populate('author', 'name email profileImageUrl'); - - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - - // Get comments for this post - const comments = await Post.find({ parentPost: postId }) - .populate('author', 'name email profileImageUrl') - .sort({ createdAt: 1 }); - - res.status(200).json({ - post, - comments - }); - } catch (error) { - console.error('Error fetching post with comments:', error); - res.status(500).json({ message: 'Failed to fetch post with comments' }); - } -}; - -// Add a comment to a post -exports.addComment = async (req, res) => { - try { - const { content, attachments } = req.body; - const postId = req.params.postId; - const userId = req.user.id; - - // Check if the parent post exists - const parentPost = await Post.findById(postId); - if (!parentPost) { - return res.status(404).json({ message: 'Parent post not found' }); - } - - // Create the comment (which is also a Post with a parentPost reference) - const newComment = new Post({ - content, - author: userId, - forum: parentPost.forum, // Same forum as parent post - parentPost: postId, - attachments: attachments || [] - }); - - await newComment.save(); - - // Update the forum's lastActivity - await Forum.findByIdAndUpdate(parentPost.forum, { - lastActivity: new Date() - }); - - // Populate author details before sending response - await newComment.populate('author', 'name email profileImageUrl'); - - res.status(201).json(newComment); - } catch (error) { - console.error('Error adding comment:', error); - res.status(500).json({ message: 'Failed to add comment' }); - } -}; - -// Upvote a post -exports.upvotePost = async (req, res) => { - try { - const postId = req.params.postId; - const userId = req.user.id; - - const post = await Post.findById(postId); - - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - - // Check if user has already upvoted - if (post.upvotes.includes(userId)) { - // Remove upvote (toggle) - post.upvotes = post.upvotes.filter(id => id.toString() !== userId); - } else { - // Add upvote - post.upvotes.push(userId); - } - - await post.save(); - res.status(200).json({ upvotes: post.upvotes.length }); - } catch (error) { - console.error('Error upvoting post:', error); - res.status(500).json({ message: 'Failed to upvote post' }); - } -}; - -// Update a post (author only) -exports.updatePost = async (req, res) => { - try { - const { content, attachments } = req.body; - const postId = req.params.postId; - const userId = req.user.id; - - const post = await Post.findById(postId); - - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - - // Check if the user is the author - if (post.author.toString() !== userId) { - return res.status(403).json({ message: 'Only the author can update this post' }); - } - - // Update the post - if (content !== undefined) post.content = content; - if (attachments !== undefined) post.attachments = attachments; - post.isEdited = true; - post.lastEditedAt = new Date(); - - await post.save(); - - // Populate author details before sending response - await post.populate('author', 'name email profileImageUrl'); - - res.status(200).json(post); - } catch (error) { - console.error('Error updating post:', error); - res.status(500).json({ message: 'Failed to update post' }); - } -}; - -// Delete a post (author only) -exports.deletePost = async (req, res) => { - try { - const postId = req.params.postId; - const userId = req.user.id; - - const post = await Post.findById(postId); - - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - - // Check if the user is the author - if (post.author.toString() !== userId) { - return res.status(403).json({ message: 'Only the author can delete this post' }); - } - - // If this is a top-level post, also delete all comments - if (!post.parentPost) { - await Post.deleteMany({ parentPost: postId }); - - // Remove the post from the forum's posts array - await Forum.findByIdAndUpdate(post.forum, { - $pull: { posts: postId } - }); - } - - await Post.findByIdAndDelete(postId); - res.status(200).json({ message: 'Post deleted successfully' }); - } catch (error) { - console.error('Error deleting post:', error); - res.status(500).json({ message: 'Failed to delete post' }); - } -}; - -// Update a forum (creator only) -exports.updateForum = async (req, res) => { - try { - const { title, description, category, tags, isActive } = req.body; - const forumId = req.params.id; - const userId = req.user.id; - - const forum = await Forum.findById(forumId); - - if (!forum) { - return res.status(404).json({ message: 'Forum not found' }); - } - - // Check if the user is the creator - if (forum.creator.toString() !== userId) { - return res.status(403).json({ message: 'Only the creator can update this forum' }); - } - - // Update the forum - if (title !== undefined) forum.title = title; - if (description !== undefined) forum.description = description; - if (category !== undefined) forum.category = category; - if (tags !== undefined) forum.tags = tags; - if (isActive !== undefined) forum.isActive = isActive; - - await forum.save(); - res.status(200).json(forum); - } catch (error) { - console.error('Error updating forum:', error); - res.status(500).json({ message: 'Failed to update forum' }); - } -}; - -// Delete a forum (creator only) -exports.deleteForum = async (req, res) => { - try { - const forumId = req.params.id; - const userId = req.user.id; - - const forum = await Forum.findById(forumId); - - if (!forum) { - return res.status(404).json({ message: 'Forum not found' }); - } - - // Check if the user is the creator - if (forum.creator.toString() !== userId) { - return res.status(403).json({ message: 'Only the creator can delete this forum' }); - } - - // Delete all posts in the forum - await Post.deleteMany({ forum: forumId }); - - // Delete the forum - await Forum.findByIdAndDelete(forumId); - res.status(200).json({ message: 'Forum deleted successfully' }); - } catch (error) { - console.error('Error deleting forum:', error); - res.status(500).json({ message: 'Failed to delete forum' }); - } -}; - -// Get user's posts -exports.getUserPosts = async (req, res) => { - try { - const userId = req.user.id; - - const posts = await Post.find({ author: userId }) - .populate('forum', 'title') - .sort({ createdAt: -1 }); - - res.status(200).json(posts); - } catch (error) { - console.error('Error fetching user posts:', error); - res.status(500).json({ message: 'Failed to fetch user posts' }); - } -}; \ No newline at end of file diff --git a/backend/controllers/mentorshipController.js b/backend/controllers/mentorshipController.js deleted file mode 100644 index 92e0d21..0000000 --- a/backend/controllers/mentorshipController.js +++ /dev/null @@ -1,398 +0,0 @@ -const Mentorship = require('../models/Mentorship'); -const User = require('../models/User'); - -// Request a mentorship -exports.requestMentorship = async (req, res) => { - try { - const { mentorId, topics, goals } = req.body; - const menteeId = req.user.id; - - // Validate that the mentor exists - const mentor = await User.findById(mentorId); - if (!mentor) { - return res.status(404).json({ message: 'Mentor not found' }); - } - - // Check if there's already an active mentorship between these users - const existingMentorship = await Mentorship.findOne({ - mentor: mentorId, - mentee: menteeId, - status: { $in: ['pending', 'active'] } - }); - - if (existingMentorship) { - return res.status(400).json({ - message: 'You already have a pending or active mentorship with this mentor' - }); - } - - // Create the mentorship request - const newMentorship = new Mentorship({ - mentor: mentorId, - mentee: menteeId, - status: 'pending', - topics: topics || [], - goals: goals || [] - }); - - await newMentorship.save(); - res.status(201).json(newMentorship); - } catch (error) { - console.error('Error requesting mentorship:', error); - res.status(500).json({ message: 'Failed to request mentorship' }); - } -}; - -// Accept or reject a mentorship request -exports.respondToMentorshipRequest = async (req, res) => { - try { - const { action } = req.body; - const mentorshipId = req.params.id; - const userId = req.user.id; - - const mentorship = await Mentorship.findById(mentorshipId); - - if (!mentorship) { - return res.status(404).json({ message: 'Mentorship request not found' }); - } - - // Check if the user is the mentor - if (mentorship.mentor.toString() !== userId) { - return res.status(403).json({ message: 'Only the mentor can respond to this request' }); - } - - // Check if the request is still pending - if (mentorship.status !== 'pending') { - return res.status(400).json({ message: 'This request has already been processed' }); - } - - if (action === 'accept') { - mentorship.status = 'active'; - mentorship.startDate = new Date(); - // Set default end date to 3 months from now - const endDate = new Date(); - endDate.setMonth(endDate.getMonth() + 3); - mentorship.endDate = endDate; - } else if (action === 'reject') { - mentorship.status = 'rejected'; - } else { - return res.status(400).json({ message: 'Invalid action. Use "accept" or "reject"' }); - } - - await mentorship.save(); - res.status(200).json({ - message: `Mentorship request ${action}ed successfully`, - mentorship - }); - } catch (error) { - console.error('Error responding to mentorship request:', error); - res.status(500).json({ message: 'Failed to respond to mentorship request' }); - } -}; - -// Get all mentorships for a user (as either mentor or mentee) -exports.getUserMentorships = async (req, res) => { - try { - const userId = req.user.id; - const { role, status } = req.query; - - const query = {}; - - // Filter by role if specified - if (role === 'mentor') { - query.mentor = userId; - } else if (role === 'mentee') { - query.mentee = userId; - } else { - // If no role specified, get all mentorships where user is either mentor or mentee - query.$or = [{ mentor: userId }, { mentee: userId }]; - } - - // Filter by status if specified - if (status) { - query.status = status; - } - - const mentorships = await Mentorship.find(query) - .populate('mentor', 'name email profileImageUrl') - .populate('mentee', 'name email profileImageUrl') - .sort({ createdAt: -1 }); - - res.status(200).json(mentorships); - } catch (error) { - console.error('Error fetching user mentorships:', error); - res.status(500).json({ message: 'Failed to fetch user mentorships' }); - } -}; - -// Get a specific mentorship by ID -exports.getMentorshipById = async (req, res) => { - try { - const mentorshipId = req.params.id; - const userId = req.user.id; - - const mentorship = await Mentorship.findById(mentorshipId) - .populate('mentor', 'name email profileImageUrl') - .populate('mentee', 'name email profileImageUrl'); - - if (!mentorship) { - return res.status(404).json({ message: 'Mentorship not found' }); - } - - // Check if the user is either the mentor or the mentee - if ( - mentorship.mentor._id.toString() !== userId && - mentorship.mentee._id.toString() !== userId - ) { - return res.status(403).json({ message: 'You do not have permission to view this mentorship' }); - } - - res.status(200).json(mentorship); - } catch (error) { - console.error('Error fetching mentorship:', error); - res.status(500).json({ message: 'Failed to fetch mentorship' }); - } -}; - -// Add a note to a mentorship -exports.addMentorshipNote = async (req, res) => { - try { - const { content } = req.body; - const mentorshipId = req.params.id; - const userId = req.user.id; - - const mentorship = await Mentorship.findById(mentorshipId); - - if (!mentorship) { - return res.status(404).json({ message: 'Mentorship not found' }); - } - - // Check if the user is either the mentor or the mentee - if ( - mentorship.mentor.toString() !== userId && - mentorship.mentee.toString() !== userId - ) { - return res.status(403).json({ message: 'You do not have permission to add notes to this mentorship' }); - } - - // Add the note - const newNote = { - content, - author: userId, - createdAt: new Date() - }; - - mentorship.notes.push(newNote); - await mentorship.save(); - - res.status(201).json(newNote); - } catch (error) { - console.error('Error adding mentorship note:', error); - res.status(500).json({ message: 'Failed to add mentorship note' }); - } -}; - -// Schedule a meeting for a mentorship -exports.scheduleMeeting = async (req, res) => { - try { - const { title, date, duration, location, description } = req.body; - const mentorshipId = req.params.id; - const userId = req.user.id; - - const mentorship = await Mentorship.findById(mentorshipId); - - if (!mentorship) { - return res.status(404).json({ message: 'Mentorship not found' }); - } - - // Check if the user is either the mentor or the mentee - if ( - mentorship.mentor.toString() !== userId && - mentorship.mentee.toString() !== userId - ) { - return res.status(403).json({ - message: 'You do not have permission to schedule meetings for this mentorship' - }); - } - - // Add the meeting - const newMeeting = { - title, - date, - duration, - location, - description, - scheduledBy: userId, - status: 'scheduled' - }; - - mentorship.meetings.push(newMeeting); - await mentorship.save(); - - res.status(201).json(newMeeting); - } catch (error) { - console.error('Error scheduling meeting:', error); - res.status(500).json({ message: 'Failed to schedule meeting' }); - } -}; - -// Update meeting status (confirm, cancel, complete) -exports.updateMeetingStatus = async (req, res) => { - try { - const { meetingId, status } = req.body; - const mentorshipId = req.params.id; - const userId = req.user.id; - - const mentorship = await Mentorship.findById(mentorshipId); - - if (!mentorship) { - return res.status(404).json({ message: 'Mentorship not found' }); - } - - // Check if the user is either the mentor or the mentee - if ( - mentorship.mentor.toString() !== userId && - mentorship.mentee.toString() !== userId - ) { - return res.status(403).json({ - message: 'You do not have permission to update meetings for this mentorship' - }); - } - - // Find the meeting - const meetingIndex = mentorship.meetings.findIndex( - meeting => meeting._id.toString() === meetingId - ); - - if (meetingIndex === -1) { - return res.status(404).json({ message: 'Meeting not found' }); - } - - // Update the meeting status - if (['confirmed', 'cancelled', 'completed'].includes(status)) { - mentorship.meetings[meetingIndex].status = status; - if (status === 'completed') { - mentorship.meetings[meetingIndex].completedAt = new Date(); - } - } else { - return res.status(400).json({ - message: 'Invalid status. Use "confirmed", "cancelled", or "completed"' - }); - } - - await mentorship.save(); - res.status(200).json(mentorship.meetings[meetingIndex]); - } catch (error) { - console.error('Error updating meeting status:', error); - res.status(500).json({ message: 'Failed to update meeting status' }); - } -}; - -// Update mentorship progress -exports.updateProgress = async (req, res) => { - try { - const { progressUpdate } = req.body; - const mentorshipId = req.params.id; - const userId = req.user.id; - - const mentorship = await Mentorship.findById(mentorshipId); - - if (!mentorship) { - return res.status(404).json({ message: 'Mentorship not found' }); - } - - // Check if the user is either the mentor or the mentee - if ( - mentorship.mentor.toString() !== userId && - mentorship.mentee.toString() !== userId - ) { - return res.status(403).json({ - message: 'You do not have permission to update progress for this mentorship' - }); - } - - // Add the progress update - const newProgress = { - update: progressUpdate, - updatedBy: userId, - date: new Date() - }; - - mentorship.progress.push(newProgress); - await mentorship.save(); - - res.status(201).json(newProgress); - } catch (error) { - console.error('Error updating mentorship progress:', error); - res.status(500).json({ message: 'Failed to update mentorship progress' }); - } -}; - -// End a mentorship (can be done by either mentor or mentee) -exports.endMentorship = async (req, res) => { - try { - const { feedback } = req.body; - const mentorshipId = req.params.id; - const userId = req.user.id; - - const mentorship = await Mentorship.findById(mentorshipId); - - if (!mentorship) { - return res.status(404).json({ message: 'Mentorship not found' }); - } - - // Check if the user is either the mentor or the mentee - if ( - mentorship.mentor.toString() !== userId && - mentorship.mentee.toString() !== userId - ) { - return res.status(403).json({ message: 'You do not have permission to end this mentorship' }); - } - - // Check if the mentorship is active - if (mentorship.status !== 'active') { - return res.status(400).json({ message: 'This mentorship is not currently active' }); - } - - // End the mentorship - mentorship.status = 'completed'; - mentorship.endDate = new Date(); - - // Add feedback if provided - if (feedback) { - mentorship.endFeedback = { - content: feedback, - providedBy: userId, - date: new Date() - }; - } - - await mentorship.save(); - res.status(200).json({ message: 'Mentorship ended successfully', mentorship }); - } catch (error) { - console.error('Error ending mentorship:', error); - res.status(500).json({ message: 'Failed to end mentorship' }); - } -}; - -// Get available mentors -exports.getAvailableMentors = async (req, res) => { - try { - // In a real application, you would have a way to identify users who are available as mentors - // For now, we'll just return all users except the current user - const userId = req.user.id; - const { topic } = req.query; - - // This is a placeholder implementation - // In a real app, you would have a field in the User model to indicate mentor status - // and possibly a separate MentorProfile model with additional details - const mentors = await User.find({ _id: { $ne: userId } }) - .select('name email profileImageUrl') - .limit(20); - - res.status(200).json(mentors); - } catch (error) { - console.error('Error fetching available mentors:', error); - res.status(500).json({ message: 'Failed to fetch available mentors' }); - } -}; \ No newline at end of file diff --git a/backend/controllers/peerReviewController.js b/backend/controllers/peerReviewController.js deleted file mode 100644 index 77fffa7..0000000 --- a/backend/controllers/peerReviewController.js +++ /dev/null @@ -1,302 +0,0 @@ -const PeerReview = require('../models/PeerReview'); -const Session = require('../models/Session'); -const User = require('../models/User'); -const Question = require('../models/Question'); - -// Create a new peer review -exports.createPeerReview = async (req, res) => { - try { - const { - intervieweeId, - sessionId, - questionId, - feedback, - rating, - strengths, - improvements, - isAnonymous - } = req.body; - const reviewerId = req.user.id; - - // Validate that the session and question exist - const session = await Session.findById(sessionId); - if (!session) { - return res.status(404).json({ message: 'Session not found' }); - } - - const question = await Question.findById(questionId); - if (!question) { - return res.status(404).json({ message: 'Question not found' }); - } - - // Validate that the interviewee exists - const interviewee = await User.findById(intervieweeId); - if (!interviewee) { - return res.status(404).json({ message: 'Interviewee not found' }); - } - - // Create the peer review - const newPeerReview = new PeerReview({ - reviewer: reviewerId, - interviewee: intervieweeId, - session: sessionId, - question: questionId, - feedback, - rating, - strengths: strengths || [], - improvements: improvements || [], - isAnonymous: isAnonymous !== undefined ? isAnonymous : false, - status: 'submitted' - }); - - await newPeerReview.save(); - res.status(201).json(newPeerReview); - } catch (error) { - console.error('Error creating peer review:', error); - res.status(500).json({ message: 'Failed to create peer review' }); - } -}; - -// Get all peer reviews for a specific user (as interviewee) -exports.getUserPeerReviews = async (req, res) => { - try { - const userId = req.user.id; - - const peerReviews = await PeerReview.find({ interviewee: userId }) - .populate({ - path: 'reviewer', - select: 'name email profileImageUrl', - // Don't populate reviewer details if the review is anonymous - match: { isAnonymous: false } - }) - .populate('session', 'role experience topicsToFocus') - .populate('question', 'question answer') - .sort({ createdAt: -1 }); - - // For anonymous reviews, remove reviewer details - const formattedReviews = peerReviews.map(review => { - const reviewObj = review.toObject(); - if (reviewObj.isAnonymous) { - reviewObj.reviewer = { name: 'Anonymous Reviewer' }; - } - return reviewObj; - }); - - res.status(200).json(formattedReviews); - } catch (error) { - console.error('Error fetching user peer reviews:', error); - res.status(500).json({ message: 'Failed to fetch user peer reviews' }); - } -}; - -// Get all peer reviews given by a user (as reviewer) -exports.getReviewsGivenByUser = async (req, res) => { - try { - const userId = req.user.id; - - const peerReviews = await PeerReview.find({ reviewer: userId }) - .populate('interviewee', 'name email profileImageUrl') - .populate('session', 'role experience topicsToFocus') - .populate('question', 'question answer') - .sort({ createdAt: -1 }); - - res.status(200).json(peerReviews); - } catch (error) { - console.error('Error fetching reviews given by user:', error); - res.status(500).json({ message: 'Failed to fetch reviews given by user' }); - } -}; - -// Get a specific peer review by ID -exports.getPeerReviewById = async (req, res) => { - try { - const peerReview = await PeerReview.findById(req.params.id) - .populate('reviewer', 'name email profileImageUrl') - .populate('interviewee', 'name email profileImageUrl') - .populate('session', 'role experience topicsToFocus') - .populate('question', 'question answer'); - - if (!peerReview) { - return res.status(404).json({ message: 'Peer review not found' }); - } - - // Check if the user is either the reviewer or the interviewee - const userId = req.user.id; - if ( - peerReview.reviewer._id.toString() !== userId && - peerReview.interviewee._id.toString() !== userId - ) { - return res.status(403).json({ message: 'You do not have permission to view this review' }); - } - - // If the review is anonymous and the requester is the interviewee, hide reviewer details - const reviewObj = peerReview.toObject(); - if (reviewObj.isAnonymous && reviewObj.interviewee._id.toString() === userId) { - reviewObj.reviewer = { name: 'Anonymous Reviewer' }; - } - - res.status(200).json(reviewObj); - } catch (error) { - console.error('Error fetching peer review:', error); - res.status(500).json({ message: 'Failed to fetch peer review' }); - } -}; - -// Update a peer review (reviewer only) -exports.updatePeerReview = async (req, res) => { - try { - const { feedback, rating, strengths, improvements, isAnonymous } = req.body; - const reviewId = req.params.id; - const userId = req.user.id; - - const peerReview = await PeerReview.findById(reviewId); - - if (!peerReview) { - return res.status(404).json({ message: 'Peer review not found' }); - } - - // Check if the user is the reviewer - if (peerReview.reviewer.toString() !== userId) { - return res.status(403).json({ message: 'Only the reviewer can update this review' }); - } - - // Update the review - if (feedback !== undefined) peerReview.feedback = feedback; - if (rating !== undefined) peerReview.rating = rating; - if (strengths !== undefined) peerReview.strengths = strengths; - if (improvements !== undefined) peerReview.improvements = improvements; - if (isAnonymous !== undefined) peerReview.isAnonymous = isAnonymous; - - await peerReview.save(); - res.status(200).json(peerReview); - } catch (error) { - console.error('Error updating peer review:', error); - res.status(500).json({ message: 'Failed to update peer review' }); - } -}; - -// Delete a peer review (reviewer only) -exports.deletePeerReview = async (req, res) => { - try { - const reviewId = req.params.id; - const userId = req.user.id; - - const peerReview = await PeerReview.findById(reviewId); - - if (!peerReview) { - return res.status(404).json({ message: 'Peer review not found' }); - } - - // Check if the user is the reviewer - if (peerReview.reviewer.toString() !== userId) { - return res.status(403).json({ message: 'Only the reviewer can delete this review' }); - } - - await PeerReview.findByIdAndDelete(reviewId); - res.status(200).json({ message: 'Peer review deleted successfully' }); - } catch (error) { - console.error('Error deleting peer review:', error); - res.status(500).json({ message: 'Failed to delete peer review' }); - } -}; - -// Request a peer review for a specific question -exports.requestPeerReview = async (req, res) => { - try { - const { questionId, message } = req.body; - const userId = req.user.id; - - // Validate that the question exists and belongs to the user - const question = await Question.findById(questionId); - if (!question) { - return res.status(404).json({ message: 'Question not found' }); - } - - // Get the session to verify ownership - const session = await Session.findById(question.session); - if (!session || session.user.toString() !== userId) { - return res.status(403).json({ - message: 'You do not have permission to request a review for this question' - }); - } - - // Create a peer review request (status: 'requested') - const peerReviewRequest = new PeerReview({ - interviewee: userId, - session: session._id, - question: questionId, - status: 'requested', - requestMessage: message || 'Please review my interview answer' - }); - - await peerReviewRequest.save(); - res.status(201).json({ - message: 'Peer review request created successfully', - request: peerReviewRequest - }); - } catch (error) { - console.error('Error requesting peer review:', error); - res.status(500).json({ message: 'Failed to request peer review' }); - } -}; - -// Get all open peer review requests (that need reviewers) -exports.getOpenPeerReviewRequests = async (req, res) => { - try { - const userId = req.user.id; - - // Find all peer review requests that don't have a reviewer assigned - // and don't belong to the current user - const openRequests = await PeerReview.find({ - reviewer: { $exists: false }, - interviewee: { $ne: userId }, - status: 'requested' - }) - .populate('interviewee', 'name email profileImageUrl') - .populate('session', 'role experience topicsToFocus') - .populate('question', 'question') - .sort({ createdAt: -1 }); - - res.status(200).json(openRequests); - } catch (error) { - console.error('Error fetching open peer review requests:', error); - res.status(500).json({ message: 'Failed to fetch open peer review requests' }); - } -}; - -// Accept a peer review request -exports.acceptPeerReviewRequest = async (req, res) => { - try { - const requestId = req.params.id; - const userId = req.user.id; - - const peerReviewRequest = await PeerReview.findById(requestId); - - if (!peerReviewRequest) { - return res.status(404).json({ message: 'Peer review request not found' }); - } - - // Check if the request is still open - if (peerReviewRequest.status !== 'requested') { - return res.status(400).json({ message: 'This request has already been accepted or completed' }); - } - - // Check if the user is not the interviewee - if (peerReviewRequest.interviewee.toString() === userId) { - return res.status(400).json({ message: 'You cannot review your own interview answer' }); - } - - // Assign the reviewer and update status - peerReviewRequest.reviewer = userId; - peerReviewRequest.status = 'in_progress'; - await peerReviewRequest.save(); - - res.status(200).json({ - message: 'Peer review request accepted successfully', - request: peerReviewRequest - }); - } catch (error) { - console.error('Error accepting peer review request:', error); - res.status(500).json({ message: 'Failed to accept peer review request' }); - } -}; \ No newline at end of file diff --git a/backend/controllers/studyGroupController.js b/backend/controllers/studyGroupController.js deleted file mode 100644 index 16d1408..0000000 --- a/backend/controllers/studyGroupController.js +++ /dev/null @@ -1,429 +0,0 @@ -const StudyGroup = require('../models/StudyGroup'); -const User = require('../models/User'); - -// Create a new study group -exports.createStudyGroup = async (req, res) => { - try { - const { name, description, topics, isPublic, maxMembers } = req.body; - const userId = req.user.id; - - const newStudyGroup = new StudyGroup({ - name, - description, - creator: userId, - members: [userId], // Creator is automatically a member - topics: topics || [], - isPublic: isPublic !== undefined ? isPublic : true, - maxMembers: maxMembers || 10 - }); - - await newStudyGroup.save(); - res.status(201).json(newStudyGroup); - } catch (error) { - console.error('Error creating study group:', error); - res.status(500).json({ message: 'Failed to create study group' }); - } -}; - -// Get all study groups (with filtering options) -exports.getAllStudyGroups = async (req, res) => { - try { - const { topic, isPublic, search } = req.query; - const query = {}; - - // Apply filters if provided - if (topic) query.topics = { $in: [topic] }; - if (isPublic !== undefined) query.isPublic = isPublic === 'true'; - if (search) query.name = { $regex: search, $options: 'i' }; - - const studyGroups = await StudyGroup.find(query) - .populate('creator', 'name email profileImageUrl') - .populate('members', 'name email profileImageUrl') - .sort({ createdAt: -1 }); - - res.status(200).json(studyGroups); - } catch (error) { - console.error('Error fetching study groups:', error); - res.status(500).json({ message: 'Failed to fetch study groups' }); - } -}; - -// Get a specific study group by ID -exports.getStudyGroupById = async (req, res) => { - try { - const studyGroup = await StudyGroup.findById(req.params.id) - .populate('creator', 'name email profileImageUrl') - .populate('members', 'name email profileImageUrl') - .populate('joinRequests.user', 'name email profileImageUrl'); - - if (!studyGroup) { - return res.status(404).json({ message: 'Study group not found' }); - } - - res.status(200).json(studyGroup); - } catch (error) { - console.error('Error fetching study group:', error); - res.status(500).json({ message: 'Failed to fetch study group' }); - } -}; - -// Join a study group -exports.joinStudyGroup = async (req, res) => { - try { - const studyGroup = await StudyGroup.findById(req.params.id); - const userId = req.user.id; - - if (!studyGroup) { - return res.status(404).json({ message: 'Study group not found' }); - } - - // Check if user is already a member - if (studyGroup.members.includes(userId)) { - return res.status(400).json({ message: 'You are already a member of this group' }); - } - - // Check if group is full - if (studyGroup.members.length >= studyGroup.maxMembers) { - return res.status(400).json({ message: 'This group has reached its maximum capacity' }); - } - - // If group is public, add user directly - if (studyGroup.isPublic) { - studyGroup.members.push(userId); - await studyGroup.save(); - return res.status(200).json({ message: 'Successfully joined the study group' }); - } else { - // If group is private, create a join request - // Check if there's already a pending request - const existingRequest = studyGroup.joinRequests.find( - request => request.user.toString() === userId && request.status === 'pending' - ); - - if (existingRequest) { - return res.status(400).json({ message: 'You already have a pending request to join this group' }); - } - - studyGroup.joinRequests.push({ - user: userId, - status: 'pending', - requestDate: new Date() - }); - - await studyGroup.save(); - return res.status(200).json({ message: 'Join request sent successfully' }); - } - } catch (error) { - console.error('Error joining study group:', error); - res.status(500).json({ message: 'Failed to join study group' }); - } -}; - -// Handle join requests (accept/reject) -exports.handleJoinRequest = async (req, res) => { - try { - const { requestId, action } = req.body; - const groupId = req.params.id; - const userId = req.user.id; - - const studyGroup = await StudyGroup.findById(groupId); - - if (!studyGroup) { - return res.status(404).json({ message: 'Study group not found' }); - } - - // Check if user is the creator of the group - if (studyGroup.creator.toString() !== userId) { - return res.status(403).json({ message: 'Only the group creator can handle join requests' }); - } - - // Find the request - const requestIndex = studyGroup.joinRequests.findIndex( - request => request._id.toString() === requestId - ); - - if (requestIndex === -1) { - return res.status(404).json({ message: 'Join request not found' }); - } - - const request = studyGroup.joinRequests[requestIndex]; - - if (action === 'accept') { - // Add user to members - studyGroup.members.push(request.user); - request.status = 'accepted'; - } else if (action === 'reject') { - request.status = 'rejected'; - } else { - return res.status(400).json({ message: 'Invalid action. Use "accept" or "reject"' }); - } - - await studyGroup.save(); - res.status(200).json({ message: `Join request ${action}ed successfully` }); - } catch (error) { - console.error('Error handling join request:', error); - res.status(500).json({ message: 'Failed to handle join request' }); - } -}; - -// Leave a study group -exports.leaveStudyGroup = async (req, res) => { - try { - const studyGroup = await StudyGroup.findById(req.params.id); - const userId = req.user.id; - - if (!studyGroup) { - return res.status(404).json({ message: 'Study group not found' }); - } - - // Check if user is a member - if (!studyGroup.members.includes(userId)) { - return res.status(400).json({ message: 'You are not a member of this group' }); - } - - // Check if user is the creator - if (studyGroup.creator.toString() === userId) { - return res.status(400).json({ message: 'As the creator, you cannot leave the group. You can delete it instead.' }); - } - - // Remove user from members - studyGroup.members = studyGroup.members.filter(member => member.toString() !== userId); - await studyGroup.save(); - - res.status(200).json({ message: 'Successfully left the study group' }); - } catch (error) { - console.error('Error leaving study group:', error); - res.status(500).json({ message: 'Failed to leave study group' }); - } -}; - -// Add a resource to a study group -exports.addResource = async (req, res) => { - try { - const { title, description, url } = req.body; - const groupId = req.params.id; - const userId = req.user.id; - - const studyGroup = await StudyGroup.findById(groupId); - - if (!studyGroup) { - return res.status(404).json({ message: 'Study group not found' }); - } - - // Check if user is a member - if (!studyGroup.members.includes(userId)) { - return res.status(403).json({ message: 'Only members can add resources to the group' }); - } - - const newResource = { - title, - description, - url, - addedBy: userId, - addedAt: new Date() - }; - - studyGroup.resources.push(newResource); - await studyGroup.save(); - - res.status(201).json(newResource); - } catch (error) { - console.error('Error adding resource:', error); - res.status(500).json({ message: 'Failed to add resource' }); - } -}; - -// Delete a study group (creator only) -exports.deleteStudyGroup = async (req, res) => { - try { - const studyGroup = await StudyGroup.findById(req.params.id); - const userId = req.user.id; - - if (!studyGroup) { - return res.status(404).json({ message: 'Study group not found' }); - } - - // Check if user is the creator - if (studyGroup.creator.toString() !== userId) { - return res.status(403).json({ message: 'Only the creator can delete the group' }); - } - - await StudyGroup.findByIdAndDelete(req.params.id); - res.status(200).json({ message: 'Study group deleted successfully' }); - } catch (error) { - console.error('Error deleting study group:', error); - res.status(500).json({ message: 'Failed to delete study group' }); - } -}; - -// Get study groups for a specific user -exports.getUserStudyGroups = async (req, res) => { - try { - const userId = req.user.id; - - const studyGroups = await StudyGroup.find({ members: userId }) - .populate('creator', 'name email profileImageUrl') - .populate('members', 'name email profileImageUrl') - .sort({ createdAt: -1 }); - - res.status(200).json(studyGroups); - } catch (error) { - console.error('Error fetching user study groups:', error); - res.status(500).json({ message: 'Failed to fetch user study groups' }); - } -}; - -// Invite a user to a study group -exports.inviteToStudyGroup = async (req, res) => { - try { - const { userId } = req.body; - const groupId = req.params.id; - const inviterId = req.user.id; - - // Validate input - if (!userId) { - return res.status(400).json({ message: 'User ID is required' }); - } - - const studyGroup = await StudyGroup.findById(groupId); - - if (!studyGroup) { - return res.status(404).json({ message: 'Study group not found' }); - } - - // Check if inviter is a member of the group - if (!studyGroup.members.includes(inviterId)) { - return res.status(403).json({ message: 'Only members can invite others to the group' }); - } - - // Check if user is already a member - if (studyGroup.members.includes(userId)) { - return res.status(400).json({ message: 'User is already a member of this group' }); - } - - // Check if group is full - if (studyGroup.members.length >= studyGroup.maxMembers) { - return res.status(400).json({ message: 'This group has reached its maximum capacity' }); - } - - // Check if there's already a pending invitation - const existingInvitation = studyGroup.invitations.find( - invitation => invitation.user.toString() === userId && invitation.status === 'pending' - ); - - if (existingInvitation) { - return res.status(400).json({ message: 'User already has a pending invitation to this group' }); - } - - // Add invitation - studyGroup.invitations.push({ - user: userId, - invitedBy: inviterId, - status: 'pending', - invitationDate: new Date() - }); - - await studyGroup.save(); - res.status(200).json({ message: 'Invitation sent successfully' }); - } catch (error) { - console.error('Error inviting to study group:', error); - res.status(500).json({ message: 'Failed to send invitation' }); - } -}; - -// Invite a user to a study group by email -exports.inviteByEmail = async (req, res) => { - try { - const { email } = req.body; - const groupId = req.params.id; - const inviterId = req.user.id; - - // Validate input - if (!email) { - return res.status(400).json({ message: 'Email is required' }); - } - - const studyGroup = await StudyGroup.findById(groupId); - - if (!studyGroup) { - return res.status(404).json({ message: 'Study group not found' }); - } - - // Check if inviter is a member of the group - if (!studyGroup.members.includes(inviterId)) { - return res.status(403).json({ message: 'Only members can invite others to the group' }); - } - - // Find user by email - const user = await User.findOne({ email }); - - // If user exists, send them an invitation - if (user) { - // Check if user is already a member - if (studyGroup.members.includes(user._id)) { - return res.status(400).json({ message: 'User is already a member of this group' }); - } - - // Check if group is full - if (studyGroup.members.length >= studyGroup.maxMembers) { - return res.status(400).json({ message: 'This group has reached its maximum capacity' }); - } - - // Check if there's already a pending invitation - const existingInvitation = studyGroup.invitations.find( - invitation => invitation.user.toString() === user._id.toString() && invitation.status === 'pending' - ); - - if (existingInvitation) { - return res.status(400).json({ message: 'User already has a pending invitation to this group' }); - } - - // Add invitation - studyGroup.invitations.push({ - user: user._id, - invitedBy: inviterId, - status: 'pending', - invitationDate: new Date() - }); - - await studyGroup.save(); - return res.status(200).json({ message: 'Invitation sent successfully' }); - } - - // If user doesn't exist, send them an email invitation (in a real app) - // For now, just return a success message - res.status(200).json({ message: 'Invitation email sent successfully' }); - } catch (error) { - console.error('Error inviting by email:', error); - res.status(500).json({ message: 'Failed to send invitation' }); - } -}; - -// Search for users to invite -exports.searchUsers = async (req, res) => { - try { - const { query } = req.query; - const userId = req.user.id; - - if (!query || query.length < 2) { - return res.status(400).json({ message: 'Search query must be at least 2 characters' }); - } - - // Search for users by name or email, excluding the current user - const users = await User.find({ - $and: [ - { _id: { $ne: userId } }, - { - $or: [ - { name: { $regex: query, $options: 'i' } }, - { email: { $regex: query, $options: 'i' } } - ] - } - ] - }).select('name email profileImageUrl'); - - res.status(200).json(users); - } catch (error) { - console.error('Error searching users:', error); - res.status(500).json({ message: 'Failed to search users' }); - } -}; \ No newline at end of file diff --git a/backend/models/Forum.js b/backend/models/Forum.js deleted file mode 100644 index 190a02b..0000000 --- a/backend/models/Forum.js +++ /dev/null @@ -1,30 +0,0 @@ -const mongoose = require("mongoose"); - -const postSchema = new mongoose.Schema({ - content: { type: String, required: true }, - author: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - upvotes: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], - parentPost: { type: mongoose.Schema.Types.ObjectId, ref: "Post" }, // For comments - attachments: [{ - type: { type: String, enum: ["image", "document", "link"] }, - url: { type: String }, - name: { type: String } - }] -}, { timestamps: true }); - -const forumSchema = new mongoose.Schema({ - title: { type: String, required: true }, - description: { type: String, required: true }, - category: { type: String, enum: ["company", "topic", "general"], required: true }, - tags: [{ type: String }], - createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - posts: [{ type: mongoose.Schema.Types.ObjectId, ref: "Post" }], - isActive: { type: Boolean, default: true }, - viewCount: { type: Number, default: 0 }, - lastActivity: { type: Date, default: Date.now } -}, { timestamps: true }); - -const Post = mongoose.model("Post", postSchema); -const Forum = mongoose.model("Forum", forumSchema); - -module.exports = { Forum, Post }; \ No newline at end of file diff --git a/backend/models/Mentorship.js b/backend/models/Mentorship.js deleted file mode 100644 index 7ac72cc..0000000 --- a/backend/models/Mentorship.js +++ /dev/null @@ -1,25 +0,0 @@ -const mongoose = require("mongoose"); - -const mentorshipSchema = new mongoose.Schema({ - mentor: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - mentee: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - status: { type: String, enum: ["pending", "active", "completed", "declined"], default: "pending" }, - startDate: { type: Date }, - endDate: { type: Date }, - topics: [{ type: String }], - goals: [{ type: String }], - notes: { type: String }, - meetings: [{ - date: { type: Date }, - duration: { type: Number }, // in minutes - notes: { type: String }, - completed: { type: Boolean, default: false } - }], - progress: [{ - date: { type: Date, default: Date.now }, - note: { type: String }, - addedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" } - }] -}, { timestamps: true }); - -module.exports = mongoose.model("Mentorship", mentorshipSchema); \ No newline at end of file diff --git a/backend/models/PeerReview.js b/backend/models/PeerReview.js deleted file mode 100644 index fd8cf6b..0000000 --- a/backend/models/PeerReview.js +++ /dev/null @@ -1,16 +0,0 @@ -const mongoose = require("mongoose"); - -const peerReviewSchema = new mongoose.Schema({ - reviewer: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - interviewee: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - session: { type: mongoose.Schema.Types.ObjectId, ref: "InterviewSession", required: true }, - question: { type: mongoose.Schema.Types.ObjectId, ref: "Question" }, - feedback: { type: String, required: true }, - rating: { type: Number, min: 1, max: 5, required: true }, - strengths: [{ type: String }], - improvements: [{ type: String }], - isAnonymous: { type: Boolean, default: false }, - status: { type: String, enum: ["pending", "submitted", "accepted"], default: "pending" } -}, { timestamps: true }); - -module.exports = mongoose.model("PeerReview", peerReviewSchema); \ No newline at end of file diff --git a/backend/models/StudyGroup.js b/backend/models/StudyGroup.js deleted file mode 100644 index e8a9c05..0000000 --- a/backend/models/StudyGroup.js +++ /dev/null @@ -1,31 +0,0 @@ -const mongoose = require("mongoose"); - -const studyGroupSchema = new mongoose.Schema({ - name: { type: String, required: true }, - description: { type: String, required: true }, - creator: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - members: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], - topics: [{ type: String }], - isPublic: { type: Boolean, default: true }, - maxMembers: { type: Number, default: 10 }, - joinRequests: [{ - user: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, - status: { type: String, enum: ["pending", "accepted", "rejected"], default: "pending" }, - requestDate: { type: Date, default: Date.now } - }], - invitations: [{ - user: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, - invitedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, - status: { type: String, enum: ["pending", "accepted", "rejected"], default: "pending" }, - invitationDate: { type: Date, default: Date.now } - }], - resources: [{ - title: { type: String }, - description: { type: String }, - url: { type: String }, - addedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, - addedAt: { type: Date, default: Date.now } - }] -}, { timestamps: true }); - -module.exports = mongoose.model("StudyGroup", studyGroupSchema); \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 2090137..9c15de6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^5.1.0", + "express-async-handler": "^1.2.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.17.0", "multer": "^2.0.2" @@ -553,6 +554,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-async-handler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", + "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index d368d24..422d868 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^5.1.0", + "express-async-handler": "^1.2.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.17.0", "multer": "^2.0.2" diff --git a/backend/routes/forumRoutes.js b/backend/routes/forumRoutes.js deleted file mode 100644 index 8f8937d..0000000 --- a/backend/routes/forumRoutes.js +++ /dev/null @@ -1,44 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const forumController = require('../controllers/forumController'); -const { protect } = require('../middlewares/authMiddleware'); - -// Forum routes -// Create a new forum -router.post('/', protect, forumController.createForum); - -// Get all forums (with filtering options) -router.get('/', protect, forumController.getAllForums); - -// Get a specific forum by ID with its posts -router.get('/:id', protect, forumController.getForumById); - -// Create a new post in a forum -router.post('/:id/posts', protect, forumController.createPost); - -// Update a forum (creator only) -router.put('/:id', protect, forumController.updateForum); - -// Delete a forum (creator only) -router.delete('/:id', protect, forumController.deleteForum); - -// Post routes -// Get a specific post with its comments -router.get('/posts/:postId', protect, forumController.getPostWithComments); - -// Add a comment to a post -router.post('/posts/:postId/comments', protect, forumController.addComment); - -// Upvote a post -router.post('/posts/:postId/upvote', protect, forumController.upvotePost); - -// Update a post (author only) -router.put('/posts/:postId', protect, forumController.updatePost); - -// Delete a post (author only) -router.delete('/posts/:postId', protect, forumController.deletePost); - -// Get user's posts -router.get('/user/posts', protect, forumController.getUserPosts); - -module.exports = router; \ No newline at end of file diff --git a/backend/routes/mentorshipRoutes.js b/backend/routes/mentorshipRoutes.js deleted file mode 100644 index 3cb90ec..0000000 --- a/backend/routes/mentorshipRoutes.js +++ /dev/null @@ -1,36 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const mentorshipController = require('../controllers/mentorshipController'); -const { protect } = require('../middlewares/authMiddleware'); - -// Request a mentorship -router.post('/request', protect, mentorshipController.requestMentorship); - -// Accept or reject a mentorship request -router.post('/:id/respond', protect, mentorshipController.respondToMentorshipRequest); - -// Get all mentorships for a user (as either mentor or mentee) -router.get('/', protect, mentorshipController.getUserMentorships); - -// Get a specific mentorship by ID -router.get('/:id', protect, mentorshipController.getMentorshipById); - -// Add a note to a mentorship -router.post('/:id/notes', protect, mentorshipController.addMentorshipNote); - -// Schedule a meeting for a mentorship -router.post('/:id/meetings', protect, mentorshipController.scheduleMeeting); - -// Update meeting status (confirm, cancel, complete) -router.put('/:id/meetings', protect, mentorshipController.updateMeetingStatus); - -// Update mentorship progress -router.post('/:id/progress', protect, mentorshipController.updateProgress); - -// End a mentorship (can be done by either mentor or mentee) -router.post('/:id/end', protect, mentorshipController.endMentorship); - -// Get available mentors -router.get('/mentors/available', protect, mentorshipController.getAvailableMentors); - -module.exports = router; \ No newline at end of file diff --git a/backend/routes/peerReviewRoutes.js b/backend/routes/peerReviewRoutes.js deleted file mode 100644 index 3e47f69..0000000 --- a/backend/routes/peerReviewRoutes.js +++ /dev/null @@ -1,33 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const peerReviewController = require('../controllers/peerReviewController'); -const { protect } = require('../middlewares/authMiddleware'); - -// Create a new peer review -router.post('/', protect, peerReviewController.createPeerReview); - -// Get all peer reviews for a specific user (as interviewee) -router.get('/received', protect, peerReviewController.getUserPeerReviews); - -// Get all peer reviews given by a user (as reviewer) -router.get('/given', protect, peerReviewController.getReviewsGivenByUser); - -// Get a specific peer review by ID -router.get('/:id', protect, peerReviewController.getPeerReviewById); - -// Update a peer review (reviewer only) -router.put('/:id', protect, peerReviewController.updatePeerReview); - -// Delete a peer review (reviewer only) -router.delete('/:id', protect, peerReviewController.deletePeerReview); - -// Request a peer review for a specific question -router.post('/request', protect, peerReviewController.requestPeerReview); - -// Get all open peer review requests (that need reviewers) -router.get('/requests/open', protect, peerReviewController.getOpenPeerReviewRequests); - -// Accept a peer review request -router.post('/requests/:id/accept', protect, peerReviewController.acceptPeerReviewRequest); - -module.exports = router; \ No newline at end of file diff --git a/backend/routes/studyGroupRoutes.js b/backend/routes/studyGroupRoutes.js deleted file mode 100644 index 13a624a..0000000 --- a/backend/routes/studyGroupRoutes.js +++ /dev/null @@ -1,42 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const studyGroupController = require('../controllers/studyGroupController'); -const { protect } = require('../middlewares/authMiddleware'); - -// Create a new study group -router.post('/', protect, studyGroupController.createStudyGroup); - -// Get all study groups (with filtering options) -router.get('/', protect, studyGroupController.getAllStudyGroups); - -// Get study groups for a specific user -router.get('/user/groups', protect, studyGroupController.getUserStudyGroups); - -// Search for users to invite -router.get('/search-users', protect, studyGroupController.searchUsers); - -// Get a specific study group by ID -router.get('/:id', protect, studyGroupController.getStudyGroupById); - -// Join a study group -router.post('/:id/join', protect, studyGroupController.joinStudyGroup); - -// Handle join requests (accept/reject) -router.post('/:id/handle-request', protect, studyGroupController.handleJoinRequest); - -// Leave a study group -router.post('/:id/leave', protect, studyGroupController.leaveStudyGroup); - -// Add a resource to a study group -router.post('/:id/resources', protect, studyGroupController.addResource); - -// Invite a user to a study group -router.post('/:id/invite', protect, studyGroupController.inviteToStudyGroup); - -// Invite a user to a study group by email -router.post('/:id/invite-by-email', protect, studyGroupController.inviteByEmail); - -// Delete a study group (creator only) -router.delete('/:id', protect, studyGroupController.deleteStudyGroup); - -module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index d270e88..c06556f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -45,17 +45,6 @@ app.use("/api/ai/generate-questions", protect, generateInterviewQuestions); app.use('/api/feedback', feedbackRoutes); app.use("/api/ai", aiRoutes); -// Collaborative feature routes -const studyGroupRoutes = require('./routes/studyGroupRoutes'); -const peerReviewRoutes = require('./routes/peerReviewRoutes'); -const mentorshipRoutes = require('./routes/mentorshipRoutes'); -const forumRoutes = require('./routes/forumRoutes'); - -app.use('/api/study-groups', studyGroupRoutes); -app.use('/api/peer-reviews', peerReviewRoutes); -app.use('/api/mentorships', mentorshipRoutes); -app.use('/api/forums', forumRoutes); - // Serve uploads folder app.use("/uploads", express.static(path.join(__dirname, "uploads"), {})); diff --git a/frontend/interview-perp-ai/src/App.jsx b/frontend/interview-perp-ai/src/App.jsx index 93206a1..9447cba 100644 --- a/frontend/interview-perp-ai/src/App.jsx +++ b/frontend/interview-perp-ai/src/App.jsx @@ -18,14 +18,6 @@ import Login from './pages/Auth/Login'; import AnalyticsDashboard from './pages/Analytics/AnalyticsDashboard'; import PracticePage from './pages/PracticePage'; -// Import collaborative feature components -import StudyGroups from './pages/Collaborative/StudyGroups'; -import StudyGroupDetail from './pages/Collaborative/StudyGroupDetail'; -import PeerReview from './pages/Collaborative/PeerReview'; -import Mentorship from './pages/Collaborative/Mentorship'; -import Forum from './pages/Collaborative/Forum'; -import ForumDetail from './pages/Collaborative/ForumDetail'; - // ✅ ADD THIS COMPONENT DEFINITION // This component checks for a token and protects routes. @@ -76,32 +68,6 @@ const App = () => { path="/review" element={} /> - - {/* Collaborative Features Routes */} - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> { - const location = useLocation(); - const path = location.pathname; - - const navItems = [ - { name: 'Study Groups', path: '/study-groups', icon: 'users' }, - { name: 'Peer Reviews', path: '/peer-reviews', icon: 'clipboard-check' }, - { name: 'Mentorships', path: '/mentorships', icon: 'user-graduate' }, - { name: 'Forums', path: '/forums', icon: 'comments' }, - ]; - - return ( -
-

Collaborative Learning

-
- {navItems.map((item) => ( - - - {item.name} - - ))} -
-
- ); -}; - -export default CollaborativeNav; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/pages/Collaborative/Forum.jsx b/frontend/interview-perp-ai/src/pages/Collaborative/Forum.jsx deleted file mode 100644 index 2c0dff6..0000000 --- a/frontend/interview-perp-ai/src/pages/Collaborative/Forum.jsx +++ /dev/null @@ -1,303 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import DashboardLayout from '../../components/layouts/DashboardLayout'; -import axiosInstance from '../../utils/axiosInstance'; -import { API_PATHS } from '../../utils/apiPaths'; -import SpinnerLoader from '../../components/Loader/SpinnerLoader'; -import { toast } from 'react-hot-toast'; - -const Forum = () => { - const navigate = useNavigate(); - const [forums, setForums] = useState([]); - const [loading, setLoading] = useState(true); - const [showCreateModal, setShowCreateModal] = useState(false); - const [formData, setFormData] = useState({ - title: '', - description: '', - category: 'general', - tags: '' - }); - const [searchQuery, setSearchQuery] = useState(''); - const [selectedCategory, setSelectedCategory] = useState('all'); - - const categories = [ - 'general', - 'algorithms', - 'system-design', - 'frontend', - 'backend', - 'database', - 'behavioral', - 'career' - ]; - - useEffect(() => { - fetchForums(); - }, []); - - const fetchForums = async () => { - try { - setLoading(true); - const response = await axiosInstance.get(API_PATHS.FORUMS.GET_ALL); - setForums(response.data); - } catch (error) { - console.error('Error fetching forums:', error); - toast.error('Failed to load forums'); - } finally { - setLoading(false); - } - }; - - const handleInputChange = (e) => { - const { name, value } = e.target; - setFormData({ - ...formData, - [name]: value - }); - }; - - const handleCreateForum = async (e) => { - e.preventDefault(); - try { - setLoading(true); - const tagsArray = formData.tags.split(',').map(tag => tag.trim()); - const payload = { - ...formData, - tags: tagsArray - }; - - const response = await axiosInstance.post(API_PATHS.FORUMS.CREATE, payload); - toast.success('Forum created successfully!'); - setShowCreateModal(false); - fetchForums(); - setFormData({ - title: '', - description: '', - category: 'general', - tags: '' - }); - - // Navigate to the newly created forum - navigate(`/forums/${response.data._id}`); - } catch (error) { - console.error('Error creating forum:', error); - toast.error('Failed to create forum'); - } finally { - setLoading(false); - } - }; - - const handleViewForum = (forumId) => { - navigate(`/forums/${forumId}`); - }; - - const formatDate = (dateString) => { - return new Date(dateString).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); - }; - - const getFilteredForums = () => { - return forums.filter(forum => { - const matchesSearch = searchQuery === '' || - forum.title.toLowerCase().includes(searchQuery.toLowerCase()) || - forum.description.toLowerCase().includes(searchQuery.toLowerCase()) || - forum.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())); - - const matchesCategory = selectedCategory === 'all' || forum.category === selectedCategory; - - return matchesSearch && matchesCategory; - }); - }; - - return ( - -
-
-

Discussion Forums

- -
- -
-
-
- setSearchQuery(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-amber-500" - /> - - - -
-
- -
-
-
- - {loading ? ( -
- -
- ) : ( -
- {getFilteredForums().length === 0 ? ( -

- No forums found. {searchQuery || selectedCategory !== 'all' ? 'Try adjusting your filters.' : 'Create one to get started!'} -

- ) : ( - getFilteredForums().map((forum) => ( -
handleViewForum(forum._id)} - > -
-

{forum.title}

- - {forum.category.charAt(0).toUpperCase() + forum.category.slice(1)} - -
-

{forum.description}

-
- {forum.tags.map((tag, index) => ( - - {tag} - - ))} -
-
-
- {forum.posts.length} posts - - {forum.viewCount} views -
-
- Created by {forum.creator.name} - - Last active {formatDate(forum.lastActivity || forum.createdAt)} -
-
-
- )) - )} -
- )} - - {/* Create Forum Modal */} - {showCreateModal && ( -
-
-

Create New Forum

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- )} -
-
- ); -}; - -export default Forum; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/pages/Collaborative/ForumDetail.jsx b/frontend/interview-perp-ai/src/pages/Collaborative/ForumDetail.jsx deleted file mode 100644 index 1337aef..0000000 --- a/frontend/interview-perp-ai/src/pages/Collaborative/ForumDetail.jsx +++ /dev/null @@ -1,376 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import DashboardLayout from '../../components/layouts/DashboardLayout'; -import axiosInstance from '../../utils/axiosInstance'; -import { API_PATHS } from '../../utils/apiPaths'; -import SpinnerLoader from '../../components/Loader/SpinnerLoader'; -import { toast } from 'react-hot-toast'; - -const ForumDetail = () => { - const { forumId } = useParams(); - const navigate = useNavigate(); - const [forum, setForum] = useState(null); - const [posts, setPosts] = useState([]); - const [loading, setLoading] = useState(true); - const [showCreatePostModal, setShowCreatePostModal] = useState(false); - const [postFormData, setPostFormData] = useState({ - title: '', - content: '' - }); - const [commentFormData, setCommentFormData] = useState({}); - const [showCommentForm, setShowCommentForm] = useState({}); - - useEffect(() => { - if (forumId) { - fetchForumDetails(); - } - }, [forumId]); - - const fetchForumDetails = async () => { - try { - setLoading(true); - const response = await axiosInstance.get(`${API_PATHS.FORUMS.GET_BY_ID}/${forumId}`); - setForum(response.data); - - const postsResponse = await axiosInstance.get(`${API_PATHS.FORUMS.GET_POSTS}/${forumId}`); - setPosts(postsResponse.data); - } catch (error) { - console.error('Error fetching forum details:', error); - toast.error('Failed to load forum details'); - navigate('/forums'); - } finally { - setLoading(false); - } - }; - - const handleInputChange = (e) => { - const { name, value } = e.target; - setPostFormData({ - ...postFormData, - [name]: value - }); - }; - - const handleCommentInputChange = (e, postId) => { - const { value } = e.target; - setCommentFormData({ - ...commentFormData, - [postId]: value - }); - }; - - const handleCreatePost = async (e) => { - e.preventDefault(); - try { - setLoading(true); - const payload = { - ...postFormData, - forumId - }; - - await axiosInstance.post(API_PATHS.FORUMS.CREATE_POST, payload); - toast.success('Post created successfully!'); - setShowCreatePostModal(false); - setPostFormData({ - title: '', - content: '' - }); - fetchForumDetails(); - } catch (error) { - console.error('Error creating post:', error); - toast.error('Failed to create post'); - } finally { - setLoading(false); - } - }; - - const handleAddComment = async (postId) => { - try { - if (!commentFormData[postId] || commentFormData[postId].trim() === '') { - toast.error('Comment cannot be empty'); - return; - } - - const payload = { - content: commentFormData[postId], - postId - }; - - await axiosInstance.post(`${API_PATHS.FORUMS.ADD_COMMENT}/${postId}`, payload); - toast.success('Comment added successfully!'); - setCommentFormData({ - ...commentFormData, - [postId]: '' - }); - setShowCommentForm({ - ...showCommentForm, - [postId]: false - }); - fetchForumDetails(); - } catch (error) { - console.error('Error adding comment:', error); - toast.error('Failed to add comment'); - } - }; - - const handleUpvotePost = async (postId) => { - try { - await axiosInstance.post(`${API_PATHS.FORUMS.UPVOTE_POST}/${postId}`); - fetchForumDetails(); - } catch (error) { - console.error('Error upvoting post:', error); - toast.error('Failed to upvote post'); - } - }; - - const formatDate = (dateString) => { - return new Date(dateString).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }; - - const toggleCommentForm = (postId) => { - setShowCommentForm({ - ...showCommentForm, - [postId]: !showCommentForm[postId] - }); - }; - - return ( - -
- {loading && !forum ? ( -
- -
- ) : forum ? ( - <> -
-
-
- -

{forum.title}

-
- -
-
-
-
-

{forum.description}

-
- {forum.tags.map((tag, index) => ( - - {tag} - - ))} -
-
- - {forum.category.charAt(0).toUpperCase() + forum.category.slice(1)} - -
-
- Created by {forum.creator.name} - - Created on {formatDate(forum.createdAt)} - - {forum.viewCount} views -
-
-
- -
-

Posts

- {posts.length === 0 ? ( -

- No posts yet. Be the first to create a post! -

- ) : ( -
- {posts.map((post) => ( -
-
-
-

{post.title}

-
- -
-
-

{post.content}

-
- Posted by {post.author.name} - - {formatDate(post.createdAt)} -
-
- -
-
-

Comments ({post.comments.length})

- -
- - {showCommentForm[post._id] && ( -
- -
- -
-
- )} - - {post.comments.length > 0 ? ( -
- {post.comments.map((comment) => ( -
-

{comment.content}

-
- {comment.author.name} - - {formatDate(comment.createdAt)} -
-
- ))} -
- ) : ( -

No comments yet.

- )} -
-
- ))} -
- )} -
- - {/* Create Post Modal */} - {showCreatePostModal && ( -
-
-

Create New Post

-
-
- - -
-
- - -
-
- - -
-
-
-
- )} - - ) : ( -
-

Forum not found or has been removed.

- -
- )} -
-
- ); -}; - -export default ForumDetail; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/pages/Collaborative/Mentorship.jsx b/frontend/interview-perp-ai/src/pages/Collaborative/Mentorship.jsx deleted file mode 100644 index a2f5910..0000000 --- a/frontend/interview-perp-ai/src/pages/Collaborative/Mentorship.jsx +++ /dev/null @@ -1,383 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import DashboardLayout from '../../components/layouts/DashboardLayout'; -import axiosInstance from '../../utils/axiosInstance'; -import { API_PATHS } from '../../utils/apiPaths'; -import SpinnerLoader from '../../components/Loader/SpinnerLoader'; -import { toast } from 'react-hot-toast'; - -const Mentorship = () => { - const navigate = useNavigate(); - const [mentorships, setMentorships] = useState([]); - const [availableMentors, setAvailableMentors] = useState([]); - const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState('active'); - const [showRequestModal, setShowRequestModal] = useState(false); - const [requestData, setRequestData] = useState({ - mentorId: '', - topics: '', - goals: '' - }); - - useEffect(() => { - fetchMentorships(); - fetchAvailableMentors(); - }, []); - - const fetchMentorships = async () => { - try { - setLoading(true); - const response = await axiosInstance.get(API_PATHS.MENTORSHIPS.GET_ALL); - setMentorships(response.data); - } catch (error) { - console.error('Error fetching mentorships:', error); - toast.error('Failed to load mentorships'); - } finally { - setLoading(false); - } - }; - - const fetchAvailableMentors = async () => { - try { - const response = await axiosInstance.get(API_PATHS.MENTORSHIPS.GET_AVAILABLE_MENTORS); - setAvailableMentors(response.data); - } catch (error) { - console.error('Error fetching available mentors:', error); - toast.error('Failed to load available mentors'); - } - }; - - const handleInputChange = (e) => { - const { name, value } = e.target; - setRequestData({ - ...requestData, - [name]: value - }); - }; - - const handleRequestMentorship = async (e) => { - e.preventDefault(); - try { - setLoading(true); - const topicsArray = requestData.topics.split(',').map(topic => topic.trim()); - const payload = { - ...requestData, - topics: topicsArray - }; - - await axiosInstance.post(API_PATHS.MENTORSHIPS.REQUEST, payload); - toast.success('Mentorship request sent successfully!'); - setShowRequestModal(false); - fetchMentorships(); - setRequestData({ - mentorId: '', - topics: '', - goals: '' - }); - } catch (error) { - console.error('Error requesting mentorship:', error); - toast.error('Failed to request mentorship'); - } finally { - setLoading(false); - } - }; - - const handleRespondToRequest = async (mentorshipId, status) => { - try { - await axiosInstance.post(API_PATHS.MENTORSHIPS.RESPOND(mentorshipId), { status }); - toast.success(`Request ${status === 'accepted' ? 'accepted' : 'rejected'} successfully!`); - fetchMentorships(); - } catch (error) { - console.error('Error responding to request:', error); - toast.error('Failed to respond to request'); - } - }; - - const handleEndMentorship = async (mentorshipId) => { - try { - await axiosInstance.post(API_PATHS.MENTORSHIPS.END(mentorshipId)); - toast.success('Mentorship ended successfully!'); - fetchMentorships(); - } catch (error) { - console.error('Error ending mentorship:', error); - toast.error('Failed to end mentorship'); - } - }; - - const handleViewMentorship = (mentorshipId) => { - navigate(`/mentorships/${mentorshipId}`); - }; - - const formatDate = (dateString) => { - return new Date(dateString).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); - }; - - const getFilteredMentorships = () => { - switch (activeTab) { - case 'active': - return mentorships.filter(m => m.status === 'active'); - case 'pending': - return mentorships.filter(m => m.status === 'pending'); - case 'requests': - return mentorships.filter(m => m.status === 'pending' && m.mentor._id === localStorage.getItem('userId')); - case 'completed': - return mentorships.filter(m => m.status === 'completed'); - default: - return []; - } - }; - - const renderTabContent = () => { - if (loading) { - return ( -
- -
- ); - } - - const filteredMentorships = getFilteredMentorships(); - - if (filteredMentorships.length === 0) { - return ( -

- {activeTab === 'active' && 'You have no active mentorships.'} - {activeTab === 'pending' && 'You have no pending mentorship requests.'} - {activeTab === 'requests' && 'You have no mentorship requests to respond to.'} - {activeTab === 'completed' && 'You have no completed mentorships.'} -

- ); - } - - return ( -
- {filteredMentorships.map((mentorship) => ( -
-
-
-

- {mentorship.mentee._id === localStorage.getItem('userId') - ? `Mentored by ${mentorship.mentor.name}` - : `Mentoring ${mentorship.mentee.name}`} -

-

- {mentorship.status === 'active' && `Started on ${formatDate(mentorship.startDate)}`} - {mentorship.status === 'pending' && `Requested on ${formatDate(mentorship.createdAt)}`} - {mentorship.status === 'completed' && `Completed on ${formatDate(mentorship.endDate)}`} -

-
- - {mentorship.status.charAt(0).toUpperCase() + mentorship.status.slice(1)} - -
- -
-

Topics:

-
- {mentorship.topics.map((topic, index) => ( - - {topic} - - ))} -
-
- - {mentorship.goals && ( -
-

Goals:

-

{mentorship.goals}

-
- )} - -
- {mentorship.status === 'pending' && mentorship.mentor._id === localStorage.getItem('userId') && ( -
- - -
- )} - - {mentorship.status === 'active' && ( -
- - -
- )} - - {mentorship.status === 'completed' && ( - - )} -
-
- ))} -
- ); - }; - - return ( - -
-
-

Mentorship

- -
- -
-
- -
-
- - {renderTabContent()} - - {/* Request Mentorship Modal */} - {showRequestModal && ( -
-
-

Request Mentorship

- {availableMentors.length === 0 ? ( -
-

No mentors are currently available.

- -
- ) : ( -
-
- - -
-
- - -
-
- - -
-
- - -
-
- )} -
-
- )} -
-
- ); -}; - -export default Mentorship; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/pages/Collaborative/PeerReview.jsx b/frontend/interview-perp-ai/src/pages/Collaborative/PeerReview.jsx deleted file mode 100644 index f0f63a4..0000000 --- a/frontend/interview-perp-ai/src/pages/Collaborative/PeerReview.jsx +++ /dev/null @@ -1,414 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import DashboardLayout from '../../components/layouts/DashboardLayout'; -import axiosInstance from '../../utils/axiosInstance'; -import { API_PATHS } from '../../utils/apiPaths'; -import SpinnerLoader from '../../components/Loader/SpinnerLoader'; -import { toast } from 'react-hot-toast'; - -const PeerReview = () => { - const navigate = useNavigate(); - const [receivedReviews, setReceivedReviews] = useState([]); - const [givenReviews, setGivenReviews] = useState([]); - const [openRequests, setOpenRequests] = useState([]); - const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState('received'); - const [showRequestModal, setShowRequestModal] = useState(false); - const [requestData, setRequestData] = useState({ - sessionId: '', - questionId: '', - note: '' - }); - const [sessions, setSessions] = useState([]); - const [questions, setQuestions] = useState([]); - - useEffect(() => { - fetchData(); - fetchSessions(); - }, []); - - const fetchData = async () => { - setLoading(true); - try { - const [receivedRes, givenRes, requestsRes] = await Promise.all([ - axiosInstance.get(API_PATHS.PEER_REVIEWS.GET_RECEIVED), - axiosInstance.get(API_PATHS.PEER_REVIEWS.GET_GIVEN), - axiosInstance.get(API_PATHS.PEER_REVIEWS.GET_OPEN_REQUESTS) - ]); - - setReceivedReviews(receivedRes.data); - setGivenReviews(givenRes.data); - setOpenRequests(requestsRes.data); - } catch (error) { - console.error('Error fetching peer reviews:', error); - toast.error('Failed to load peer reviews'); - } finally { - setLoading(false); - } - }; - - const fetchSessions = async () => { - try { - const response = await axiosInstance.get(API_PATHS.SESSIONS.GET_MY_SESSIONS); - setSessions(response.data); - } catch (error) { - console.error('Error fetching sessions:', error); - toast.error('Failed to load sessions'); - } - }; - - const fetchQuestions = async (sessionId) => { - try { - const response = await axiosInstance.get(API_PATHS.SESSIONS.GET_ONE(sessionId)); - setQuestions(response.data.questions || []); - } catch (error) { - console.error('Error fetching questions:', error); - toast.error('Failed to load questions'); - } - }; - - const handleInputChange = (e) => { - const { name, value } = e.target; - setRequestData({ - ...requestData, - [name]: value - }); - - // If session changed, fetch questions for that session - if (name === 'sessionId' && value) { - fetchQuestions(value); - } - }; - - const handleRequestReview = async (e) => { - e.preventDefault(); - try { - setLoading(true); - await axiosInstance.post(API_PATHS.PEER_REVIEWS.REQUEST, requestData); - toast.success('Peer review request sent successfully!'); - setShowRequestModal(false); - fetchData(); - setRequestData({ - sessionId: '', - questionId: '', - note: '' - }); - } catch (error) { - console.error('Error requesting peer review:', error); - toast.error('Failed to request peer review'); - } finally { - setLoading(false); - } - }; - - const handleAcceptRequest = async (requestId) => { - try { - await axiosInstance.post(API_PATHS.PEER_REVIEWS.ACCEPT_REQUEST(requestId)); - toast.success('Request accepted!'); - fetchData(); - } catch (error) { - console.error('Error accepting request:', error); - toast.error('Failed to accept request'); - } - }; - - const handleViewReview = (reviewId) => { - navigate(`/peer-reviews/${reviewId}`); - }; - - const formatDate = (dateString) => { - return new Date(dateString).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); - }; - - const renderTabContent = () => { - if (loading) { - return ( -
- -
- ); - } - - switch (activeTab) { - case 'received': - return ( -
- {receivedReviews.length === 0 ? ( -

You haven't received any peer reviews yet.

- ) : ( -
- {receivedReviews.map((review) => ( -
-
-
-

{review.question?.question || 'Question not available'}

-

Reviewed by: {review.isAnonymous ? 'Anonymous' : review.reviewer?.name}

-
- {formatDate(review.createdAt)} -
-
-
- Rating: -
- {[...Array(5)].map((_, i) => ( - - - - ))} -
-
-
-
-

Feedback:

-

{review.feedback}

-
-
-
-

Strengths:

-

{review.strengths}

-
-
-

Areas for Improvement:

-

{review.improvements}

-
-
- -
- ))} -
- )} -
- ); - - case 'given': - return ( -
- {givenReviews.length === 0 ? ( -

You haven't given any peer reviews yet.

- ) : ( -
- {givenReviews.map((review) => ( -
-
-
-

{review.question?.question || 'Question not available'}

-

Reviewed for: {review.interviewee?.name}

-
- {formatDate(review.createdAt)} -
-
-
- Your Rating: -
- {[...Array(5)].map((_, i) => ( - - - - ))} -
-
-
- -
- ))} -
- )} -
- ); - - case 'requests': - return ( -
- {openRequests.length === 0 ? ( -

There are no open peer review requests.

- ) : ( -
- {openRequests.map((request) => ( -
-
-
-

{request.question?.question || 'Question not available'}

-

Requested by: {request.interviewee?.name}

-
- {formatDate(request.createdAt)} -
- {request.note && ( -
-

Note from requester:

-

{request.note}

-
- )} - -
- ))} -
- )} -
- ); - - default: - return null; - } - }; - - return ( - -
-
-

Peer Reviews

- -
- -
-
- -
-
- - {renderTabContent()} - - {/* Request Review Modal */} - {showRequestModal && ( -
-
-

Request Peer Review

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- )} -
-
- ); -}; - -export default PeerReview; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/pages/Collaborative/StudyGroupDetail.jsx b/frontend/interview-perp-ai/src/pages/Collaborative/StudyGroupDetail.jsx deleted file mode 100644 index 1094e28..0000000 --- a/frontend/interview-perp-ai/src/pages/Collaborative/StudyGroupDetail.jsx +++ /dev/null @@ -1,507 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import DashboardLayout from '../../components/layouts/DashboardLayout'; -import axiosInstance from '../../utils/axiosInstance'; -import { API_PATHS } from '../../utils/apiPaths'; -import SpinnerLoader from '../../components/Loader/SpinnerLoader'; -import { toast } from 'react-hot-toast'; - -const StudyGroupDetail = () => { - const { groupId } = useParams(); - const navigate = useNavigate(); - const [studyGroup, setStudyGroup] = useState(null); - const [loading, setLoading] = useState(true); - const [showAddResourceModal, setShowAddResourceModal] = useState(false); - const [showInviteFriendModal, setShowInviteFriendModal] = useState(false); - const [resourceForm, setResourceForm] = useState({ - title: '', - description: '', - url: '' - }); - const [inviteForm, setInviteForm] = useState({ - email: '' - }); - const [users, setUsers] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); - - useEffect(() => { - fetchStudyGroupDetails(); - }, [groupId]); - - const fetchStudyGroupDetails = async () => { - try { - setLoading(true); - const response = await axiosInstance.get(API_PATHS.STUDY_GROUPS.GET_ONE(groupId)); - setStudyGroup(response.data); - } catch (error) { - console.error('Error fetching study group details:', error); - toast.error('Failed to load study group details'); - navigate('/study-groups'); - } finally { - setLoading(false); - } - }; - - const handleInputChange = (e) => { - const { name, value } = e.target; - setResourceForm({ - ...resourceForm, - [name]: value - }); - }; - - const handleAddResource = async (e) => { - e.preventDefault(); - try { - setLoading(true); - await axiosInstance.post(API_PATHS.STUDY_GROUPS.ADD_RESOURCE(groupId), resourceForm); - toast.success('Resource added successfully!'); - setShowAddResourceModal(false); - fetchStudyGroupDetails(); - setResourceForm({ - title: '', - description: '', - url: '' - }); - } catch (error) { - console.error('Error adding resource:', error); - toast.error('Failed to add resource'); - } finally { - setLoading(false); - } - }; - - const handleLeaveGroup = async () => { - if (window.confirm('Are you sure you want to leave this study group?')) { - try { - await axiosInstance.post(API_PATHS.STUDY_GROUPS.LEAVE(groupId)); - toast.success('You have left the study group'); - navigate('/study-groups'); - } catch (error) { - console.error('Error leaving group:', error); - toast.error('Failed to leave group'); - } - } - }; - - const handleDeleteGroup = async () => { - if (window.confirm('Are you sure you want to delete this study group? This action cannot be undone.')) { - try { - await axiosInstance.delete(API_PATHS.STUDY_GROUPS.DELETE(groupId)); - toast.success('Study group deleted successfully'); - navigate('/study-groups'); - } catch (error) { - console.error('Error deleting group:', error); - toast.error('Failed to delete group'); - } - } - }; - - const handleJoinRequest = async (requestId, action) => { - try { - await axiosInstance.post(API_PATHS.STUDY_GROUPS.HANDLE_REQUEST(groupId), { - requestId, - action - }); - toast.success(`Request ${action === 'accept' ? 'accepted' : 'rejected'}`); - fetchStudyGroupDetails(); - } catch (error) { - console.error('Error handling join request:', error); - toast.error('Failed to process request'); - } - }; - - const handleInviteInputChange = (e) => { - const { name, value } = e.target; - setInviteForm({ - ...inviteForm, - [name]: value - }); - }; - - const handleSearchChange = (e) => { - setSearchTerm(e.target.value); - }; - - const searchUsers = async () => { - if (searchTerm.trim().length < 2) return; - - try { - setLoading(true); - const response = await axiosInstance.get(`${API_PATHS.STUDY_GROUPS.SEARCH_USERS}?query=${searchTerm}`); - setUsers(response.data); - setLoading(false); - } catch (error) { - console.error('Error searching users:', error); - toast.error('Failed to search users'); - setLoading(false); - } - }; - - const handleInviteUser = async (userId) => { - try { - await axiosInstance.post(API_PATHS.STUDY_GROUPS.INVITE_USER(groupId), { userId }); - toast.success('Invitation sent successfully!'); - - // Remove the invited user from the list - setUsers(users.filter(user => user._id !== userId)); - } catch (error) { - console.error('Error inviting user:', error); - toast.error(error.response?.data?.message || 'Failed to send invitation'); - } - }; - - const handleSendEmailInvite = async (e) => { - e.preventDefault(); - try { - await axiosInstance.post(API_PATHS.STUDY_GROUPS.INVITE_BY_EMAIL(groupId), inviteForm); - toast.success(`Invitation email sent to ${inviteForm.email}`); - setInviteForm({ email: '' }); - } catch (error) { - console.error('Error sending invitation:', error); - toast.error(error.response?.data?.message || 'Failed to send invitation'); - } - }; - - const isCreator = studyGroup?.creator?._id === localStorage.getItem('userId'); - - return ( - -
- {loading && !studyGroup ? ( -
- -
- ) : studyGroup ? ( - <> -
-

{studyGroup.name}

-
- {isCreator ? ( - <> - - - - ) : ( - <> - - - - )} - -
-
- -
-
-

About

-

{studyGroup.description}

-
- -
-

Topics

-
- {studyGroup.topics.map((topic, index) => ( - - {topic} - - ))} -
-
- -
-

Members ({studyGroup.members.length}/{studyGroup.maxMembers})

-
- {studyGroup.members.map((member) => ( -
- {member.profileImageUrl ? ( - {member.name} - ) : ( -
- {member.name.charAt(0)} -
- )} -
-

{member.name}

-

{member.email}

-
- {member._id === studyGroup.creator._id && ( - - Creator - - )} -
- ))} -
-
- - {isCreator && studyGroup.joinRequests && studyGroup.joinRequests.length > 0 && ( -
-

Join Requests

-
- {studyGroup.joinRequests - .filter(request => request.status === 'pending') - .map((request) => ( -
-
- {request.user.profileImageUrl ? ( - {request.user.name} - ) : ( -
- {request.user.name.charAt(0)} -
- )} -
-

{request.user.name}

-

{request.user.email}

-
-
-
- - -
-
- ))} -
-
- )} - -
-

Resources

- {studyGroup.resources && studyGroup.resources.length > 0 ? ( -
- {studyGroup.resources.map((resource, index) => ( -
-

{resource.title}

-

{resource.description}

- - View Resource - -
- Added by {studyGroup.members.find(m => m._id === resource.addedBy)?.name || 'Unknown'} -
-
- ))} -
- ) : ( -

No resources have been added yet.

- )} -
-
- - ) : ( -
-

Study group not found

- -
- )} - - {/* Add Resource Modal */} - {showAddResourceModal && ( -
-
-

Add Resource

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- )} - - {/* Invite Friend Modal */} - {showInviteFriendModal && ( -
-
-

Invite Friends

- - {/* Search for users */} -
- -
- - -
-
- - {/* Search results */} - {users.length > 0 && ( -
-

Search Results

-
- {users.map(user => ( -
-
-

{user.name}

-

{user.email}

-
- -
- ))} -
-
- )} - - {/* Invite by email */} -
-

Or invite by email

-
- - -
-

An invitation email will be sent to this address

-
- -
- -
-
-
- )} -
-
- ); -}; - -export default StudyGroupDetail; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/pages/Collaborative/StudyGroups.jsx b/frontend/interview-perp-ai/src/pages/Collaborative/StudyGroups.jsx deleted file mode 100644 index 1fabd76..0000000 --- a/frontend/interview-perp-ai/src/pages/Collaborative/StudyGroups.jsx +++ /dev/null @@ -1,299 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import DashboardLayout from '../../components/layouts/DashboardLayout'; -import axiosInstance from '../../utils/axiosInstance'; -import { API_PATHS } from '../../utils/apiPaths'; -import SpinnerLoader from '../../components/Loader/SpinnerLoader'; -import { toast } from 'react-hot-toast'; - -const StudyGroups = () => { - const navigate = useNavigate(); - const [studyGroups, setStudyGroups] = useState([]); - const [userGroups, setUserGroups] = useState([]); - const [loading, setLoading] = useState(true); - const [showCreateModal, setShowCreateModal] = useState(false); - const [formData, setFormData] = useState({ - name: '', - description: '', - topics: '', - isPublic: true, - maxMembers: 10 - }); - - useEffect(() => { - fetchStudyGroups(); - fetchUserGroups(); - }, []); - - const fetchStudyGroups = async () => { - try { - const response = await axiosInstance.get(API_PATHS.STUDY_GROUPS.GET_ALL); - setStudyGroups(response.data); - } catch (error) { - console.error('Error fetching study groups:', error); - toast.error('Failed to load study groups'); - } finally { - setLoading(false); - } - }; - - const fetchUserGroups = async () => { - try { - const response = await axiosInstance.get(API_PATHS.STUDY_GROUPS.GET_USER_GROUPS); - setUserGroups(response.data); - } catch (error) { - console.error('Error fetching user groups:', error); - } - }; - - const handleInputChange = (e) => { - const { name, value, type, checked } = e.target; - setFormData({ - ...formData, - [name]: type === 'checkbox' ? checked : value - }); - }; - - const handleCreateGroup = async (e) => { - e.preventDefault(); - try { - setLoading(true); - const topicsArray = formData.topics.split(',').map(topic => topic.trim()); - const payload = { - ...formData, - topics: topicsArray - }; - - await axiosInstance.post(API_PATHS.STUDY_GROUPS.CREATE, payload); - toast.success('Study group created successfully!'); - setShowCreateModal(false); - fetchStudyGroups(); - fetchUserGroups(); - setFormData({ - name: '', - description: '', - topics: '', - isPublic: true, - maxMembers: 10 - }); - } catch (error) { - console.error('Error creating study group:', error); - toast.error('Failed to create study group'); - } finally { - setLoading(false); - } - }; - - const handleJoinGroup = async (groupId) => { - try { - await axiosInstance.post(API_PATHS.STUDY_GROUPS.JOIN(groupId)); - toast.success('Join request sent successfully!'); - fetchStudyGroups(); - } catch (error) { - console.error('Error joining group:', error); - toast.error('Failed to join group'); - } - }; - - const handleViewGroup = (groupId) => { - navigate(`/study-groups/${groupId}`); - }; - - return ( - -
-
-

Study Groups

- -
- - {loading ? ( -
- -
- ) : ( -
- {/* My Groups Section */} - {userGroups.length > 0 && ( -
-

My Groups

-
- {userGroups.map((group) => ( -
-

{group.name}

-

{group.description}

-
- {group.topics.map((topic, index) => ( - - {topic} - - ))} -
-
- - {group.members.length}/{group.maxMembers} members - - -
-
- ))} -
-
- )} - - {/* Available Groups */} -
-

Available Groups

- {studyGroups.length === 0 ? ( -

No study groups available. Create one to get started!

- ) : ( -
- {studyGroups - .filter(group => !userGroups.some(ug => ug._id === group._id)) - .map((group) => ( -
-
-

{group.name}

- - {group.isPublic ? 'Public' : 'Private'} - -
-

{group.description}

-
- {group.topics.map((topic, index) => ( - - {topic} - - ))} -
-
- - {group.members.length}/{group.maxMembers} members - - -
-
- ))} -
- )} -
-
- )} - - {/* Create Group Modal */} - {showCreateModal && ( -
-
-

Create New Study Group

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- )} -
-
- ); -}; - -export default StudyGroups; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/pages/Home/Dashboard.jsx b/frontend/interview-perp-ai/src/pages/Home/Dashboard.jsx index b18677f..3a1729c 100644 --- a/frontend/interview-perp-ai/src/pages/Home/Dashboard.jsx +++ b/frontend/interview-perp-ai/src/pages/Home/Dashboard.jsx @@ -11,7 +11,6 @@ import axiosInstance from '../../utils/axiosInstance'; import { API_PATHS } from '../../utils/apiPaths'; import moment from "moment"; import { CARD_BG } from "../../utils/data"; -import CollaborativeNav from '../../components/Collaborative/CollaborativeNav'; const Dashboard = () => { @@ -75,10 +74,6 @@ const Dashboard = () => { )} - -
- -
{isLoading ? (

Loading...

diff --git a/frontend/interview-perp-ai/src/utils/apiPaths.js b/frontend/interview-perp-ai/src/utils/apiPaths.js index b7541f8..671d1cf 100644 --- a/frontend/interview-perp-ai/src/utils/apiPaths.js +++ b/frontend/interview-perp-ai/src/utils/apiPaths.js @@ -15,7 +15,6 @@ export const API_PATHS = { PRACTICE_FEEDBACK: "/api/ai/practice-feedback", COMPANY_TAGS: "/api/ai/company-tags", COMPANY_QUESTIONS: "/api/ai/company-questions", - GENERATE_FOLLOW_UP: "/api/ai/follow-up", }, // ✅ FIX: Renamed to SESSIONS for consistency SESSIONS: { @@ -70,56 +69,4 @@ export const API_PATHS = { RECOMMENDATIONS: "/api/learning-path/recommendations", COMPLETE_ITEM: "/api/learning-path/complete-item", }, - // New collaborative features - STUDY_GROUPS: { - CREATE: "/api/study-groups", - GET_ALL: "/api/study-groups", - GET_ONE: (id) => `/api/study-groups/${id}`, - JOIN: (id) => `/api/study-groups/${id}/join`, - LEAVE: (id) => `/api/study-groups/${id}/leave`, - HANDLE_REQUEST: (id) => `/api/study-groups/${id}/handle-request`, - ADD_RESOURCE: (id) => `/api/study-groups/${id}/resources`, - DELETE: (id) => `/api/study-groups/${id}`, - GET_USER_GROUPS: "/api/study-groups/user/groups", - INVITE_USER: (id) => `/api/study-groups/${id}/invite`, - INVITE_BY_EMAIL: (id) => `/api/study-groups/${id}/invite-by-email`, - SEARCH_USERS: "/api/study-groups/search-users", - }, - PEER_REVIEWS: { - CREATE: "/api/peer-reviews", - GET_RECEIVED: "/api/peer-reviews/received", - GET_GIVEN: "/api/peer-reviews/given", - GET_ONE: (id) => `/api/peer-reviews/${id}`, - UPDATE: (id) => `/api/peer-reviews/${id}`, - DELETE: (id) => `/api/peer-reviews/${id}`, - REQUEST: "/api/peer-reviews/request", - GET_OPEN_REQUESTS: "/api/peer-reviews/requests/open", - ACCEPT_REQUEST: (id) => `/api/peer-reviews/requests/${id}/accept`, - }, - MENTORSHIPS: { - REQUEST: "/api/mentorships/request", - RESPOND: (id) => `/api/mentorships/${id}/respond`, - GET_ALL: "/api/mentorships", - GET_ONE: (id) => `/api/mentorships/${id}`, - ADD_NOTE: (id) => `/api/mentorships/${id}/notes`, - SCHEDULE_MEETING: (id) => `/api/mentorships/${id}/meetings`, - UPDATE_MEETING: (id) => `/api/mentorships/${id}/meetings`, - UPDATE_PROGRESS: (id) => `/api/mentorships/${id}/progress`, - END: (id) => `/api/mentorships/${id}/end`, - GET_AVAILABLE_MENTORS: "/api/mentorships/mentors/available", - }, - FORUMS: { - CREATE: "/api/forums", - GET_ALL: "/api/forums", - GET_ONE: (id) => `/api/forums/${id}`, - UPDATE: (id) => `/api/forums/${id}`, - DELETE: (id) => `/api/forums/${id}`, - CREATE_POST: (id) => `/api/forums/${id}/posts`, - GET_POST: (postId) => `/api/forums/posts/${postId}`, - ADD_COMMENT: (postId) => `/api/forums/posts/${postId}/comments`, - UPVOTE_POST: (postId) => `/api/forums/posts/${postId}/upvote`, - UPDATE_POST: (postId) => `/api/forums/posts/${postId}`, - DELETE_POST: (postId) => `/api/forums/posts/${postId}`, - GET_USER_POSTS: "/api/forums/user/posts", - }, }; From 56121784ce863e670a4e0e36bc96b047f7abee0a Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 19 Sep 2025 00:45:18 +0530 Subject: [PATCH 002/198] feat: New Featues Added: Filters, Ratings --- backend/controllers/questionController.js | 84 ++++++ backend/controllers/sessionController.js | 84 ++++++ backend/models/Question.js | 14 + backend/models/Session.js | 18 +- backend/routes/questionRoutes.js | 6 +- backend/routes/sessionRoutes.js | 12 +- .../src/components/Cards/QuestionCard.jsx | 2 +- .../src/components/Cards/SummaryCard.jsx | 146 +++++++--- .../components/Cards/SummaryCard_backup.jsx | 81 ++++++ .../src/components/QuestionFilter.jsx | 254 ++++++++++++++++++ .../src/components/RatingModal.jsx | 131 +++++++++ .../src/components/SessionFilter.jsx | 221 +++++++++++++++ .../src/hooks/useQuestionFilter.js | 111 ++++++++ .../src/hooks/useSessionFilter.js | 132 +++++++++ .../src/pages/Home/Dashboard.jsx | 190 +++++++++++-- .../src/pages/InterviewPrep/InterviewPrep.jsx | 7 + .../interview-perp-ai/src/utils/apiPaths.js | 4 + 17 files changed, 1443 insertions(+), 54 deletions(-) create mode 100644 frontend/interview-perp-ai/src/components/Cards/SummaryCard_backup.jsx create mode 100644 frontend/interview-perp-ai/src/components/QuestionFilter.jsx create mode 100644 frontend/interview-perp-ai/src/components/RatingModal.jsx create mode 100644 frontend/interview-perp-ai/src/components/SessionFilter.jsx create mode 100644 frontend/interview-perp-ai/src/hooks/useQuestionFilter.js create mode 100644 frontend/interview-perp-ai/src/hooks/useSessionFilter.js diff --git a/backend/controllers/questionController.js b/backend/controllers/questionController.js index 2dd67fa..abb95e3 100644 --- a/backend/controllers/questionController.js +++ b/backend/controllers/questionController.js @@ -182,10 +182,94 @@ const reviewQuestion = async (req, res) => { +// Removed updateQuestionRating - ratings are now only for sessions + +// @desc Update question justification (admin only for now) +// @route PUT /api/questions/:id/justification +// @access Private +const updateQuestionJustification = async (req, res) => { + try { + const { id } = req.params; + const { probability, reasoning, commonCompanies, interviewType } = req.body; + + const question = await Question.findById(id); + if (!question) { + return res.status(404).json({ message: "Question not found" }); + } + + // Update justification fields + if (probability !== undefined) question.justification.probability = probability; + if (reasoning !== undefined) question.justification.reasoning = reasoning; + if (commonCompanies !== undefined) question.justification.commonCompanies = commonCompanies; + if (interviewType !== undefined) question.justification.interviewType = interviewType; + + await question.save(); + res.status(200).json({ message: "Justification updated successfully", question }); + + } catch (error) { + console.error("Error updating question justification:", error); + res.status(500).json({ message: "Server Error" }); + } +}; + +// @desc Get questions with filtering options +// @route GET /api/questions/filter +// @access Private +const getFilteredQuestions = async (req, res) => { + try { + const userId = req.user._id; + const { + difficulty, + category, + interviewType, + probability, + isPinned, + isMastered, + minRating, + tags + } = req.query; + + // Build filter object + let filter = {}; + + // Get user's sessions first + const sessions = await Session.find({ user: userId }); + const sessionIds = sessions.map(session => session._id); + filter.session = { $in: sessionIds }; + + if (difficulty) filter.difficulty = difficulty; + if (category) filter.category = category; + if (interviewType) filter['justification.interviewType'] = interviewType; + if (probability) filter['justification.probability'] = probability; + if (isPinned !== undefined) filter.isPinned = isPinned === 'true'; + if (isMastered !== undefined) filter.isMastered = isMastered === 'true'; + if (tags) filter.tags = { $in: tags.split(',') }; + + let questions = await Question.find(filter).populate('session'); + + // Filter by minimum rating if specified + if (minRating) { + const minRatingNum = parseFloat(minRating); + questions = questions.filter(q => { + const avgRating = (q.userRating.difficulty + q.userRating.usefulness + q.userRating.clarity) / 3; + return avgRating >= minRatingNum; + }); + } + + res.status(200).json({ questions }); + + } catch (error) { + console.error("Error filtering questions:", error); + res.status(500).json({ message: "Server Error" }); + } +}; + module.exports = { addQuestionsToSession, togglePinQuestion, updateQuestionNote, toggleMasteredStatus, reviewQuestion, + updateQuestionJustification, + getFilteredQuestions, }; diff --git a/backend/controllers/sessionController.js b/backend/controllers/sessionController.js index 42e9cd0..f9c9f5d 100644 --- a/backend/controllers/sessionController.js +++ b/backend/controllers/sessionController.js @@ -108,10 +108,94 @@ const getReviewQueue = async (req, res) => { } }; +// @desc Update session rating +// @route PUT /api/sessions/:id/rating +// @access Private +const updateSessionRating = async (req, res) => { + try { + const { id } = req.params; + const { overall, difficulty, usefulness } = req.body; + const userId = req.user._id; + + // Validate rating values + const ratings = { overall, difficulty, usefulness }; + for (const [key, value] of Object.entries(ratings)) { + if (value !== undefined && (value < 1 || value > 5)) { + return res.status(400).json({ message: `${key} rating must be between 1 and 5` }); + } + } + + const session = await Session.findById(id); + if (!session) { + return res.status(404).json({ message: "Session not found" }); + } + + // Verify the session belongs to the user + if (session.user.toString() !== userId.toString()) { + return res.status(401).json({ message: "Not authorized" }); + } + + // Update ratings + if (overall !== undefined) session.userRating.overall = overall; + if (difficulty !== undefined) session.userRating.difficulty = difficulty; + if (usefulness !== undefined) session.userRating.usefulness = usefulness; + + await session.save(); + res.status(200).json({ message: "Rating updated successfully", session }); + + } catch (error) { + console.error("Error updating session rating:", error); + res.status(500).json({ message: "Server Error" }); + } +}; + +// @desc Update session progress +// @route PUT /api/sessions/:id/progress +// @access Private +const updateSessionProgress = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user._id; + + const session = await Session.findById(id).populate('questions'); + if (!session) { + return res.status(404).json({ message: "Session not found" }); + } + + if (session.user.toString() !== userId.toString()) { + return res.status(401).json({ message: "Not authorized" }); + } + + // Calculate progress based on mastered questions + const totalQuestions = session.questions.length; + const masteredQuestions = session.questions.filter(q => q.isMastered).length; + const completionPercentage = totalQuestions > 0 ? Math.round((masteredQuestions / totalQuestions) * 100) : 0; + + session.masteredQuestions = masteredQuestions; + session.completionPercentage = completionPercentage; + + // Auto-update status based on progress + if (completionPercentage === 100) { + session.status = 'Completed'; + } else if (completionPercentage > 0) { + session.status = 'Active'; + } + + await session.save(); + res.status(200).json({ message: "Progress updated successfully", session }); + + } catch (error) { + console.error("Error updating session progress:", error); + res.status(500).json({ message: "Server Error" }); + } +}; + module.exports = { createSession, getMySessions, getSessionById, deleteSession, getReviewQueue, + updateSessionRating, + updateSessionProgress, }; diff --git a/backend/models/Question.js b/backend/models/Question.js index 4613628..4123683 100644 --- a/backend/models/Question.js +++ b/backend/models/Question.js @@ -20,6 +20,20 @@ const questionSchema = new mongoose.Schema({ } ], + // --- NEW FEATURES --- + // Justification for why this question is relevant in real interviews + justification: { + probability: { type: String, enum: ['Very High', 'High', 'Medium', 'Low'], default: 'Medium' }, + reasoning: { type: String, default: '' }, + commonCompanies: [{ type: String }], + interviewType: { type: String, enum: ['Technical', 'Behavioral', 'System Design', 'Coding', 'General'], default: 'Technical' } + }, + + // Additional metadata for filtering + tags: [{ type: String }], + difficulty: { type: String, enum: ['Easy', 'Medium', 'Hard'], default: 'Medium' }, + category: { type: String, default: 'General' }, + // --- EXISTING SPACED REPETITION FIELDS --- dueDate: { type: Date, default: () => new Date() }, isPinned: { type: Boolean, default: false }, diff --git a/backend/models/Session.js b/backend/models/Session.js index 3eb8b0f..96787f9 100644 --- a/backend/models/Session.js +++ b/backend/models/Session.js @@ -7,7 +7,23 @@ const sessionSchema = new mongoose.Schema({ topicsToFocus: {type: String, required: true}, description: String, questions: [{type: mongoose.Schema.Types.ObjectId, ref: "Question"}], + + // User rating for the session + userRating: { + overall: { type: Number, min: 1, max: 5, default: 3 }, + difficulty: { type: Number, min: 1, max: 5, default: 3 }, + usefulness: { type: Number, min: 1, max: 5, default: 3 } + }, + + // Session metadata for filtering + category: { type: String, default: 'General' }, + tags: [{ type: String }], + status: { type: String, enum: ['Active', 'Completed', 'Paused'], default: 'Active' }, + + // Progress tracking + completionPercentage: { type: Number, default: 0, min: 0, max: 100 }, + masteredQuestions: { type: Number, default: 0 }, }, {timestamps: true }); -module.exports = mongoose.model("Session", sessionSchema); \ No newline at end of file +module.exports = mongoose.model("Session", sessionSchema); \ No newline at end of file diff --git a/backend/routes/questionRoutes.js b/backend/routes/questionRoutes.js index b5753c9..bc30043 100644 --- a/backend/routes/questionRoutes.js +++ b/backend/routes/questionRoutes.js @@ -5,7 +5,8 @@ const { addQuestionsToSession, toggleMasteredStatus, reviewQuestion, - getQuestionsByCompany + updateQuestionJustification, + getFilteredQuestions } = require('../controllers/questionController'); const { protect } = require('../middlewares/authMiddleware'); @@ -19,5 +20,8 @@ router.put('/:id/note', protect, updateQuestionNote); router.put('/:id/master', protect, toggleMasteredStatus); router.put('/:id/review', protect, reviewQuestion); +// New routes for justifications and filtering (removed rating route) +router.put('/:id/justification', protect, updateQuestionJustification); +router.get('/filter', protect, getFilteredQuestions); module.exports = router; diff --git a/backend/routes/sessionRoutes.js b/backend/routes/sessionRoutes.js index 9d13dfc..b4a5c4e 100644 --- a/backend/routes/sessionRoutes.js +++ b/backend/routes/sessionRoutes.js @@ -4,7 +4,9 @@ const { getSessionById, getMySessions, deleteSession, - getReviewQueue + getReviewQueue, + updateSessionRating, + updateSessionProgress } = require('../controllers/sessionController'); const { protect } = require('../middlewares/authMiddleware'); @@ -33,5 +35,13 @@ router.get('/:id', protect, getSessionById); // Deletes a single session by its unique ID. router.delete('/:id', protect, deleteSession); +// PUT /api/sessions/:id/rating +// Updates the rating for a session. +router.put('/:id/rating', protect, updateSessionRating); + +// PUT /api/sessions/:id/progress +// Updates the progress for a session. +router.put('/:id/progress', protect, updateSessionProgress); + // Export the router to be used in the main server file module.exports = router; diff --git a/frontend/interview-perp-ai/src/components/Cards/QuestionCard.jsx b/frontend/interview-perp-ai/src/components/Cards/QuestionCard.jsx index 5edc219..501fbf3 100644 --- a/frontend/interview-perp-ai/src/components/Cards/QuestionCard.jsx +++ b/frontend/interview-perp-ai/src/components/Cards/QuestionCard.jsx @@ -120,4 +120,4 @@ const QuestionCard = ({ ); }; -export default QuestionCard; +export default QuestionCard; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/components/Cards/SummaryCard.jsx b/frontend/interview-perp-ai/src/components/Cards/SummaryCard.jsx index f1aa2c4..8751ac3 100644 --- a/frontend/interview-perp-ai/src/components/Cards/SummaryCard.jsx +++ b/frontend/interview-perp-ai/src/components/Cards/SummaryCard.jsx @@ -1,5 +1,5 @@ import React from 'react' -import { LuTrash2 } from 'react-icons/lu'; +import { LuTrash2, LuStar } from 'react-icons/lu'; import { getInitials } from '../../utils/helper'; const SummaryCard = ({ @@ -11,12 +11,32 @@ const SummaryCard = ({ description, lastUpdated, onSelect, - onDelete + onDelete, + // New props for enhanced features + userRating = { overall: 3, difficulty: 3, usefulness: 3 }, + status = 'Active', + completionPercentage = 0, + masteredQuestions = 0, + onRateClick, + sessionId }) => { - return
{ + switch (status) { + case 'Active': return 'text-green-700 bg-green-100 border-green-200'; + case 'Completed': return 'text-blue-700 bg-blue-100 border-blue-200'; + case 'Paused': return 'text-yellow-700 bg-yellow-100 border-yellow-200'; + default: return 'text-gray-700 bg-gray-100 border-gray-200'; + } + }; + + + const avgRating = (userRating.overall + userRating.difficulty + userRating.usefulness) / 3; + + return ( +
+ >
- + {/* Enhanced Header with Status and Rating */} +
+ + {status} + +
+ + +
+
-
-
-
- Experience: {experience} {experience == 1 ? "Year" : "Years"} +
+ {/* Rating Display */} +
+
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ ({avgRating.toFixed(1)})
- -
- {questions} Q&A + + {completionPercentage > 0 && ( +
+
+
+
+ {completionPercentage}% +
+ )} +
+ + {/* Enhanced Stats */} +
+
+
Experience
+
{experience} {experience == 1 ? "Year" : "Years"}
- -
- Last Updated: {lastUpdated} + +
+
Questions
+
{questions} Q&A
+ + {masteredQuestions > 0 && ( +
+
Mastered Questions
+
{masteredQuestions} completed
+
+ )}
-

- {description} -

-
+ {/* Description */} + {description && ( +

+ {description} +

+ )} + + {/* Last Updated */} +
+ + + + Updated {lastUpdated} +
- - -} +
+ ); +}; export default SummaryCard; \ No newline at end of file diff --git a/frontend/interview-perp-ai/src/components/Cards/SummaryCard_backup.jsx b/frontend/interview-perp-ai/src/components/Cards/SummaryCard_backup.jsx new file mode 100644 index 0000000..8398460 --- /dev/null +++ b/frontend/interview-perp-ai/src/components/Cards/SummaryCard_backup.jsx @@ -0,0 +1,81 @@ +import React from 'react' +import { LuTrash2 } from 'react-icons/lu'; +import { getInitials } from '../../utils/helper'; + +const SummaryCard = ({ + colors, + role, + topicsToFocus, + experience, + questions, + description, + lastUpdated, + onSelect, + onDelete +}) => { + return
+
+
+
+ + {getInitials(role)} + +
+ +
+
+
+

{role}

+

+ {topicsToFocus} +

+
+
+
+
+ + +
+ +
+
+
+ Experience: {experience} {experience == 1 ? "Year" : "Years"} +
+ +
+ {questions} Q&A +
+ +
+ Last Updated: {lastUpdated} +
+
+ +

+ {description} +

+
+
+ + + +} + +export default SummaryCard; diff --git a/frontend/interview-perp-ai/src/components/QuestionFilter.jsx b/frontend/interview-perp-ai/src/components/QuestionFilter.jsx new file mode 100644 index 0000000..ff52af3 --- /dev/null +++ b/frontend/interview-perp-ai/src/components/QuestionFilter.jsx @@ -0,0 +1,254 @@ +import React, { useState } from 'react'; +import { LuFilter, LuX, LuSearch, LuStar } from 'react-icons/lu'; + +const QuestionFilter = ({ onFilterChange, activeFilters = {} }) => { + const [isOpen, setIsOpen] = useState(false); + const [filters, setFilters] = useState({ + difficulty: '', + interviewType: '', + probability: '', + isPinned: '', + isMastered: '', + minRating: '', + searchTerm: '', + ...activeFilters + }); + + const handleFilterChange = (key, value) => { + const newFilters = { ...filters, [key]: value }; + setFilters(newFilters); + onFilterChange(newFilters); + }; + + const clearFilters = () => { + const clearedFilters = { + difficulty: '', + interviewType: '', + probability: '', + isPinned: '', + isMastered: '', + minRating: '', + searchTerm: '' + }; + setFilters(clearedFilters); + onFilterChange(clearedFilters); + }; + + const getActiveFilterCount = () => { + return Object.values(filters).filter(value => value !== '').length; + }; + + return ( +
+ {/* Filter Toggle Button */} +
+ + + {/* Quick Search */} +
+ + handleFilterChange('searchTerm', e.target.value)} + placeholder="Search questions..." + className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" + /> +
+ + {getActiveFilterCount() > 0 && ( + + )} +
+ + {/* Filter Panel */} + {isOpen && ( +
+
+

Filter Question Cards

+ +
+ +
+ {/* Difficulty Filter */} +
+ + +
+ + {/* Interview Type Filter */} +
+ + +
+ + {/* Probability Filter */} +
+ + +
+ + {/* Rating Filter */} +
+ + +
+
+ + {/* Status Filters */} +
+ +
+ + + + + +
+
+ + {/* Active Filters Summary */} + {getActiveFilterCount() > 0 && ( +
+
+ + {getActiveFilterCount()} filter{getActiveFilterCount() > 1 ? 's' : ''} active + + +
+
+ )} +
+ )} +
+ ); +}; + +export default QuestionFilter; diff --git a/frontend/interview-perp-ai/src/components/RatingModal.jsx b/frontend/interview-perp-ai/src/components/RatingModal.jsx new file mode 100644 index 0000000..5201684 --- /dev/null +++ b/frontend/interview-perp-ai/src/components/RatingModal.jsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { LuStar } from 'react-icons/lu'; + +const RatingModal = ({ isOpen, onClose, sessionData, onSubmit }) => { + const [ratings, setRatings] = useState({ + overall: 3, + difficulty: 3, + usefulness: 3 + }); + + useEffect(() => { + if (isOpen && sessionData?.userRating) { + setRatings(sessionData.userRating); + } + }, [isOpen, sessionData]); + + const handleSubmit = () => { + onSubmit(ratings); + onClose(); + }; + + const StarRating = ({ value, onChange, label, description }) => { + return ( +
+
+ {label} + ({value}/5) +
+ {description && ( +

{description}

+ )} +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ ); + }; + + if (!isOpen) return null; + + return ( +
+
+
+ +
+ {/* Header */} +
+
+
+

Rate Your Session

+

+ {sessionData?.role} • {sessionData?.topicsToFocus} +

+
+ +
+
+ + {/* Content */} +
+ setRatings(prev => ({ ...prev, overall: value }))} + /> + + setRatings(prev => ({ ...prev, difficulty: value }))} + /> + + setRatings(prev => ({ ...prev, usefulness: value }))} + /> +
+ + {/* Footer */} +
+
+ + +
+
+
+
+
+ ); +}; + +export default RatingModal; diff --git a/frontend/interview-perp-ai/src/components/SessionFilter.jsx b/frontend/interview-perp-ai/src/components/SessionFilter.jsx new file mode 100644 index 0000000..1dc7b80 --- /dev/null +++ b/frontend/interview-perp-ai/src/components/SessionFilter.jsx @@ -0,0 +1,221 @@ +import React, { useState } from 'react'; +import { LuFilter, LuX, LuSearch, LuStar } from 'react-icons/lu'; + +const SessionFilter = ({ onFilterChange, activeFilters = {} }) => { + const [isOpen, setIsOpen] = useState(false); + const [filters, setFilters] = useState({ + experience: '', + status: '', + minRating: '', + searchTerm: '', + sortBy: 'lastUpdated', + sortOrder: 'desc', + ...activeFilters + }); + + const handleFilterChange = (key, value) => { + const newFilters = { ...filters, [key]: value }; + setFilters(newFilters); + onFilterChange(newFilters); + }; + + const clearFilters = () => { + const clearedFilters = { + experience: '', + status: '', + minRating: '', + searchTerm: '', + sortBy: 'lastUpdated', + sortOrder: 'desc' + }; + setFilters(clearedFilters); + onFilterChange(clearedFilters); + }; + + const getActiveFilterCount = () => { + const { sortBy, sortOrder, ...filterableFields } = filters; + return Object.values(filterableFields).filter(value => value !== '').length; + }; + + return ( +
+ {/* Enhanced Filter Header */} +
+
+ {/* Search Input */} +
+
+ +
+ handleFilterChange('searchTerm', e.target.value)} + className="block w-full pl-10 pr-4 py-3 border border-gray-200 rounded-xl text-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50 transition-all duration-200" + /> +
+ + {/* Filter Toggle Button */} + +
+ + {/* Sort and Clear Options */} +
+ + + {getActiveFilterCount() > 0 && ( + + )} +
+
+ + {/* Filter Panel */} + {isOpen && ( +
+
+

Filter Interview Sessions

+ +
+ +
+ {/* Experience Filter */} +
+ + +
+ + {/* Status Filter */} +
+ + +
+ + {/* Rating Filter */} +
+ + +
+ + {/* Quick Filters */} +
+ +
+ + +
+
+
+ + {/* Active Filters Summary */} + {getActiveFilterCount() > 0 && ( +
+
+ + {getActiveFilterCount()} filter{getActiveFilterCount() > 1 ? 's' : ''} active + + +
+
+ )} +
+ )} +
+ ); +}; + +export default SessionFilter; diff --git a/frontend/interview-perp-ai/src/hooks/useQuestionFilter.js b/frontend/interview-perp-ai/src/hooks/useQuestionFilter.js new file mode 100644 index 0000000..60a2ddf --- /dev/null +++ b/frontend/interview-perp-ai/src/hooks/useQuestionFilter.js @@ -0,0 +1,111 @@ +import { useState, useMemo } from 'react'; + +export const useQuestionFilter = (questions = []) => { + const [filters, setFilters] = useState({ + difficulty: '', + interviewType: '', + probability: '', + isPinned: '', + isMastered: '', + minRating: '', + searchTerm: '' + }); + + const filteredQuestions = useMemo(() => { + if (!questions || questions.length === 0) return []; + + return questions.filter(question => { + // Search term filter + if (filters.searchTerm) { + const searchLower = filters.searchTerm.toLowerCase(); + const questionText = question.question?.toLowerCase() || ''; + const answerText = question.answer?.toLowerCase() || ''; + if (!questionText.includes(searchLower) && !answerText.includes(searchLower)) { + return false; + } + } + + // Difficulty filter + if (filters.difficulty && question.difficulty !== filters.difficulty) { + return false; + } + + // Interview type filter + if (filters.interviewType && question.justification?.interviewType !== filters.interviewType) { + return false; + } + + // Probability filter + if (filters.probability && question.justification?.probability !== filters.probability) { + return false; + } + + // Pinned filter + if (filters.isPinned !== '') { + const isPinned = filters.isPinned === 'true'; + if (question.isPinned !== isPinned) { + return false; + } + } + + // Mastered filter + if (filters.isMastered !== '') { + const isMastered = filters.isMastered === 'true'; + if (question.isMastered !== isMastered) { + return false; + } + } + + // Rating filter + if (filters.minRating) { + const minRating = parseFloat(filters.minRating); + const userRating = question.userRating || { difficulty: 3, usefulness: 3, clarity: 3 }; + const avgRating = (userRating.difficulty + userRating.usefulness + userRating.clarity) / 3; + if (avgRating < minRating) { + return false; + } + } + + return true; + }); + }, [questions, filters]); + + const updateFilters = (newFilters) => { + setFilters(newFilters); + }; + + const clearFilters = () => { + setFilters({ + difficulty: '', + interviewType: '', + probability: '', + isPinned: '', + isMastered: '', + minRating: '', + searchTerm: '' + }); + }; + + const getFilterStats = () => { + const total = questions.length; + const filtered = filteredQuestions.length; + const activeFilters = Object.values(filters).filter(value => value !== '').length; + + return { + total, + filtered, + activeFilters, + isFiltered: activeFilters > 0 + }; + }; + + return { + filters, + filteredQuestions, + updateFilters, + clearFilters, + getFilterStats + }; +}; + +export default useQuestionFilter; diff --git a/frontend/interview-perp-ai/src/hooks/useSessionFilter.js b/frontend/interview-perp-ai/src/hooks/useSessionFilter.js new file mode 100644 index 0000000..15f7a15 --- /dev/null +++ b/frontend/interview-perp-ai/src/hooks/useSessionFilter.js @@ -0,0 +1,132 @@ +import { useState, useMemo } from 'react'; + +export const useSessionFilter = (sessions = []) => { + const [filters, setFilters] = useState({ + experience: '', + status: '', + minRating: '', + searchTerm: '', + sortBy: 'lastUpdated', + sortOrder: 'desc' + }); + + const filteredAndSortedSessions = useMemo(() => { + if (!sessions || sessions.length === 0) return []; + + let filtered = sessions.filter(session => { + // Search term filter + if (filters.searchTerm) { + const searchLower = filters.searchTerm.toLowerCase(); + const roleText = session.role?.toLowerCase() || ''; + const topicsText = session.topicsToFocus?.toLowerCase() || ''; + const descriptionText = session.description?.toLowerCase() || ''; + + if (!roleText.includes(searchLower) && + !topicsText.includes(searchLower) && + !descriptionText.includes(searchLower)) { + return false; + } + } + + // Experience filter + if (filters.experience) { + const sessionExp = session.experience?.toString(); + if (filters.experience === '5' && parseInt(sessionExp) < 5) { + return false; + } else if (filters.experience !== '5' && sessionExp !== filters.experience) { + return false; + } + } + + // Status filter + if (filters.status && session.status !== filters.status) { + return false; + } + + // Rating filter + if (filters.minRating) { + const minRating = parseFloat(filters.minRating); + const userRating = session.userRating || { overall: 3, difficulty: 3, usefulness: 3 }; + const avgRating = (userRating.overall + userRating.difficulty + userRating.usefulness) / 3; + if (avgRating < minRating) { + return false; + } + } + + return true; + }); + + // Sort the filtered results + filtered.sort((a, b) => { + let aValue, bValue; + + switch (filters.sortBy) { + case 'role': + aValue = a.role?.toLowerCase() || ''; + bValue = b.role?.toLowerCase() || ''; + break; + case 'questions': + aValue = a.questions?.length || 0; + bValue = b.questions?.length || 0; + break; + case 'createdAt': + aValue = new Date(a.createdAt); + bValue = new Date(b.createdAt); + break; + case 'lastUpdated': + default: + aValue = new Date(a.updatedAt); + bValue = new Date(b.updatedAt); + break; + } + + if (filters.sortOrder === 'asc') { + return aValue > bValue ? 1 : -1; + } else { + return aValue < bValue ? 1 : -1; + } + }); + + return filtered; + }, [sessions, filters]); + + const updateFilters = (newFilters) => { + setFilters(newFilters); + }; + + const clearFilters = () => { + setFilters({ + experience: '', + status: '', + minRating: '', + searchTerm: '', + sortBy: 'lastUpdated', + sortOrder: 'desc' + }); + }; + + const getFilterStats = () => { + const total = sessions.length; + const filtered = filteredAndSortedSessions.length; + const activeFilters = Object.entries(filters) + .filter(([key, value]) => key !== 'sortBy' && key !== 'sortOrder' && value !== '') + .length; + + return { + total, + filtered, + activeFilters, + isFiltered: activeFilters > 0 + }; + }; + + return { + filters, + filteredSessions: filteredAndSortedSessions, + updateFilters, + clearFilters, + getFilterStats + }; +}; + +export default useSessionFilter; diff --git a/frontend/interview-perp-ai/src/pages/Home/Dashboard.jsx b/frontend/interview-perp-ai/src/pages/Home/Dashboard.jsx index 3a1729c..8e44c7c 100644 --- a/frontend/interview-perp-ai/src/pages/Home/Dashboard.jsx +++ b/frontend/interview-perp-ai/src/pages/Home/Dashboard.jsx @@ -7,10 +7,13 @@ import SummaryCard from '../../components/Cards/SummaryCard'; import Modal from '../../components/Modal'; import CreateSessionForm from './CreateSessionForm'; import DeleteAlertContent from '../../components/DeleteAlertContent'; +import SessionFilter from '../../components/SessionFilter'; +import useSessionFilter from '../../hooks/useSessionFilter'; import axiosInstance from '../../utils/axiosInstance'; import { API_PATHS } from '../../utils/apiPaths'; import moment from "moment"; import { CARD_BG } from "../../utils/data"; +import RatingModal from '../../components/RatingModal'; const Dashboard = () => { @@ -23,6 +26,15 @@ const Dashboard = () => { open: false, data: null, }); + + // State for rating modal + const [ratingModal, setRatingModal] = useState({ + open: false, + session: null + }); + + // Filter functionality for session cards + const { filters, filteredSessions, updateFilters, getFilterStats } = useSessionFilter(sessions); const fetchDashboardData = useCallback(async () => { setIsLoading(true); @@ -59,6 +71,29 @@ const Dashboard = () => { toast.error("Failed to delete session."); } }; + + const handleSessionRating = async (sessionId, ratings) => { + try { + await axiosInstance.put(API_PATHS.SESSIONS.UPDATE_RATING(sessionId), ratings); + toast.success("Session rating updated successfully!"); + setRatingModal({ open: false, session: null }); + fetchDashboardData(); + } catch (error) { + toast.error("Failed to update session rating."); + } + }; + + const openRatingModal = (session) => { + setRatingModal({ + open: true, + session: { + id: session._id, + role: session.role, + topicsToFocus: session.topicsToFocus, + userRating: session.userRating || { overall: 3, difficulty: 3, usefulness: 3 } + } + }); + }; useEffect(() => { fetchDashboardData(); @@ -66,41 +101,150 @@ const Dashboard = () => { return ( -
-
- {reviewCount > 0 && ( - - Start Review ({reviewCount} {reviewCount === 1 ? 'card' : 'cards'} due) - - )} +
+ {/* Hero Section */} +
+
+
+
+

+ My Interview Sessions +

+
+ +
+ {getFilterStats().filtered} of {getFilterStats().total} sessions +
+ {getFilterStats().filtered !== getFilterStats().total && ( + + (Filtered view) + + )} +
+
+ +
+ {reviewCount > 0 && ( + +
+ Start Review ({reviewCount}) + + )} + +
+
+
-
- {isLoading ? ( -

Loading...

- ) : sessions.length > 0 ? ( - sessions.map((data, index) => ( + + {/* Filter Section */} +
+
+ +
+
+ {/* Sessions Grid */} +
+
+ {isLoading ? ( +
+
+
+
+
+
+
+

Loading your sessions...

+

This won't take long

+
+
+
+ ) : filteredSessions.length > 0 ? ( + filteredSessions.map((data, index) => ( navigate(`/interview-prep/${data._id}`)} onDelete={() => setOpenDeleteAlert({ open: true, data })} + // Enhanced props + userRating={data.userRating || { overall: 3, difficulty: 3, usefulness: 3 }} + status={data.status || 'Active'} + completionPercentage={data.completionPercentage || 0} + masteredQuestions={data.masteredQuestions || 0} + onRateClick={() => openRatingModal(data)} /> )) + ) : getFilterStats().total > 0 ? ( +
+
+
+ + + +
+
+

No matching sessions

+

We couldn't find any sessions that match your current filters. Try adjusting your search criteria.

+ +
+
+
) : ( -

No sessions found. Click "Add New" to get started!

+
+
+
+ + + +
+
+

Ready to start your interview prep?

+

Create your first interview session and get AI-generated questions tailored to your role and experience level.

+
+ +
+
+
+
)} +
-
setOpenCreateModal(false)} hideHeader> { @@ -120,6 +264,14 @@ const Dashboard = () => { />
+ + {/* Rating Modal */} + setRatingModal({ open: false, session: null })} + sessionData={ratingModal.session} + onSubmit={(ratings) => handleSessionRating(ratingModal.session?.id, ratings)} + /> ); }; diff --git a/frontend/interview-perp-ai/src/pages/InterviewPrep/InterviewPrep.jsx b/frontend/interview-perp-ai/src/pages/InterviewPrep/InterviewPrep.jsx index 0345dce..0795156 100644 --- a/frontend/interview-perp-ai/src/pages/InterviewPrep/InterviewPrep.jsx +++ b/frontend/interview-perp-ai/src/pages/InterviewPrep/InterviewPrep.jsx @@ -12,6 +12,9 @@ import QuestionCard from '../../components/Cards/QuestionCard'; import Drawer from '../../components/Drawer'; import SkeletonLoader from '../../components/Loader/SkeletonLoader'; import AIResponsePreview from './components/AIResponsePreview'; +// Removed QuestionFilter - moved to Dashboard +// import QuestionFilter from '../../components/QuestionFilter'; +// import useQuestionFilter from '../../hooks/useQuestionFilter'; // ✅ FIX: Added the missing API path for the follow-up feature const API_PATHS = { @@ -57,6 +60,8 @@ const InterviewPrep = () => { const [followUpContent, setFollowUpContent] = useState(null); const [isFollowUpLoading, setIsFollowUpLoading] = useState(false); const [followUpError, setFollowUpError] = useState(""); + + // Filter functionality removed - moved to Dashboard const fetchSessionDetailsById = async () => { try { @@ -134,6 +139,8 @@ const InterviewPrep = () => { toast.error("Failed to update status."); } }; + + // Removed handleRatingUpdate - ratings are now only for sessions const uploadMoreQuestions = async () => { setIsUpdateLoader(true); diff --git a/frontend/interview-perp-ai/src/utils/apiPaths.js b/frontend/interview-perp-ai/src/utils/apiPaths.js index 671d1cf..521b7d4 100644 --- a/frontend/interview-perp-ai/src/utils/apiPaths.js +++ b/frontend/interview-perp-ai/src/utils/apiPaths.js @@ -23,6 +23,8 @@ export const API_PATHS = { GET_ONE: (id) => `/api/sessions/${id}`, DELETE: (id) => `/api/sessions/${id}`, GET_REVIEW_QUEUE: "/api/sessions/review-queue", + UPDATE_RATING: (id) => `/api/sessions/${id}/rating`, + UPDATE_PROGRESS: (id) => `/api/sessions/${id}/progress`, }, QUESTION: { ADD_TO_SESSION: "/api/questions/add", @@ -31,6 +33,8 @@ export const API_PATHS = { TOGGLE_MASTERED: (id) => `/api/questions/${id}/master`, GET_QUESTIONS_BY_COMPANY: "/api/questions/by-company", REVIEW: (id) => `/api/questions/${id}/review`, // ✅ FIX: Added the missing REVIEW path + UPDATE_JUSTIFICATION: (id) => `/api/questions/${id}/justification`, + FILTER: "/api/questions/filter", }, ANALYTICS: { GET_PERFORMANCE_OVER_TIME: "/api/analytics/performance-over-time", From 3c3cee4b432188ef6b76f8e297f0afdcbcefd728 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 19 Sep 2025 12:48:15 +0530 Subject: [PATCH 003/198] Feat: Added New Features:- Filter Sessions, Rate Sessions, UI/UX improvements --- README.md | 127 +++++++- backend/controllers/authController.js | 6 +- backend/controllers/questionController.js | 37 ++- backend/middlewares/authMiddleware.js | 10 +- backend/models/Question.js | 7 + backend/routes/questionRoutes.js | 4 +- backend/server.js | 37 ++- frontend/interview-perp-ai/.env.example | 5 + frontend/interview-perp-ai/src/App.jsx | 2 +- .../components/Cards/QuestionCard_clean.jsx | 0 .../Cards/QuestionCard_enhanced.jsx | 296 ++++++++++++++++++ .../Cards/QuestionCard_original.jsx | 0 .../src/components/Cards/SummaryCard.jsx | 23 +- .../src/context/userContext.jsx | 11 +- .../src/pages/Home/CreateSessionForm.jsx | 2 +- .../src/pages/Home/Dashboard.jsx | 214 +++++++++---- .../src/pages/InterviewPrep/InterviewPrep.jsx | 60 ++-- .../src/pages/LandingPage.jsx | 2 +- .../src/pages/PracticePage.jsx | 2 +- .../src/pages/Review/ReviewPage.jsx | 2 +- .../interview-perp-ai/src/utils/apiPaths.js | 5 +- .../src/utils/axiosInstance.js | 17 +- frontend/interview-perp-ai/vite.config.js | 9 + netlify.toml | 11 + 24 files changed, 755 insertions(+), 134 deletions(-) create mode 100644 frontend/interview-perp-ai/.env.example create mode 100644 frontend/interview-perp-ai/src/components/Cards/QuestionCard_clean.jsx create mode 100644 frontend/interview-perp-ai/src/components/Cards/QuestionCard_enhanced.jsx create mode 100644 frontend/interview-perp-ai/src/components/Cards/QuestionCard_original.jsx create mode 100644 netlify.toml diff --git a/README.md b/README.md index 4c96bc7..b4d15eb 100644 --- a/README.md +++ b/README.md @@ -1 +1,126 @@ -Hey this is my readme file +
+ + +

Interview Prep AI 🚀

+

+ From Zero to One: The Story of a Personal AI Interview Coach +

+

+ ✨ View the Live Application ✨ +

+
+ +
+ +> **Note from the Developer:** This project was more than a technical exercise; it was a journey. It started with a simple idea—to build a better way to prepare for tech interviews—and evolved into a comprehensive platform. Every feature, every challenge, and every line of code represents a real story of problem-solving and growth. + +--- + +### 🎬 The Final Product in Action + +*(This is the perfect place for a high-quality GIF that walks through the user journey: creating a deck, a review session, and a practice session.)* + +![Interview Prep AI Showcase GIF](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGc5c2R0dGZ5a2ZqZTV3cjV2c2w5eW5ocnZtZzB6Z2w0bHk2aW5oZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/your-gif-id/giphy.gif) + +--- + +## ✨ What It Does: A Smarter Way to Prepare + +Interview Prep AI is an intelligent learning platform that transforms how tech professionals prepare for interviews. It moves beyond static flashcards to create a dynamic, feedback-driven ecosystem that helps you not only **know** the material but also **master** communicating it. + +- **🤖 Build Custom Decks in Seconds:** Create hyper-relevant interview decks for any role, or let the AI build one for you by simply pasting a link to a real job description. +- **🧠 Learn with Spaced Repetition:** A smart SRS algorithm schedules your reviews at the optimal time to ensure knowledge moves into your long-term memory. +- **🎙️ Practice Aloud, Get Real Feedback:** Use your voice to practice your answers and receive instant, AI-powered critiques on your content, clarity, and delivery. +- **📊 Track Your Growth:** A personalized dashboard visualizes your progress, showing you exactly where you're strong and where you need to focus. + +--- + +## 🛠️ The Technology Behind the Build + +This project is a full-stack MERN application, architected for a modern, scalable, and real-time user experience. + +![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) +![Node.js](https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white) +![Express.js](https://img.shields.io/badge/Express.js-000000?style=for-the-badge&logo=express&logoColor=white) +![MongoDB](https://img.shields.io/badge/MongoDB-47A248?style=for-the-badge&logo=mongodb&logoColor=white) +![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-06B6D4?style=for-the-badge&logo=tailwindcss&logoColor=white) +![Google Gemini](https://img.shields.io/badge/Google_Gemini-8E77D8?style=for-the-badge&logo=google-gemini&logoColor=white) +![Vercel](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white) +![Render](https://img.shields.io/badge/Render-46E3B7?style=for-the-badge&logo=render&logoColor=white) + +--- + +## 🧠 The Story Behind the Build: Challenges & Learnings + +A project's true value is in the problems solved along the way. This application's most advanced features are the direct result of tackling and overcoming significant engineering hurdles. + +### 1. The AI Reliability Challenge +- **The Ambition:** To provide consistently accurate, structured feedback from the AI. +- **The Roadblock:** The LLM would occasionally "bleed" conversational text around its JSON output, breaking the frontend. After multiple failed attempts to perfect the prompt, the application's core feature was at risk. +- **The Breakthrough:** Instead of trying to force the AI to be perfect, I built a resilient system around its imperfections. I engineered a robust parsing layer on the backend that intelligently finds and extracts the valid JSON from the raw text. +- **The Takeaway:** This was a profound lesson in defensive programming. **A senior engineer's job isn't just to make things work; it's to build systems that don't break when faced with the unexpected.** + +### 2. The SRS Algorithm Challenge +- **The Ambition:** To move beyond a simple flashcard app and implement a true Spaced Repetition System. +- **The Roadblock:** Translating the theoretical SM-2 algorithm into performant, stateful backend logic was far more complex than anticipated. My initial attempts were buggy and didn't correctly schedule the review intervals. +- **The Breakthrough:** I took a step back and dedicated time to studying open-source SRS implementations. This research allowed me to refactor my logic, resulting in a stable and effective scheduling engine. +- **The Takeaway:** This taught me the value of deep research before implementation. **Sometimes, the fastest way to solve a problem is to slow down and learn from the work of others.** + +This project is a testament to the engineering process: ambition, struggle, learning, and ultimately, resilience. + +--- + +## ⚙️ Getting Started + +To run this project locally, follow these steps: + +### Prerequisites +- Node.js (v18 or later) +- MongoDB instance (local or cloud-based) +- Google Gemini API Key + +### 1. Clone the Repository +```bash +git clone +cd interview-prep + +Install Dependencies +# Install backend dependencies +cd backend +npm install + +# Install frontend dependencies +cd ../frontend +npm install + +Configure Environment Variables +In the backend directory, create a .env file. +Add your MONGO_URI and GEMINI_API_KEY. + + Run the Application +# Run the backend server (from the backend folder) +npm run dev + +# Run the frontend development server (from the frontend folder) +npm start + +Roadmap(Future Advancements) + + AI-driven behavioral interview scoring + Role-based question banks (SDE, Analyst, Designer) + Video interview simulation + Resume analysis and feedback + Leaderboards and community features + +Contributing + +Contributions are welcome! +Fork this repo +Create your feature branch: git checkout -b feature/amazing-feature +Commit changes: git commit -m 'Add amazing feature' +Push to branch: git push origin feature/amazing-feature +Open a PR 🚀 + +Author: Shashank Chakraborty +Live Project: https://interview-prep-ai-kappa.vercel.app/ +Email: shashankchakraborty712005@gmail.com diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index fe0300e..1725eda 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -55,13 +55,13 @@ const loginUser = async (req, res) => { const user = await User.findOne({ email }); if (!user) { - return res.status(500).json({ message: "Invalid email or password" }); + return res.status(401).json({ message: "Invalid email or password" }); } // Compare password const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { - return res.status(500).json({ message: "Invalid email or password" }); + return res.status(401).json({ message: "Invalid email or password" }); } // Return user data with JWT @@ -82,7 +82,7 @@ const loginUser = async (req, res) => { // @access Private (Requires JWT) const getUserProfile = async (req, res) => { try { - const user = await User.findById(req.user.id).select("-password"); + const user = await User.findById(req.user._id).select("-password"); if (!user) { return res.status(404).json({ message: "User not found" }); } diff --git a/backend/controllers/questionController.js b/backend/controllers/questionController.js index abb95e3..53a45f9 100644 --- a/backend/controllers/questionController.js +++ b/backend/controllers/questionController.js @@ -182,7 +182,41 @@ const reviewQuestion = async (req, res) => { -// Removed updateQuestionRating - ratings are now only for sessions +// @desc Update question rating +// @route PUT /api/questions/:id/rating +// @access Private +const updateQuestionRating = async (req, res) => { + try { + const { id } = req.params; + const { userRating } = req.body; + const userId = req.user._id; + + const question = await Question.findById(id); + if (!question) { + return res.status(404).json({ message: "Question not found" }); + } + + // Verify the question belongs to the user + const session = await Session.findById(question.session); + if (session.user.toString() !== userId.toString()) { + return res.status(401).json({ message: "Not authorized" }); + } + + // Update the user rating + question.userRating = { + difficulty: userRating.difficulty || 3, + usefulness: userRating.usefulness || 3, + clarity: userRating.clarity || 3 + }; + + await question.save(); + res.status(200).json({ message: "Rating updated successfully", question }); + + } catch (error) { + console.error("Error updating question rating:", error); + res.status(500).json({ message: "Server Error" }); + } +}; // @desc Update question justification (admin only for now) // @route PUT /api/questions/:id/justification @@ -270,6 +304,7 @@ module.exports = { updateQuestionNote, toggleMasteredStatus, reviewQuestion, + updateQuestionRating, updateQuestionJustification, getFilteredQuestions, }; diff --git a/backend/middlewares/authMiddleware.js b/backend/middlewares/authMiddleware.js index 3f32b34..01949da 100644 --- a/backend/middlewares/authMiddleware.js +++ b/backend/middlewares/authMiddleware.js @@ -9,12 +9,20 @@ const protect = async (req, res, next) => { if (token && token.startsWith("Bearer")) { token = token.split(" ")[1]; // Extract token const decoded = jwt.verify(token, process.env.JWT_SECRET); - req.user = await User.findById(decoded.id).select("-password"); + const user = await User.findById(decoded.id).select("-password"); + + if (!user) { + return res.status(401).json({ message: "User not found" }); + } + + req.user = user; next(); } else { + console.log("No token provided or invalid format:", req.headers.authorization); res.status(401).json({ message: "Not authorized, no token" }); } } catch (error) { + console.error("Auth middleware error:", error); res.status(401).json({ message: "Token failed", error: error.message }); } }; diff --git a/backend/models/Question.js b/backend/models/Question.js index 4123683..fd2a8d7 100644 --- a/backend/models/Question.js +++ b/backend/models/Question.js @@ -29,6 +29,13 @@ const questionSchema = new mongoose.Schema({ interviewType: { type: String, enum: ['Technical', 'Behavioral', 'System Design', 'Coding', 'General'], default: 'Technical' } }, + // User rating system + userRating: { + difficulty: { type: Number, min: 1, max: 5, default: 3 }, + usefulness: { type: Number, min: 1, max: 5, default: 3 }, + clarity: { type: Number, min: 1, max: 5, default: 3 } + }, + // Additional metadata for filtering tags: [{ type: String }], difficulty: { type: String, enum: ['Easy', 'Medium', 'Hard'], default: 'Medium' }, diff --git a/backend/routes/questionRoutes.js b/backend/routes/questionRoutes.js index bc30043..ad73483 100644 --- a/backend/routes/questionRoutes.js +++ b/backend/routes/questionRoutes.js @@ -5,6 +5,7 @@ const { addQuestionsToSession, toggleMasteredStatus, reviewQuestion, + updateQuestionRating, updateQuestionJustification, getFilteredQuestions } = require('../controllers/questionController'); @@ -19,8 +20,9 @@ router.post('/:id/pin', protect, togglePinQuestion); router.put('/:id/note', protect, updateQuestionNote); router.put('/:id/master', protect, toggleMasteredStatus); router.put('/:id/review', protect, reviewQuestion); +router.put('/:id/rating', protect, updateQuestionRating); -// New routes for justifications and filtering (removed rating route) +// New routes for justifications and filtering router.put('/:id/justification', protect, updateQuestionJustification); router.get('/filter', protect, getFilteredQuestions); diff --git a/backend/server.js b/backend/server.js index c06556f..992b3c6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -21,9 +21,20 @@ const app = express(); // Middleware to handle CORS app.use( cors({ - origin: "*", - methods: ["GET", "POST", "PUT", "DELETE"], + origin: [ + "http://localhost:5173", + "http://localhost:5174", + "http://localhost:5175", + "http://localhost:5176", + "http://localhost:3000", + "http://127.0.0.1:5173", + "http://127.0.0.1:5174", + "http://127.0.0.1:5175", + "http://127.0.0.1:5176" + ], + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization"], + credentials: true }) ); @@ -32,6 +43,21 @@ connectDB() // Middleware app.use(express.json()); +// Debugging middleware - logs all requests but doesn't interfere with routing +app.use((req, res, next) => { + console.log('Request received for:', req.method, req.originalUrl); + next(); +}); + +// Serve uploads folder +app.use("/uploads", express.static(path.join(__dirname, "uploads"), {})); + +// Debug route to test API connectivity +app.get('/api/test', (req, res) => { + console.log('Test API endpoint hit'); + res.status(200).json({ message: 'API is working' }); +}); + // Routes app.use("/api/auth", authRoutes); app.use("/api/sessions", sessionRoutes); @@ -45,8 +71,11 @@ app.use("/api/ai/generate-questions", protect, generateInterviewQuestions); app.use('/api/feedback', feedbackRoutes); app.use("/api/ai", aiRoutes); -// Serve uploads folder -app.use("/uploads", express.static(path.join(__dirname, "uploads"), {})); +// This 404 handler should only run after all other routes have been checked +app.use((req, res) => { + console.log('No route found for:', req.originalUrl); + res.status(404).json({ message: 'Route not found', path: req.originalUrl }); +}); // Start Server const PORT = process.env.PORT || 8000; diff --git a/frontend/interview-perp-ai/.env.example b/frontend/interview-perp-ai/.env.example new file mode 100644 index 0000000..a70bb1c --- /dev/null +++ b/frontend/interview-perp-ai/.env.example @@ -0,0 +1,5 @@ +# API Configuration +VITE_API_BASE_URL=http://localhost:8000 + +# Development Configuration +VITE_NODE_ENV=development diff --git a/frontend/interview-perp-ai/src/App.jsx b/frontend/interview-perp-ai/src/App.jsx index 9447cba..a1646e8 100644 --- a/frontend/interview-perp-ai/src/App.jsx +++ b/frontend/interview-perp-ai/src/App.jsx @@ -13,7 +13,7 @@ import Dashboard from './pages/Home/Dashboard'; import InterviewPrep from './pages/InterviewPrep/InterviewPrep'; import UserProvider from './context/userContext'; import ReviewPage from './pages/Review/ReviewPage'; -import SignUp from './pages/Auth/Signup'; +import SignUp from './pages/Auth/SignUp.jsx'; import Login from './pages/Auth/Login'; import AnalyticsDashboard from './pages/Analytics/AnalyticsDashboard'; import PracticePage from './pages/PracticePage'; diff --git a/frontend/interview-perp-ai/src/components/Cards/QuestionCard_clean.jsx b/frontend/interview-perp-ai/src/components/Cards/QuestionCard_clean.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/interview-perp-ai/src/components/Cards/QuestionCard_enhanced.jsx b/frontend/interview-perp-ai/src/components/Cards/QuestionCard_enhanced.jsx new file mode 100644 index 0000000..ee603e3 --- /dev/null +++ b/frontend/interview-perp-ai/src/components/Cards/QuestionCard_enhanced.jsx @@ -0,0 +1,296 @@ +import React, { useEffect, useRef, useState } from "react"; +import { LuChevronDown, LuPin, LuPinOff, LuMessageSquarePlus, LuCheck, LuStar } from "react-icons/lu"; +import AIResponsePreview from "../../pages/InterviewPrep/components/AIResponsePreview"; + +const QuestionCard = ({ + questionId, + question, + answer, + userNote, + onAskFollowUp, + isMastered, + onToggleMastered, + isPinned, + onTogglePin, + onSaveNote, + // Enhanced props + justification, + userRating, + onUpdateRating, + difficulty, + tags, + category, +}) => { + const [isExpanded, setIsExpanded] = useState(false); + const [height, setHeight] = useState(0); + const contentRef = useRef(null); + const [note, setNote] = useState(userNote || ""); + const [showRatingModal, setShowRatingModal] = useState(false); + const [tempRating, setTempRating] = useState(userRating || { difficulty: 3, usefulness: 3, clarity: 3 }); + + useEffect(() => { + setNote(userNote || ""); + }, [userNote]); + + useEffect(() => { + if (isExpanded && contentRef.current) { + const contentHeight = contentRef.current.scrollHeight; + setHeight(contentHeight + 20); + } else { + setHeight(0); + } + }, [isExpanded, answer, userNote]); + + const toggleExpand = () => { + setIsExpanded(!isExpanded); + }; + + const handleRatingSubmit = () => { + onUpdateRating(questionId, tempRating); + setShowRatingModal(false); + }; + + const getDifficultyColor = (diff) => { + switch (diff) { + case 'Easy': return 'bg-green-100 text-green-800 border-green-200'; + case 'Medium': return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + case 'Hard': return 'bg-red-100 text-red-800 border-red-200'; + default: return 'bg-gray-100 text-gray-800 border-gray-200'; + } + }; + + const getProbabilityColor = (prob) => { + switch (prob) { + case 'Very High': return 'bg-red-100 text-red-800 border-red-200'; + case 'High': return 'bg-orange-100 text-orange-800 border-orange-200'; + case 'Medium': return 'bg-blue-100 text-blue-800 border-blue-200'; + case 'Low': return 'bg-gray-100 text-gray-800 border-gray-200'; + default: return 'bg-gray-100 text-gray-800 border-gray-200'; + } + }; + + const getInterviewTypeIcon = (type) => { + switch (type) { + case 'Technical': return '💻'; + case 'Behavioral': return '🤝'; + case 'System Design': return '🏗️'; + case 'Coding': return '⌨️'; + default: return '📋'; + } + }; + + const avgRating = userRating ? (userRating.difficulty + userRating.usefulness + userRating.clarity) / 3 : 0; + + return ( +
+ {/* Header with badges */} +
+
+ {difficulty && ( + + {difficulty} + + )} + {justification?.interviewType && ( + + {getInterviewTypeIcon(justification.interviewType)} {justification.interviewType} + + )} + {justification?.probability && ( + + 🎯 {justification.probability} + + )} + {category && ( + + {category} + + )} +
+
+ + {/* Main content */} +
+
+
+ Q +
+

+ {question} +

+ + {/* Justification preview */} + {justification?.reasoning && ( +

+ 💡 {justification.reasoning.substring(0, 100)}... +

+ )} + + {/* Rating display */} + {userRating && ( +
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ + ({avgRating.toFixed(1)}) + +
+ )} +
+
+ +
+ {/* Action buttons */} +
+ + + + + + + +
+ + +
+
+ + {/* Expanded content */} +
+
+
+ +
+ + {/* Full justification */} + {justification?.reasoning && ( +
+

Why this question matters

+

{justification.reasoning}

+ {justification.commonCompanies && justification.commonCompanies.length > 0 && ( +
+ Common at: + + {justification.commonCompanies.join(', ')} + +
+ )} +
+ )} + + {/* Notes section */} +
+

My Notes

+ + -
@@ -243,7 +138,6 @@ const StudyBuddyChat = ({ userId }) => {
@@ -252,33 +146,6 @@ const StudyBuddyChat = ({ userId }) => { {/* Messages */}
{messages.map((msg) => ( -<<<<<<< HEAD -
-
- {msg.sender === "buddy" ? ( - - ) : ( - - )} -
- -
-
- {msg.message - .split("\n") - .map((line, i) => ( -
{line}
- ))} -
- -
- {new Date( - msg.timestamp - ).toLocaleTimeString([], { -=======
{msg.sender === "buddy" ? : } @@ -291,7 +158,6 @@ const StudyBuddyChat = ({ userId }) => {
{new Date(msg.timestamp).toLocaleTimeString([], { ->>>>>>> 991354d8d4d6c6c0980bbacfa805324e6c2f712f hour: "2-digit", minute: "2-digit", })} @@ -300,11 +166,7 @@ const StudyBuddyChat = ({ userId }) => {
))} -<<<<<<< HEAD - {/* Typing indicator */} -======= {/* Typing Indicator */} ->>>>>>> 991354d8d4d6c6c0980bbacfa805324e6c2f712f {isTyping && (
@@ -323,20 +185,6 @@ const StudyBuddyChat = ({ userId }) => {
-<<<<<<< HEAD - {/* Input Section */} -
- - -======= {/* Input */}
+ -
@@ -169,31 +153,20 @@ const StudyBuddyChat = ({ userId }) => { {/* Messages */}
{messages.map((msg) => ( -
+
- {msg.sender === "buddy" ? ( - - ) : ( - - )} + {msg.sender === "buddy" ? : }
- {msg.message - .split("\n") - .map((line, i) => ( -
{line}
- ))} + {msg.message.split("\n").map((line, i) => ( +
{line}
+ ))}
- {new Date( - msg.timestamp - ).toLocaleTimeString([], { + {new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", })} @@ -221,13 +194,11 @@ const StudyBuddyChat = ({ userId }) => {
- {/* Input Section */} + {/* Input box */}
- - -
+ {isTyping && ( +
+
+ +
+
+
+ + + +
+
)} - - ); + +
+
+ + {/* Input */} +
+ + + +
+
+ )} + + ); }; export default StudyBuddyChat; From e02e22494958e18b13e171422f2ec09e8adf2840 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 20 Nov 2025 15:40:33 +0530 Subject: [PATCH 187/198] Feat: Added Comprehensive Memory to the StudyBuddy ChatBot --- .../study_buddy/rag/generation/generator.py | 32 ++++++++--- ai-training/study_buddy/rag/rag_pipeline.py | 32 ++++------- .../components/StudyBuddy/StudyBuddyChat.jsx | 53 +++++++++++++++++++ 3 files changed, 88 insertions(+), 29 deletions(-) diff --git a/ai-training/study_buddy/rag/generation/generator.py b/ai-training/study_buddy/rag/generation/generator.py index 9347bfd..be339b6 100644 --- a/ai-training/study_buddy/rag/generation/generator.py +++ b/ai-training/study_buddy/rag/generation/generator.py @@ -38,14 +38,16 @@ def generate_response( query: str, retrieved_docs: List[Dict[str, Any]], user_context: Optional[Dict[str, Any]] = None, + conversation_memory: Optional[List[Dict[str, Any]]] = None, use_fast_model: bool = False ) -> Dict[str, Any]: - """Generate response using retrieved documents and user context. + """Generate response using retrieved documents, user context, and conversation memory. Args: query: User query retrieved_docs: Retrieved documents from RAG user_context: User context information + conversation_memory: Previous conversation messages for context use_fast_model: Whether to use fast model for quick responses Returns: @@ -55,8 +57,8 @@ def generate_response( # Build context from retrieved documents context = self._build_context(retrieved_docs) - # Create prompt - prompt = self._create_prompt(query, context, user_context) + # Create prompt with conversation memory + prompt = self._create_prompt(query, context, user_context, conversation_memory) # Choose model based on complexity model = self.fast_model_instance if use_fast_model else self.model @@ -159,23 +161,39 @@ def _build_context(self, retrieved_docs: List[Dict[str, Any]]) -> str: return "\n\n".join(context_parts) - def _create_prompt(self, query: str, context: str, user_context: Optional[Dict[str, Any]]) -> str: - """Create prompt for response generation. + def _create_prompt(self, query: str, context: str, user_context: Optional[Dict[str, Any]], conversation_memory: Optional[List[Dict[str, Any]]] = None) -> str: + """Create prompt for response generation with conversation history. Args: query: User query context: Retrieved context user_context: User context information + conversation_memory: Previous conversation messages Returns: Generated prompt """ + # Build conversation history section + history_section = "" + if conversation_memory and len(conversation_memory) > 0: + history_section = "\n\nRECENT CONVERSATION HISTORY:\n" + # Use last 10 messages for context + recent_messages = conversation_memory[-10:] + for msg in recent_messages: + role = msg.get('role', 'user') + text = msg.get('text', '') + if role == 'user': + history_section += f"User: {text}\n" + elif role == 'assistant': + history_section += f"Assistant: {text}\n" + history_section += "\n" + # Base prompt template prompt_template = """You are a Smart Study Buddy AI, a knowledgeable companion that helps users with interview preparation. You provide accurate, helpful information while being encouraging and supportive. CONTEXT FROM KNOWLEDGE BASE: {context} - +{history} USER INFORMATION: {user_info} @@ -183,6 +201,7 @@ def _create_prompt(self, query: str, context: str, user_context: Optional[Dict[s INSTRUCTIONS: - FIRST: Answer the user's question directly and accurately using the context provided +- If the user is referencing previous conversation, use the conversation history to understand context - Provide clear, specific explanations with examples when helpful - Use the context information to give comprehensive, factual answers - THEN: Add encouragement and reference user progress when relevant @@ -203,6 +222,7 @@ def _create_prompt(self, query: str, context: str, user_context: Optional[Dict[s return prompt_template.format( context=context, + history=history_section, user_info=user_info, query=query ) diff --git a/ai-training/study_buddy/rag/rag_pipeline.py b/ai-training/study_buddy/rag/rag_pipeline.py index 57e01be..3096c0a 100644 --- a/ai-training/study_buddy/rag/rag_pipeline.py +++ b/ai-training/study_buddy/rag/rag_pipeline.py @@ -92,11 +92,11 @@ def add_documents(self, documents: List[Dict[str, Any]]): raise def chat(self, query: str, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """Main chat interface for the study buddy. + """Main chat interface for the study buddy with persistent memory support. Args: query: User query - user_context: User context information + user_context: User context information (includes persistent memory) Returns: Chat response with metadata @@ -105,12 +105,11 @@ def chat(self, query: str, user_context: Optional[Dict[str, Any]] = None) -> Dic return self._create_error_response("RAG pipeline not ready") try: - # Add to conversation history - self.conversation_history.append({ - 'type': 'user', - 'content': query, - 'timestamp': self._get_timestamp() - }) + # Extract conversation memory from user context (persistent storage) + conversation_memory = [] + if user_context and 'memory' in user_context: + conversation_memory = user_context['memory'] + logger.info(f"Using {len(conversation_memory)} messages from persistent memory") # Retrieve relevant documents retrieved_docs = self.retriever.retrieve_with_context( @@ -122,28 +121,15 @@ def chat(self, query: str, user_context: Optional[Dict[str, Any]] = None) -> Dic # Determine if we should use fast model use_fast_model = self._should_use_fast_model(query) - # Generate response + # Generate response WITH conversation memory response = self.generator.generate_response( query=query, retrieved_docs=retrieved_docs, user_context=user_context, + conversation_memory=conversation_memory, use_fast_model=use_fast_model ) - # Add to conversation history - self.conversation_history.append({ - 'type': 'assistant', - 'content': response['response'], - 'timestamp': self._get_timestamp(), - 'metadata': { - 'retrieved_docs': len(retrieved_docs), - 'model_used': response['model_used'] - } - }) - - # Limit conversation history - self._trim_conversation_history() - return response except Exception as e: diff --git a/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.jsx b/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.jsx index d501b5e..a18034a 100644 --- a/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.jsx +++ b/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.jsx @@ -17,6 +17,7 @@ const StudyBuddyChat = ({ userId }) => { ]); const [inputMessage, setInputMessage] = useState(""); const [isTyping, setIsTyping] = useState(false); + const [isLoadingHistory, setIsLoadingHistory] = useState(false); const messagesEndRef = useRef(null); @@ -37,6 +38,45 @@ const StudyBuddyChat = ({ userId }) => { }); }, []); + // Load conversation history on mount + useEffect(() => { + const loadConversationHistory = async () => { + if (!userId || userId === "anonymous") { + console.log("Skipping history load for anonymous user"); + return; + } + + try { + setIsLoadingHistory(true); + const res = await fetch(`${BASE_URL}/api/ai/memory/${userId}`); + const data = await res.json(); + + if (data.success && data.memory && data.memory.length > 0) { + console.log(`📚 Loaded ${data.memory.length} previous messages`); + + // Convert backend memory format to UI message format + const historicalMessages = data.memory.map((entry, idx) => ({ + id: `history-${idx}-${Date.now()}`, + sender: entry.role === "user" ? "user" : "buddy", + message: entry.text, + timestamp: entry.ts || new Date().toISOString(), + })); + + // Replace messages with welcome + history + setMessages([messages[0], ...historicalMessages]); + } else { + console.log("No previous conversation history found"); + } + } catch (err) { + console.error("Failed to load conversation history:", err); + } finally { + setIsLoadingHistory(false); + } + }; + + loadConversationHistory(); + }, [userId]); + const checkAIHealth = async () => { try { const res = await fetch(`${BASE_URL}/api/ai/health`); @@ -154,6 +194,19 @@ const StudyBuddyChat = ({ userId }) => { {/* Messages */}
+ {isLoadingHistory && ( +
+
+ +
+
+
+ Loading previous conversation... +
+
+
+ )} + {messages.map((msg) => (
From a3422f34da3970e46095b0e27a6654a65110b106 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 20 Nov 2025 19:06:23 +0530 Subject: [PATCH 188/198] Feat: Added more Training Data for the StudyBuddy ChatBot --- ai-training/study_buddy/api/chat_api.py | 15 +++ .../data/generative_ai_llm_mastery.txt | 127 ++++++++++++++++++ .../study_buddy/data/process_new_documents.py | 11 +- 3 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 ai-training/study_buddy/data/generative_ai_llm_mastery.txt diff --git a/ai-training/study_buddy/api/chat_api.py b/ai-training/study_buddy/api/chat_api.py index 33909fa..56860e8 100644 --- a/ai-training/study_buddy/api/chat_api.py +++ b/ai-training/study_buddy/api/chat_api.py @@ -286,6 +286,21 @@ async def _load_initial_data(self): data = json.load(open(reminders)) documents.extend(self._convert_reminders_data(data)) + # enhanced training data + enhanced_data = data_dir / "processed" / "enhanced_training_data.json" + if enhanced_data.exists(): + try: + data = json.load(open(enhanced_data, encoding='utf-8')) + # Map chunk_id to id for vector store + for item in data: + if 'id' not in item and 'chunk_id' in item: + item['id'] = item['chunk_id'] + + documents.extend(data) + logger.info(f"Loaded {len(data)} enhanced training documents") + except Exception as e: + logger.error(f"Error loading enhanced data: {e}") + if documents: self.rag_pipeline.add_documents(documents) logger.info(f"Loaded {len(documents)} documents into RAG") diff --git a/ai-training/study_buddy/data/generative_ai_llm_mastery.txt b/ai-training/study_buddy/data/generative_ai_llm_mastery.txt new file mode 100644 index 0000000..20047fe --- /dev/null +++ b/ai-training/study_buddy/data/generative_ai_llm_mastery.txt @@ -0,0 +1,127 @@ +# Generative AI and Large Language Models (LLM) Mastery + +## Transformer Architecture + +### Core Concepts +- **Attention Mechanism**: Allows the model to weigh the importance of different words in a sentence regardless of their distance. +- **Self-Attention**: Mechanism relating different positions of a single sequence to compute a representation of the sequence. +- **Multi-Head Attention**: Running multiple attention mechanisms in parallel to capture different types of relationships. +- **Positional Encodings**: Injected information about the relative or absolute position of tokens in the sequence. +- **Encoder-Decoder**: + - **Encoder**: Processes the input sequence (e.g., BERT). + - **Decoder**: Generates the output sequence (e.g., GPT). + - **Encoder-Decoder**: Uses both (e.g., T5, BART). + +### Key Models +- **BERT (Bidirectional Encoder Representations from Transformers)**: Masked language modeling, good for understanding/classification. +- **GPT (Generative Pre-trained Transformer)**: Autoregressive language modeling, good for generation. +- **T5 (Text-to-Text Transfer Transformer)**: Treats every NLP problem as a text-to-text task. +- **Llama**: Open-source efficient foundation models by Meta. + +## LLM Training Pipeline + +### 1. Pre-training +- **Objective**: Learn statistical patterns of language from massive datasets. +- **Task**: Next-token prediction (Causal LM) or Masked token prediction (Masked LM). +- **Compute**: Requires thousands of GPUs and months of time. + +### 2. Supervised Fine-Tuning (SFT) +- **Objective**: Adapt the base model to follow instructions or specific tasks. +- **Data**: High-quality instruction-response pairs (e.g., "Summarize this article" -> "Summary..."). +- **Techniques**: Full fine-tuning vs. PEFT (Parameter-Efficient Fine-Tuning). + +### 3. RLHF (Reinforcement Learning from Human Feedback) +- **Reward Model**: Trained to predict human preference between two model outputs. +- **PPO (Proximal Policy Optimization)**: Optimizes the policy (LLM) to maximize reward while staying close to the initial model. +- **DPO (Direct Preference Optimization)**: Optimizes policy directly from preferences without a separate reward model. + +## Retrieval Augmented Generation (RAG) + +### Architecture +- **Retriever**: Finds relevant documents from a knowledge base given a query. +- **Generator**: Uses the retrieved context + query to generate an answer. +- **Vector Database**: Stores embeddings of documents for fast similarity search (e.g., Pinecone, Chroma, Milvus). + +### RAG Techniques +- **Chunking**: Splitting documents into smaller, meaningful pieces (Fixed-size, Semantic, Recursive). +- **Hybrid Search**: Combining Keyword search (BM25) with Semantic search (Vector/Cosine Similarity). +- **Re-ranking**: Using a cross-encoder to re-score the top-k retrieved documents for better precision. +- **Query Expansion**: Generating synonyms or related questions to improve retrieval recall. + +## Prompt Engineering + +### Strategies +- **Zero-shot**: Asking the model to perform a task without examples. +- **Few-shot**: Providing 1-3 examples of input-output pairs before the actual task. +- **Chain-of-Thought (CoT)**: Encouraging the model to "think step-by-step" to improve reasoning. +- **ReAct**: Combining Reasoning and Acting (using tools) to solve complex tasks. +- **System Prompts**: Setting the persona, constraints, and tone of the AI. + +## Efficient Deployment + +### Quantization +- **FP16/BF16**: Half-precision training/inference. +- **INT8/INT4**: Reducing weights to 8-bit or 4-bit integers to save memory with minimal accuracy loss. +- **GPTQ / AWQ**: Advanced quantization techniques for Transformers. + +### Optimization Techniques +- **KV Cache**: Caching Key and Value matrices during generation to avoid re-computation. +- **Flash Attention**: IO-aware exact attention algorithm that speeds up training and inference. +- **Speculative Decoding**: Using a small model to draft tokens and a large model to verify them. + +## Interview Questions + +### Conceptual +1. **Explain the difference between Encoder-only, Decoder-only, and Encoder-Decoder architectures.** + - Encoder-only (BERT) is bi-directional, good for understanding. Decoder-only (GPT) is uni-directional, good for generation. Encoder-Decoder (T5) combines both for translation/summarization. + +2. **What is the "Context Window" and why is it a limitation?** + - It's the maximum number of tokens the model can process at once. Quadratic complexity of attention ($O(N^2)$) makes large windows computationally expensive. + +3. **How does RAG solve the hallucination problem?** + - By grounding the generation in retrieved, factual context rather than relying solely on the model's internal parametric memory. + +4. **What is LoRA (Low-Rank Adaptation)?** + - A PEFT technique that freezes pre-trained weights and injects trainable low-rank decomposition matrices, reducing trainable parameters by 99%. + +### Practical +1. **How would you build a Q&A bot for a private company documentation?** + - Ingest docs -> Chunking -> Embeddings -> Vector DB. + - Query -> Embed -> Retrieve Top-K -> LLM Generation with Context. + +2. **Compare Vector Search vs. Keyword Search.** + - Vector search captures semantic meaning (e.g., "canine" matches "dog"). Keyword search matches exact tokens. Hybrid is often best. + +3. **How do you evaluate an LLM application?** + - **RAGAS**: Metrics for RAG (Faithfulness, Answer Relevance, Context Precision). + - **LLM-as-a-Judge**: Using a stronger model (GPT-4) to grade responses. + - **Human Eval**: Gold standard but expensive. + +### Code Snippet: Simple RAG with LangChain +```python +from langchain.vectorstores import Chroma +from langchain.embeddings import OpenAIEmbeddings +from langchain.chat_models import ChatOpenAI +from langchain.chains import RetrievalQA + +# 1. Setup Vector DB +embeddings = OpenAIEmbeddings() +vectordb = Chroma(persist_directory="./db", embedding_function=embeddings) + +# 2. Setup Retriever +retriever = vectordb.as_retriever(search_kwargs={"k": 3}) + +# 3. Setup LLM +llm = ChatOpenAI(model_name="gpt-3.5-turbo") + +# 4. Create Chain +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + chain_type="stuff", + retriever=retriever +) + +# 5. Ask Question +response = qa_chain.run("How does the attention mechanism work?") +print(response) +``` diff --git a/ai-training/study_buddy/data/process_new_documents.py b/ai-training/study_buddy/data/process_new_documents.py index bbaebd4..f358458 100644 --- a/ai-training/study_buddy/data/process_new_documents.py +++ b/ai-training/study_buddy/data/process_new_documents.py @@ -27,15 +27,8 @@ def read_new_documents(self) -> List[Dict[str, Any]]: new_documents = [] # Define new document files - new_doc_files = [ - "advanced_algorithms.txt", - "system_design_interviews.txt", - "behavioral_interview_mastery.txt", - "coding_interview_patterns.txt", - "company_specific_guides.txt", - "resume_optimization.txt", - "negotiation_strategies.txt" - ] + # Dynamically find all .txt files in the directory + new_doc_files = [f.name for f in self.new_docs_dir.glob("*.txt")] for doc_file in new_doc_files: doc_path = self.new_docs_dir / doc_file From ba3f66c26baa31f5c3eb423245d0c42c56dff7a8 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 20 Nov 2025 20:06:36 +0530 Subject: [PATCH 189/198] Fix: #48 The Response of StudyBuddy --- .../components/StudyBuddy/StudyBuddyChat.css | 91 +++++++++++++++++++ .../components/StudyBuddy/StudyBuddyChat.jsx | 42 +++++++-- 2 files changed, 124 insertions(+), 9 deletions(-) diff --git a/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.css b/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.css index 028cc54..72ce163 100644 --- a/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.css +++ b/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.css @@ -342,3 +342,94 @@ transform: scale(1); } } + +/* Markdown Content Styles */ +.markdown-content { + font-size: 14px; + line-height: 1.6; +} + +.markdown-content h1, +.markdown-content h2, +.markdown-content h3, +.markdown-content h4 { + margin-top: 16px; + margin-bottom: 8px; + font-weight: 600; + color: #2d3748; +} + +.markdown-content h1 { font-size: 1.4em; } +.markdown-content h2 { font-size: 1.2em; } +.markdown-content h3 { font-size: 1.1em; } + +.markdown-content p { + margin-bottom: 12px; +} + +.markdown-content ul, +.markdown-content ol { + margin-bottom: 12px; + padding-left: 24px; +} + +.markdown-content li { + margin-bottom: 4px; +} + +.markdown-content strong { + font-weight: 600; + color: #2d3748; +} + +.markdown-content code { + background: #edf2f7; + padding: 2px 4px; + border-radius: 4px; + font-family: monospace; + font-size: 0.9em; + color: #4a5568; +} + +.markdown-content pre { + background: #2d3748; + color: #e2e8f0; + padding: 12px; + border-radius: 8px; + overflow-x: auto; + margin-bottom: 12px; +} + +.markdown-content pre code { + background: transparent; + color: inherit; + padding: 0; +} + +.markdown-content blockquote { + border-left: 4px solid #cbd5e0; + padding-left: 12px; + margin-left: 0; + margin-bottom: 12px; + color: #718096; + font-style: italic; +} + +.markdown-content a { + color: #667eea; + text-decoration: none; +} + +.markdown-content a:hover { + text-decoration: underline; +} + +/* Remove top margin from first element */ +.markdown-content > *:first-child { + margin-top: 0; +} + +/* Remove bottom margin from last element */ +.markdown-content > *:last-child { + margin-bottom: 0; +} diff --git a/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.jsx b/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.jsx index a18034a..19a5b8b 100644 --- a/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.jsx +++ b/frontend/interview-perp-ai/src/components/StudyBuddy/StudyBuddyChat.jsx @@ -1,6 +1,8 @@ // ./src/components/StudyBuddyChat/StudyBuddyChat.jsx import React, { useState, useEffect, useRef } from "react"; import { MessageCircle, Send, Bot, User, X, Minimize2 } from "lucide-react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import { BASE_URL } from "../../utils/apiPaths"; import "./StudyBuddyChat.css"; @@ -20,12 +22,23 @@ const StudyBuddyChat = ({ userId }) => { const [isLoadingHistory, setIsLoadingHistory] = useState(false); const messagesEndRef = useRef(null); + const lastMessageRef = useRef(null); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; + // Smart scrolling logic + useEffect(() => { + if (messages.length === 0) return; + + const lastMsg = messages[messages.length - 1]; - useEffect(scrollToBottom, [messages]); + // If the last message is from the bot, scroll to its start so the user sees the beginning + if (lastMsg.sender === "buddy" && lastMessageRef.current) { + lastMessageRef.current.scrollIntoView({ behavior: "smooth", block: "start" }); + } + // If it's from the user, scroll to the bottom to see the input area/response + else if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [messages]); // Health check on mount useEffect(() => { @@ -207,8 +220,12 @@ const StudyBuddyChat = ({ userId }) => {
)} - {messages.map((msg) => ( -
+ {messages.map((msg, index) => ( +
{msg.sender === "buddy" ? ( @@ -219,9 +236,16 @@ const StudyBuddyChat = ({ userId }) => {
- {msg.message.split("\n").map((line, i) => ( -
{line}
- ))} + {msg.sender === "buddy" ? ( + + {msg.message} + + ) : ( + msg.message + )}
From 69da33c823a8aa68b1f3ae3976dbc2baa873b40b Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 20 Nov 2025 20:14:40 +0530 Subject: [PATCH 190/198] Fix: Issue #46. Profile Pic --- .../src/components/Cards/ProfileInfoCard.jsx | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/frontend/interview-perp-ai/src/components/Cards/ProfileInfoCard.jsx b/frontend/interview-perp-ai/src/components/Cards/ProfileInfoCard.jsx index 4aa7bb8..66fefb4 100644 --- a/frontend/interview-perp-ai/src/components/Cards/ProfileInfoCard.jsx +++ b/frontend/interview-perp-ai/src/components/Cards/ProfileInfoCard.jsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; // Assuming useContext is needed +import React, { useContext, useState } from 'react'; // Assuming useContext is needed import { UserContext } from '../../context/userContext'; // Assuming path import { useNavigate } from 'react-router-dom'; // Assuming react-router-dom @@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom'; // Assuming react-router-dom const ProfileInfoCard = () => { const { user, clearUser } = useContext(UserContext); const navigate = useNavigate(); + const [imgError, setImgError] = useState(false); const handleLogout = () => { localStorage.clear(); @@ -13,28 +14,36 @@ const ProfileInfoCard = () => { navigate("/"); }; + const getProfileImage = () => { + if (!user.profileImageUrl || imgError) { + return `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name || 'User')}&background=random&color=fff`; + } + return user.profileImageUrl; + }; + return ( - user&&( -
- + {user.name} setImgError(true)} + className='w-10 h-10 rounded-full object-cover border-2 border-gray-200 dark:border-gray-700' /> -
-
- {user.name || ""} -
- -
-
+
+
+ {user.name || "User"} +
+ +
+
) ); }; From c88def8b0693b910598d42530f86988341ccd478 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 20 Nov 2025 23:18:07 +0530 Subject: [PATCH 191/198] Fix: #49 Fixed --- .../src/pages/LandingPage.jsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/interview-perp-ai/src/pages/LandingPage.jsx b/frontend/interview-perp-ai/src/pages/LandingPage.jsx index 023ad1b..73cff8a 100644 --- a/frontend/interview-perp-ai/src/pages/LandingPage.jsx +++ b/frontend/interview-perp-ai/src/pages/LandingPage.jsx @@ -12,7 +12,7 @@ import ProfileInfoCard from '../components/Cards/ProfileInfoCard'; const LandingPage = () => { - const {user} = useContext(UserContext); + const { user } = useContext(UserContext); const navigate = useNavigate(); const [openAuthModal, setOpenAuthModal] = useState(false); @@ -20,9 +20,9 @@ const LandingPage = () => { // Made the "Get Started" button open the modal const handleCTA = () => { - if(!user){ - setOpenAuthModal(true); - } else{ + if (!user) { + setOpenAuthModal(true); + } else { navigate("/dashboard"); } }; @@ -44,7 +44,7 @@ const LandingPage = () => { ) : (
-
+
Hero Image
From 948cdff1e523fa4c4d76e41a29d1855851c7b351 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 20 Nov 2025 23:25:32 +0530 Subject: [PATCH 192/198] Fix: Mobile Responsiveness --- .../interview-perp-ai/src/pages/LandingPage.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/interview-perp-ai/src/pages/LandingPage.jsx b/frontend/interview-perp-ai/src/pages/LandingPage.jsx index 73cff8a..35aa36e 100644 --- a/frontend/interview-perp-ai/src/pages/LandingPage.jsx +++ b/frontend/interview-perp-ai/src/pages/LandingPage.jsx @@ -28,14 +28,14 @@ const LandingPage = () => { }; return ( -
-
-
+
+
+
{/* Header */} {/* Corrected: "items-center" and "mb-16" */} -
-
+
+
Interview Prep AI
{/* Auth button */} @@ -43,7 +43,7 @@ const LandingPage = () => { ) : (
-

+

Ace Interviews with
AI-Powered From 9ff6b80d14a4131a15b1b567e532d14387de8267 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 21 Nov 2025 14:29:21 +0530 Subject: [PATCH 193/198] Fix: #50 Salary Negotiation Component --- .../salaryNegotiationController.js | 364 +++++++++--------- .../NegotiationSimulator.jsx | 252 ++++++------ 2 files changed, 313 insertions(+), 303 deletions(-) diff --git a/backend/controllers/salaryNegotiationController.js b/backend/controllers/salaryNegotiationController.js index d181554..006ee95 100644 --- a/backend/controllers/salaryNegotiationController.js +++ b/backend/controllers/salaryNegotiationController.js @@ -102,25 +102,25 @@ const recruiterPersonalities = { exports.startNegotiation = async (req, res) => { try { const { scenario, role, level, location, recruiterPersonality, communicationMode, companyName } = req.body; - + // Get market data for the role const market = marketData[role]?.[level]?.[location] || marketData['Software Engineer']['mid']['Remote']; - + // Generate initial offer (typically between p25 and p50) const baseOffer = Math.round(market.p25 + (market.p50 - market.p25) * 0.3); const equity = scenario === 'startup' ? Math.round(baseOffer * 0.15) : Math.round(baseOffer * 0.05); const signingBonus = scenario === 'faang' ? Math.round(baseOffer * 0.15) : Math.round(baseOffer * 0.05); - + // Notice period specific values (unique to Indian market) const noticePeriodDays = scenario === 'notice-period-buyout' ? 90 : 0; const buyoutAmount = scenario === 'notice-period-buyout' ? Math.round(baseOffer * 3 / 12) : 0; // 3 months salary - + // Generate recruiter details for email mode const recruiterNames = ['Priya Sharma', 'Rahul Verma', 'Anjali Patel', 'Vikram Singh', 'Neha Gupta']; const recruiterName = recruiterNames[Math.floor(Math.random() * recruiterNames.length)]; const company = companyName || 'TechCorp India'; const recruiterEmail = `${recruiterName.toLowerCase().replace(' ', '.')}@${company.toLowerCase().replace(' ', '')}.com`; - + const negotiation = new SalaryNegotiation({ user: req.user._id, scenario, @@ -143,7 +143,7 @@ exports.startNegotiation = async (req, res) => { }, marketData: market }); - + // Generate opening message from recruiter const personality = recruiterPersonalities[negotiation.recruiterPersonality]; const openingMessage = await generateRecruiterMessage( @@ -152,14 +152,14 @@ exports.startNegotiation = async (req, res) => { personality, null ); - + // Add email metadata if in email mode const messageData = { sender: 'recruiter', message: openingMessage, offer: negotiation.initialOffer }; - + if (negotiation.communicationMode === 'email') { messageData.emailMetadata = { subject: `Offer for ${negotiation.role} position at ${negotiation.companyName}`, @@ -168,11 +168,11 @@ exports.startNegotiation = async (req, res) => { cc: [] }; } - + negotiation.conversationHistory.push(messageData); - + await negotiation.save(); - + res.status(201).json({ success: true, negotiation: { @@ -202,23 +202,23 @@ exports.sendMessage = async (req, res) => { try { const { negotiationId } = req.params; const { message, counterOffer } = req.body; - + const negotiation = await SalaryNegotiation.findOne({ _id: negotiationId, user: req.user._id }); - + if (!negotiation) { return res.status(404).json({ success: false, message: 'Negotiation not found' }); } - + // Add user message to history const userMessageData = { sender: 'user', message, offer: counterOffer }; - + if (negotiation.communicationMode === 'email') { userMessageData.emailMetadata = { subject: `Re: Offer for ${negotiation.role} position at ${negotiation.companyName}`, @@ -227,32 +227,33 @@ exports.sendMessage = async (req, res) => { cc: [] }; } - + negotiation.conversationHistory.push(userMessageData); - + negotiation.negotiationRounds += 1; - + // Analyze user's message for tactics and mistakes const analysis = analyzeUserMessage(message, counterOffer, negotiation); - + // Generate recruiter response using AI + // Determine if recruiter makes a counter-offer FIRST so the AI knows what to say const personality = recruiterPersonalities[negotiation.recruiterPersonality]; + const newOffer = generateCounterOffer(negotiation, counterOffer, analysis, personality); + + // Generate recruiter response using AI with the NEW offer context const recruiterResponse = await generateRecruiterMessage( 'response', negotiation, personality, - { userMessage: message, counterOffer, analysis } + { userMessage: message, counterOffer, analysis, newOffer } ); - - // Determine if recruiter makes a counter-offer - const newOffer = generateCounterOffer(negotiation, counterOffer, analysis, personality); - + const recruiterMessageData = { sender: 'recruiter', message: recruiterResponse, offer: newOffer }; - + if (negotiation.communicationMode === 'email') { recruiterMessageData.emailMetadata = { subject: `Re: Offer for ${negotiation.role} position at ${negotiation.companyName}`, @@ -261,9 +262,9 @@ exports.sendMessage = async (req, res) => { cc: [] }; } - + negotiation.conversationHistory.push(recruiterMessageData); - + // Update performance metrics if (!negotiation.performance) { negotiation.performance = { @@ -272,13 +273,13 @@ exports.sendMessage = async (req, res) => { strengthsShown: [] }; } - + negotiation.performance.tacticsUsed.push(...analysis.tacticsUsed); negotiation.performance.mistakesMade.push(...analysis.mistakes); negotiation.performance.strengthsShown.push(...analysis.strengths); - + await negotiation.save(); - + res.json({ success: true, recruiterMessage: recruiterResponse, @@ -294,82 +295,18 @@ exports.sendMessage = async (req, res) => { } }; -// Accept or reject offer -exports.finalizeNegotiation = async (req, res) => { - try { - const { negotiationId } = req.params; - const { action, finalOffer } = req.body; // action: 'accept', 'reject', 'walk-away' - - const negotiation = await SalaryNegotiation.findOne({ - _id: negotiationId, - user: req.user._id - }); - - if (!negotiation) { - return res.status(404).json({ success: false, message: 'Negotiation not found' }); - } - - negotiation.status = action === 'accept' ? 'accepted' : action === 'reject' ? 'rejected' : 'walked-away'; - negotiation.finalOffer = finalOffer; - negotiation.completedAt = new Date(); - negotiation.duration = Math.round((negotiation.completedAt - negotiation.startedAt) / 1000); - - // Calculate final performance - const improvement = negotiation.calculateImprovement(); - negotiation.performance.improvementGained = improvement; - negotiation.performance.confidenceScore = calculateConfidenceScore(negotiation); - negotiation.performance.finalResult = getFinalResult(negotiation, improvement); - - await negotiation.save(); - - // Generate detailed feedback - const feedback = generateFeedback(negotiation); - - res.json({ - success: true, - summary: negotiation.getSummary(), - feedback - }); - } catch (error) { - console.error('Error finalizing negotiation:', error); - res.status(500).json({ success: false, message: 'Failed to finalize negotiation' }); - } -}; - -// Get user's negotiation history -exports.getNegotiationHistory = async (req, res) => { - try { - const negotiations = await SalaryNegotiation.find({ user: req.user._id }) - .sort({ createdAt: -1 }) - .limit(20); - - const summary = negotiations.map(n => n.getSummary()); - - res.json({ - success: true, - negotiations: summary, - stats: { - totalNegotiations: negotiations.length, - averageImprovement: negotiations.reduce((sum, n) => sum + parseFloat(n.calculateImprovement()), 0) / negotiations.length, - acceptedOffers: negotiations.filter(n => n.status === 'accepted').length - } - }); - } catch (error) { - console.error('Error fetching negotiation history:', error); - res.status(500).json({ success: false, message: 'Failed to fetch history' }); - } -}; +// ... (finalizeNegotiation and getNegotiationHistory remain unchanged) ... // Helper: Generate recruiter message using AI async function generateRecruiterMessage(type, negotiation, personality, context) { const model = genAI.getGenerativeModel({ model: 'gemini-pro' }); - + let prompt = ''; - + if (type === 'opening') { const isNoticePeriod = negotiation.scenario === 'notice-period-buyout'; const isEmail = negotiation.communicationMode === 'email'; - + prompt = `You are ${negotiation.recruiterName}, a ${personality.tone} recruiter for ${negotiation.companyName} in India. Generate an opening ${isEmail ? 'email' : 'message'} for a ${isNoticePeriod ? 'notice period buyout' : 'salary'} negotiation with a ${negotiation.level} ${negotiation.role} in ${negotiation.location}. @@ -394,31 +331,46 @@ Be ${personality.tone}. Use Indian salary terminology (CTC, LPA, fixed vs variab } else { const lastOffer = negotiation.conversationHistory[negotiation.conversationHistory.length - 1].offer; const isEmail = negotiation.communicationMode === 'email'; - - prompt = `You are ${negotiation.recruiterName}, a ${personality.tone} recruiter for ${negotiation.companyName}. The candidate just said: "${context.userMessage}" + const newOffer = context.newOffer; // This is the offer we MUST present -${isEmail ? `Format as a professional email reply with: -- Greeting -- Response to their message -- Your counter-offer or position -- Closing with your name + // Calculate changes to explain them + const baseChange = newOffer.baseSalary - lastOffer.baseSalary; + const equityChange = newOffer.equity - lastOffer.equity; + const bonusChange = newOffer.signingBonus - lastOffer.signingBonus; -Keep it professional but ${personality.tone}. Use proper email etiquette.` : 'Format as a conversational message.'} + const improved = baseChange > 0 || equityChange > 0 || bonusChange > 0; + const matched = context.counterOffer && + newOffer.baseSalary >= context.counterOffer.baseSalary && + newOffer.equity >= context.counterOffer.equity; -${context.counterOffer ? `They're asking for: -- Base (Fixed): ₹${context.counterOffer.baseSalary ? (context.counterOffer.baseSalary / 100000).toFixed(2) + ' LPA' : 'not specified'} -- Variable/ESOPs: ₹${context.counterOffer.equity ? (context.counterOffer.equity / 100000).toFixed(2) + ' LPA' : 'not specified'} -- Joining Bonus: ₹${context.counterOffer.signingBonus ? (context.counterOffer.signingBonus / 100000).toFixed(2) + ' LPA' : 'not specified'}` : ''} + prompt = `You are ${negotiation.recruiterName}, a ${personality.tone} recruiter for ${negotiation.companyName}. +The candidate just said: "${context.userMessage}" -Your current offer is: -- Base (Fixed): ₹${(lastOffer.baseSalary / 100000).toFixed(2)} LPA -- Variable/ESOPs: ₹${(lastOffer.equity / 100000).toFixed(2)} LPA -- Joining Bonus: ₹${(lastOffer.signingBonus / 100000).toFixed(2)} LPA +${context.counterOffer ? `They asked for: +- Base: ₹${context.counterOffer.baseSalary ? (context.counterOffer.baseSalary / 100000).toFixed(2) + ' LPA' : 'N/A'} +- Equity: ₹${context.counterOffer.equity ? (context.counterOffer.equity / 100000).toFixed(2) + ' LPA' : 'N/A'} +- Bonus: ₹${context.counterOffer.signingBonus ? (context.counterOffer.signingBonus / 100000).toFixed(2) + ' LPA' : 'N/A'}` : ''} -Respond as a ${personality.tone} recruiter in Indian context. ${personality.openness > 0.6 ? 'Be open to negotiation.' : 'Be firm but fair.'} -Use Indian salary terminology (CTC, LPA, fixed vs variable). ${isEmail ? 'Keep it under 120 words.' : 'Keep it under 80 words.'} Be realistic.`; +You have reviewed their request with the team. +HERE IS YOUR NEW OFFICIAL OFFER (You MUST stick to these numbers): +- Base (Fixed): ₹${(newOffer.baseSalary / 100000).toFixed(2)} LPA +- Variable/ESOPs: ₹${(newOffer.equity / 100000).toFixed(2)} LPA +- Joining Bonus: ₹${(newOffer.signingBonus / 100000).toFixed(2)} LPA + +INSTRUCTIONS: +1. Acknowledge their points. +2. State clearly whether you could match their request or not. + - If you improved the offer: Explain WHY you could improve it (e.g., "Given your experience...", "We really want you on board..."). + - If you could NOT match fully: Explain WHY (e.g., "This is the top of our band for this level", "We have strict equity policies", "Internal parity with other engineers"). + - If you didn't move at all: Be firm but polite (e.g., "We believe this offer is very competitive given the market..."). +3. Present the new numbers clearly. +4. Ask for their thoughts. + +Tone: ${personality.tone}. +${personality.pushback > 0.7 ? 'Be tough. Emphasize that budget is tight.' : 'Be collaborative.'} +Use Indian salary terminology (CTC, LPA). ${isEmail ? 'Keep it under 150 words.' : 'Keep it under 100 words.'}`; } - + try { const result = await model.generateContent(prompt); return result.response.text(); @@ -426,9 +378,9 @@ Use Indian salary terminology (CTC, LPA, fixed vs variable). ${isEmail ? 'Keep i console.error('AI generation error:', error); // Fallback responses if (type === 'opening') { - return `Hi! I'm excited to extend an offer for the ${negotiation.role} position. We're offering ₹${(negotiation.initialOffer.baseSalary / 100000).toFixed(2)} LPA fixed, ₹${(negotiation.initialOffer.equity / 100000).toFixed(2)} LPA in ESOPs/variable, and a ₹${(negotiation.initialOffer.signingBonus / 100000).toFixed(2)} LPA joining bonus. Total CTC comes to ₹${((negotiation.initialOffer.baseSalary + negotiation.initialOffer.equity + negotiation.initialOffer.signingBonus) / 100000).toFixed(2)} LPA. I'm here to discuss and make sure this works for you!`; + return `Hi! I'm excited to extend an offer for the ${negotiation.role} position. We're offering ₹${(negotiation.initialOffer.baseSalary / 100000).toFixed(2)} LPA fixed...`; } - return "I appreciate your perspective. Let me review this with the team and get back to you with our best offer."; + return "I've reviewed your request with the team. We can offer " + (context.newOffer.baseSalary / 100000).toFixed(2) + " LPA base."; } } @@ -438,35 +390,35 @@ function analyzeUserMessage(message, counterOffer, negotiation) { const mistakes = []; const strengths = []; const suggestions = []; - + const lowerMessage = message.toLowerCase(); - + // Check for good tactics - if (lowerMessage.includes('market rate') || lowerMessage.includes('industry standard')) { + if (lowerMessage.includes('market rate') || lowerMessage.includes('industry standard') || lowerMessage.includes('market research')) { tactics.push('market-data'); strengths.push('Referenced market data'); } - if (lowerMessage.includes('other offer') || lowerMessage.includes('competing offer')) { + if (lowerMessage.includes('other offer') || lowerMessage.includes('competing offer') || lowerMessage.includes('another company')) { tactics.push('competing-offers'); - strengths.push('Mentioned competing offers'); + strengths.push('Leveraged competing offers'); } - if (lowerMessage.includes('excited') || lowerMessage.includes('enthusiastic')) { + if (lowerMessage.includes('excited') || lowerMessage.includes('enthusiastic') || lowerMessage.includes('love the team')) { tactics.push('enthusiasm'); strengths.push('Showed enthusiasm for the role'); } - if (lowerMessage.includes('total compensation') || lowerMessage.includes('overall package')) { - tactics.push('total-comp'); - strengths.push('Focused on total compensation'); + if (lowerMessage.includes('value') || lowerMessage.includes('contribution') || lowerMessage.includes('impact')) { + tactics.push('value-creation'); + strengths.push('Focused on value and impact'); } - + // Check for mistakes - if (lowerMessage.includes('current salary') || lowerMessage.includes('currently making')) { - mistakes.push('Revealed current salary (never do this!)'); - suggestions.push('Avoid revealing your current salary. Focus on market value instead.'); + if (lowerMessage.includes('current salary') || lowerMessage.includes('currently making') || lowerMessage.includes('my package is')) { + mistakes.push('Revealed current salary'); + suggestions.push('Avoid revealing your current salary. Focus on the value you bring to this new role.'); } - if (lowerMessage.includes('need') || lowerMessage.includes('must have')) { - mistakes.push('Used desperate language'); - suggestions.push('Avoid "need" language. Use "would like" or "expect" instead.'); + if (lowerMessage.includes('need') || lowerMessage.includes('have to have') || lowerMessage.includes('bills')) { + mistakes.push('Used personal need justification'); + suggestions.push('Justify your ask based on market data and skills, not personal financial needs.'); } if (counterOffer && counterOffer.baseSalary < negotiation.initialOffer.baseSalary) { mistakes.push('Counter-offered below initial offer'); @@ -474,57 +426,109 @@ function analyzeUserMessage(message, counterOffer, negotiation) { } if (message.length < 30) { mistakes.push('Response too brief'); - suggestions.push('Provide more context and reasoning for your position.'); + suggestions.push('Provide more context and reasoning. Explain WHY you deserve more.'); } - + // Check if counter is reasonable if (counterOffer && counterOffer.baseSalary) { const increase = ((counterOffer.baseSalary - negotiation.initialOffer.baseSalary) / negotiation.initialOffer.baseSalary) * 100; - if (increase > 30) { - mistakes.push('Counter-offer too aggressive (>30% increase)'); - suggestions.push('Keep counter-offers within 15-25% of initial offer for best results.'); - } else if (increase < 5) { - mistakes.push('Counter-offer too conservative (<5% increase)'); - suggestions.push('Aim for 10-20% increase to show you value yourself appropriately.'); + if (increase > 40) { + mistakes.push('Counter-offer too aggressive (>40% increase)'); + suggestions.push('Your ask is significantly above the initial offer. Be prepared to justify it with strong data.'); + } else if (increase < 3) { + mistakes.push('Counter-offer too small (<3% increase)'); + suggestions.push('Don\'t be afraid to ask for more. A 10-20% increase is standard for a first counter.'); } } - + return { tacticsUsed: tactics, mistakes, strengths, suggestions }; } // Helper: Generate counter-offer from recruiter function generateCounterOffer(negotiation, userCounterOffer, analysis, personality) { - const lastOffer = negotiation.conversationHistory[negotiation.conversationHistory.length - 2].offer; - + // Get the absolute latest offer from history + let lastOffer = negotiation.initialOffer; + for (let i = negotiation.conversationHistory.length - 1; i >= 0; i--) { + if (negotiation.conversationHistory[i].sender === 'recruiter' && negotiation.conversationHistory[i].offer) { + lastOffer = negotiation.conversationHistory[i].offer; + break; + } + } + if (!userCounterOffer) return lastOffer; // No counter from user, keep same offer - - // Calculate how much to move based on personality and user's tactics - const movementFactor = personality.openness * (1 - (analysis.mistakes.length * 0.1)); - const maxMovement = (negotiation.marketData.p75 - lastOffer.baseSalary) * movementFactor; - - const requestedIncrease = userCounterOffer.baseSalary - lastOffer.baseSalary; - const actualIncrease = Math.min(requestedIncrease * movementFactor, maxMovement); - + + // Calculate negotiation room (max budget is typically p75 or p90 depending on personality) + const maxBudget = personality.openness > 0.7 ? negotiation.marketData.p90 : negotiation.marketData.p75; + + // How much are they willing to move? (0 to 1) + // Openness affects willingness. Mistakes reduce willingness. + let willingnessToMove = personality.openness; + if (analysis.mistakes.length > 0) willingnessToMove *= 0.8; + if (analysis.tacticsUsed.length > 0) willingnessToMove *= 1.2; + + // Cap willingness at 1.0 + willingnessToMove = Math.min(willingnessToMove, 1.0); + + // Calculate potential new base + const currentBase = lastOffer.baseSalary; + const requestedBase = userCounterOffer.baseSalary || currentBase; + + let newBase = currentBase; + + if (requestedBase > currentBase) { + const gap = requestedBase - currentBase; + const maxAllowedIncrease = maxBudget - currentBase; + + if (maxAllowedIncrease > 0) { + // They will meet you part way, depending on willingness + const increase = Math.min(gap, maxAllowedIncrease) * willingnessToMove * 0.6; // 0.6 is a damping factor so they don't fold immediately + newBase = currentBase + increase; + } + } + + // Round to nearest 10,000 + newBase = Math.round(newBase / 10000) * 10000; + + // Handle Equity and Bonus + let newEquity = lastOffer.equity; + if (userCounterOffer.equity > lastOffer.equity) { + // Equity is harder to move, usually fixed pools + newEquity = lastOffer.equity + ((userCounterOffer.equity - lastOffer.equity) * 0.2 * willingnessToMove); + } + + let newBonus = lastOffer.signingBonus; + if (userCounterOffer.signingBonus > lastOffer.signingBonus) { + // Signing bonus is often used as a lever when base can't move + const baseGap = requestedBase - newBase; + if (baseGap > 0) { + // Compensate for missing base with one-time bonus + newBonus += baseGap * 0.5; + } + newBonus += (userCounterOffer.signingBonus - lastOffer.signingBonus) * 0.3 * willingnessToMove; + } + return { - baseSalary: Math.round(lastOffer.baseSalary + actualIncrease), - equity: userCounterOffer.equity || lastOffer.equity, - signingBonus: Math.round(lastOffer.signingBonus + (actualIncrease * 0.1)), + baseSalary: Math.round(newBase), + equity: Math.round(newEquity), + signingBonus: Math.round(newBonus), relocation: lastOffer.relocation, - benefits: lastOffer.benefits + benefits: lastOffer.benefits, + noticePeriodDays: lastOffer.noticePeriodDays, + buyoutAmount: lastOffer.buyoutAmount }; } // Helper: Calculate confidence score function calculateConfidenceScore(negotiation) { let score = 50; // Base score - + // Positive factors score += negotiation.performance.strengthsShown.length * 5; score += Math.min(negotiation.negotiationRounds * 3, 15); // More rounds = more confident - + // Negative factors score -= negotiation.performance.mistakesMade.length * 8; - + return Math.max(0, Math.min(100, score)); } @@ -536,7 +540,7 @@ function getFinalResult(negotiation, improvement) { if (negotiation.status === 'rejected') { return 'You rejected the offer. Make sure you had good reasons!'; } - + if (improvement > 20) return 'Excellent negotiation! You gained significant value.'; if (improvement > 10) return 'Good negotiation! You improved the offer meaningfully.'; if (improvement > 5) return 'Decent negotiation. You got some improvement.'; @@ -547,7 +551,7 @@ function getFinalResult(negotiation, improvement) { function generateFeedback(negotiation) { const improvement = parseFloat(negotiation.calculateImprovement()); const marketPosition = calculateMarketPosition(negotiation); - + return { overall: negotiation.performance.finalResult, improvement: `${improvement}%`, @@ -564,7 +568,7 @@ function generateFeedback(negotiation) { function calculateMarketPosition(negotiation) { const finalSalary = negotiation.finalOffer.baseSalary; const market = negotiation.marketData; - + if (finalSalary >= market.p90) return { percentile: 90, description: 'Excellent - Top 10%' }; if (finalSalary >= market.p75) return { percentile: 75, description: 'Great - Top 25%' }; if (finalSalary >= market.p50) return { percentile: 50, description: 'Good - Above median' }; @@ -575,7 +579,7 @@ function calculateMarketPosition(negotiation) { // Helper: Generate recommendations function generateRecommendations(negotiation, improvement, marketPosition) { const recommendations = []; - + if (improvement < 10) { recommendations.push('Practice being more assertive. You left money on the table.'); } @@ -591,7 +595,7 @@ function generateRecommendations(negotiation, improvement, marketPosition) { if (!negotiation.performance.tacticsUsed.includes('market-data')) { recommendations.push('Always reference market data to support your position.'); } - + return recommendations; } @@ -601,11 +605,11 @@ exports.getNegotiationHistory = async (req, res) => { const negotiations = await SalaryNegotiation.find({ user: req.user._id }) .sort({ createdAt: -1 }) .select('-conversationHistory'); // Exclude full conversation for performance - + // Calculate analytics const totalNegotiations = negotiations.length; const completedNegotiations = negotiations.filter(n => n.status !== 'in-progress').length; - + // Calculate average improvement const improvementSum = negotiations .filter(n => n.status !== 'in-progress') @@ -616,13 +620,13 @@ exports.getNegotiationHistory = async (req, res) => { return sum + improvement; }, 0); const avgImprovement = completedNegotiations > 0 ? improvementSum / completedNegotiations : 0; - + // Calculate average confidence score const confidenceSum = negotiations .filter(n => n.performance.confidenceScore) .reduce((sum, n) => sum + n.performance.confidenceScore, 0); const avgConfidence = negotiations.length > 0 ? confidenceSum / negotiations.length : 0; - + // Get most used tactics const tacticsCount = {}; negotiations.forEach(n => { @@ -636,7 +640,7 @@ exports.getNegotiationHistory = async (req, res) => { .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([tactic, count]) => ({ tactic, count })); - + // Get scenario breakdown const scenarioStats = {}; negotiations.forEach(n => { @@ -651,30 +655,30 @@ exports.getNegotiationHistory = async (req, res) => { scenarioStats[n.scenario].totalImprovement += improvement; } }); - + Object.keys(scenarioStats).forEach(scenario => { const completed = negotiations.filter(n => n.scenario === scenario && n.status !== 'in-progress').length; - scenarioStats[scenario].avgImprovement = completed > 0 - ? scenarioStats[scenario].totalImprovement / completed + scenarioStats[scenario].avgImprovement = completed > 0 + ? scenarioStats[scenario].totalImprovement / completed : 0; }); - + // Calculate streak (consecutive days with negotiations) const today = new Date(); today.setHours(0, 0, 0, 0); let streak = 0; let checkDate = new Date(today); - + while (true) { const dayStart = new Date(checkDate); const dayEnd = new Date(checkDate); dayEnd.setHours(23, 59, 59, 999); - + const hasNegotiation = negotiations.some(n => { const nDate = new Date(n.createdAt); return nDate >= dayStart && nDate <= dayEnd; }); - + if (hasNegotiation) { streak++; checkDate.setDate(checkDate.getDate() - 1); @@ -682,7 +686,7 @@ exports.getNegotiationHistory = async (req, res) => { break; } } - + // Get recent achievements const achievements = []; if (totalNegotiations >= 1) achievements.push({ name: 'First Negotiation', icon: '🎯', date: negotiations[negotiations.length - 1].createdAt }); @@ -693,7 +697,7 @@ exports.getNegotiationHistory = async (req, res) => { if (avgConfidence >= 70) achievements.push({ name: 'Confident Negotiator', icon: '⭐', unlocked: true }); if (streak >= 3) achievements.push({ name: '3-Day Streak', icon: '🔥', unlocked: true }); if (streak >= 7) achievements.push({ name: '7-Day Streak', icon: '💎', unlocked: true }); - + res.json({ negotiations, analytics: { diff --git a/frontend/interview-perp-ai/src/pages/SalaryNegotiation/NegotiationSimulator.jsx b/frontend/interview-perp-ai/src/pages/SalaryNegotiation/NegotiationSimulator.jsx index 42ab0ac..eda9964 100644 --- a/frontend/interview-perp-ai/src/pages/SalaryNegotiation/NegotiationSimulator.jsx +++ b/frontend/interview-perp-ai/src/pages/SalaryNegotiation/NegotiationSimulator.jsx @@ -23,7 +23,7 @@ const NegotiationSimulator = () => { const location = useLocation(); const navigate = useNavigate(); const messagesEndRef = useRef(null); - + const [negotiation, setNegotiation] = useState(null); const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false); @@ -60,16 +60,16 @@ const NegotiationSimulator = () => { // Use relative URL if VITE_API_URL is not set, otherwise use the full URL const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'; const apiUrl = `${baseUrl}/api/salary-negotiation/start`; - + console.log('Starting negotiation with config:', config); console.log('API URL:', apiUrl); - + const response = await axios.post( apiUrl, config, { headers: { Authorization: `Bearer ${token}` } } ); - + console.log('Negotiation started:', response.data); setNegotiation(response.data.negotiation); setLoading(false); @@ -83,12 +83,16 @@ const NegotiationSimulator = () => { const handleSendMessage = async () => { if (!userMessage.trim() && !showCounterOffer) return; - + setSending(true); try { const token = localStorage.getItem('token'); const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'; - const response = await axios.post( + + // Create a minimum delay to simulate "reviewing" time (2.5 seconds) + const minDelay = new Promise(resolve => setTimeout(resolve, 2500)); + + const apiCall = axios.post( `${baseUrl}/api/salary-negotiation/${negotiation.id}/message`, { message: userMessage, @@ -100,7 +104,10 @@ const NegotiationSimulator = () => { }, { headers: { Authorization: `Bearer ${token}` } } ); - + + // Wait for both the API and the delay + const [response] = await Promise.all([apiCall, minDelay]); + // Update conversation history setNegotiation(prev => ({ ...prev, @@ -122,7 +129,7 @@ const NegotiationSimulator = () => { } ] })); - + setAnalysis(response.data.analysis); setUserMessage(''); setCounterOffer({ baseSalary: '', equity: '', signingBonus: '' }); @@ -151,7 +158,7 @@ const NegotiationSimulator = () => { { action, finalOffer }, { headers: { Authorization: `Bearer ${token}` } } ); - + navigate('/salary-negotiation/results', { state: { summary: response.data.summary, @@ -187,7 +194,7 @@ const NegotiationSimulator = () => { const getMarketPosition = (salary) => { const market = negotiation?.marketData; if (!market) return null; - + if (salary >= market.p90) return { label: 'Top 10%', color: 'emerald', percentile: 90 }; if (salary >= market.p75) return { label: 'Top 25%', color: 'blue', percentile: 75 }; if (salary >= market.p50) return { label: 'Above Median', color: 'indigo', percentile: 50 }; @@ -255,7 +262,7 @@ const NegotiationSimulator = () => { Current Offer

- +
Base Salary
@@ -263,7 +270,7 @@ const NegotiationSimulator = () => { {formatCurrency(currentOffer?.baseSalary || 0)}
- +
Equity
@@ -278,7 +285,7 @@ const NegotiationSimulator = () => {
- +
Total Compensation
@@ -294,7 +301,7 @@ const NegotiationSimulator = () => { Market Data - + {marketPosition && (
@@ -305,7 +312,7 @@ const NegotiationSimulator = () => {
)} - +
90th percentile: @@ -333,7 +340,7 @@ const NegotiationSimulator = () => { AI Analysis - + {analysis.tacticsDetected?.length > 0 && (
✓ Good Tactics
@@ -347,7 +354,7 @@ const NegotiationSimulator = () => {
)} - + {analysis.suggestions?.length > 0 && (
💡 Suggestions
@@ -376,127 +383,126 @@ const NegotiationSimulator = () => { />
) : ( -
- {/* Messages */} -
- {negotiation.conversationHistory?.map((msg, idx) => ( -
-
-
- {msg.sender === 'user' ? 'You' : 'Recruiter'} -
-
- {msg.message} -
- {msg.offer && ( -
-
Base: {formatCurrency(msg.offer.baseSalary)}
- {msg.offer.equity > 0 &&
Equity: {formatCurrency(msg.offer.equity)}
} - {msg.offer.signingBonus > 0 &&
Signing: {formatCurrency(msg.offer.signingBonus)}
} +
+ {/* Messages */} +
+ {negotiation.conversationHistory?.map((msg, idx) => ( +
+
+
+ {msg.sender === 'user' ? 'You' : 'Recruiter'}
- )} +
+ {msg.message} +
+ {msg.offer && ( +
+
Base: {formatCurrency(msg.offer.baseSalary)}
+ {msg.offer.equity > 0 &&
Equity: {formatCurrency(msg.offer.equity)}
} + {msg.offer.signingBonus > 0 &&
Signing: {formatCurrency(msg.offer.signingBonus)}
} +
+ )} +
-
- ))} -
-
+ ))} +
+
- {/* Input Area */} -
- {showCounterOffer && ( -
-
Counter Offer (in Lakhs)
-
-
- - setCounterOffer({ ...counterOffer, baseSalary: e.target.value })} - className="w-full px-3 py-2 border border-indigo-200 dark:border-slate-600 dark:bg-slate-600 dark:text-white rounded-lg text-sm" - /> -
-
- - setCounterOffer({ ...counterOffer, equity: e.target.value })} - className="w-full px-3 py-2 border border-indigo-200 dark:border-slate-600 dark:bg-slate-600 dark:text-white rounded-lg text-sm" - /> -
-
- - setCounterOffer({ ...counterOffer, signingBonus: e.target.value })} - className="w-full px-3 py-2 border border-indigo-200 dark:border-slate-600 dark:bg-slate-600 dark:text-white rounded-lg text-sm" - /> + {/* Input Area */} +
+ {showCounterOffer && ( +
+
Counter Offer (in Lakhs)
+
+
+ + setCounterOffer({ ...counterOffer, baseSalary: e.target.value })} + className="w-full px-3 py-2 border border-indigo-200 dark:border-slate-600 dark:bg-slate-600 dark:text-white rounded-lg text-sm" + /> +
+
+ + setCounterOffer({ ...counterOffer, equity: e.target.value })} + className="w-full px-3 py-2 border border-indigo-200 dark:border-slate-600 dark:bg-slate-600 dark:text-white rounded-lg text-sm" + /> +
+
+ + setCounterOffer({ ...counterOffer, signingBonus: e.target.value })} + className="w-full px-3 py-2 border border-indigo-200 dark:border-slate-600 dark:bg-slate-600 dark:text-white rounded-lg text-sm" + /> +
+ )} + +
+