From a7777ba1d06edad205b7651a5bcc76f6368294de Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 24 May 2025 21:41:00 +0300 Subject: [PATCH 01/11] Add Initial Quiz Frontend Page Signed-off-by: Tal Jacob --- nextstep-frontend/src/App.tsx | 2 + nextstep-frontend/src/components/TopBar.tsx | 7 +- nextstep-frontend/src/pages/Quiz.tsx | 378 ++++++++++++++++++++ 3 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 nextstep-frontend/src/pages/Quiz.tsx diff --git a/nextstep-frontend/src/App.tsx b/nextstep-frontend/src/App.tsx index 525d2b2..0aa4d26 100644 --- a/nextstep-frontend/src/App.tsx +++ b/nextstep-frontend/src/App.tsx @@ -14,6 +14,7 @@ import Resume from './pages/Resume'; import TopBar from './components/TopBar'; import Layout from './components/Layout'; import MainDashboard from './pages/MainDashboard'; +import Quiz from './pages/Quiz'; const App: React.FC = () => { return ( @@ -30,6 +31,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> diff --git a/nextstep-frontend/src/components/TopBar.tsx b/nextstep-frontend/src/components/TopBar.tsx index c717820..949e3d3 100644 --- a/nextstep-frontend/src/components/TopBar.tsx +++ b/nextstep-frontend/src/components/TopBar.tsx @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { AppBar, Toolbar, IconButton, Tooltip, Box } from '@mui/material'; -import { Home, Person, Message, Logout, DocumentScannerTwoTone, Feed } from '@mui/icons-material'; +import { Home, Person, Message, Logout, DocumentScannerTwoTone, Feed, Quiz } from '@mui/icons-material'; import {getUserAuth, removeUserAuth} from "../handlers/userAuth.ts"; import api from "../serverApi.ts"; @@ -32,6 +32,11 @@ const TopBar: React.FC = () => { + + navigate('/quiz')} sx={{ mx: 1 }}> + + + navigate('/feed')} sx={{ mx: 1 }}> diff --git a/nextstep-frontend/src/pages/Quiz.tsx b/nextstep-frontend/src/pages/Quiz.tsx new file mode 100644 index 0000000..b4941db --- /dev/null +++ b/nextstep-frontend/src/pages/Quiz.tsx @@ -0,0 +1,378 @@ +import React, { useState } from 'react'; +import { + Container, + Box, + Typography, + TextField, + Button, + CircularProgress, + IconButton, + Tooltip, + Paper, + Slider, + Stack, + Divider, +} from '@mui/material'; +import { + Visibility, + VisibilityOff, + LightbulbOutlined as LightbulbOutlinedIcon, +} from '@mui/icons-material'; +import api from '../serverApi'; // Assuming you have a configured axios instance + +// Define interfaces for the API response schemas +interface GeneratedQuestion { + question: string; +} + +interface QuizGenerationResponse { + _id: string; + title: string; + tags: string[]; + content: string; + job_role: string; + company_name_en: string; + company_name_he: string; + process_details: string; + question_list: string[]; + answer_list: string[]; + keywords: string[]; + interviewer_mindset: string; +} + +interface UserAnsweredQuiz { + _id: string; + title: string; + tags: string[]; + content: string; + job_role: string; + company_name_en: string; + company_name_he: string; + process_details: string; + question_list: string[]; + answer_list: string[]; + user_answer_list: string[]; // New field for user answers + keywords: string[]; + interviewer_mindset: string; +} + +interface GradedAnswer { + question: string; + user_answer: string; + grade: number; + tip: string; +} + +interface QuizGradingResponse { + graded_answers: GradedAnswer[]; + final_quiz_grade: number; + final_summary_tip: string; +} + +// Internal state structure for the quiz, combining generated and graded data +interface QuizStateQuestion { + originalQuestion: string; + userAnswer: string; + correctAnswer?: string; // Will be populated after grading + grade?: number; // Will be populated after grading + tip?: string; // Will be populated after grading +} + +interface QuizState { + _id: string; + subject: string; + questions: QuizStateQuestion[]; + finalGrade?: number; + finalTip?: string; + // Potentially store other fields from the initial generation response if needed for display + title?: string; + tags?: string[]; + content?: string; + jobRole?: string; +} + +const Quiz: React.FC = () => { + const [subject, setSubject] = useState(''); + const [quiz, setQuiz] = useState(null); + const [loading, setLoading] = useState(false); + const [showAnswer, setShowAnswer] = useState<{ [key: number]: boolean }>({}); // To toggle answer visibility + const [quizSubmitted, setQuizSubmitted] = useState(false); // To control UI after submission + + const handleGenerateQuiz = async () => { + if (!subject) return; + setLoading(true); + setQuiz(null); // Clear previous quiz + setQuizSubmitted(false); // Reset submission status + setShowAnswer({}); // Reset answer visibility + try { + const response = await api.post('http://localhost:3000/quiz/generate', { subject }); + + const generatedQuestions: QuizStateQuestion[] = response.data.question_list.map((q: string) => ({ + originalQuestion: q, + userAnswer: '', // Initialize empty user answer + })); + + setQuiz({ + _id: response.data._id, + subject: subject, // Use the input subject for consistency + questions: generatedQuestions, + title: response.data.title, + tags: response.data.tags, + content: response.data.content, + jobRole: response.data.job_role, + // ... include other fields from response.data if you want to display them + }); + } catch (error) { + console.error('Error generating quiz:', error); + alert('Failed to generate quiz. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleUserAnswerChange = (index: number, answer: string) => { + if (quiz) { + const updatedQuestions = [...quiz.questions]; + updatedQuestions[index].userAnswer = answer; + setQuiz({ ...quiz, questions: updatedQuestions }); + } + }; + + const handleToggleAnswerVisibility = (index: number) => { + setShowAnswer(prev => ({ + ...prev, + [index]: !prev[index], + })); + }; + + const handleSubmitQuiz = async () => { + if (!quiz || quizSubmitted) return; // Prevent multiple submissions + setLoading(true); + + const answeredQuizData: UserAnsweredQuiz = { + _id: quiz._id, + title: quiz.title || '', + tags: quiz.tags || [], + content: quiz.content || '', + job_role: quiz.jobRole || '', + company_name_en: '', // These are not directly from the quiz state, you might need to fetch/store them + company_name_he: '', // Or adjust your backend to not require them for grading + process_details: '', // Or adjust your backend to not require them for grading + question_list: quiz.questions.map(q => q.originalQuestion), + answer_list: quiz.questions.map(q => q.correctAnswer || ''), // Send known correct answers if available, otherwise empty + user_answer_list: quiz.questions.map(q => q.userAnswer), + keywords: [], // These are not directly from the quiz state, you might need to fetch/store them + interviewer_mindset: '', // These are not directly from the quiz state, you might need to fetch/store them + }; + + try { + // Send the entire schema with user_answer_list + const response = await api.post('http://localhost:3000/quiz/grade', answeredQuizData); + + const gradedQuizData = response.data; + const updatedQuestions = quiz.questions.map((q, index) => { + const gradedAnswer = gradedQuizData.graded_answers.find(ga => ga.question === q.originalQuestion); + return { + ...q, + grade: gradedAnswer?.grade, + tip: gradedAnswer?.tip, + // The correct answer from the initial generation might not be sent back + // with 'graded_answers'. If your backend sends it, update this line. + // For now, we'll assume the original 'answer_list' from generation is the correct answer + correctAnswer: quiz.answer_list?.[index], // Assuming answer_list was stored from generation + }; + }); + + setQuiz({ + ...quiz, + questions: updatedQuestions, + finalGrade: gradedQuizData.final_quiz_grade, + finalTip: gradedQuizData.final_summary_tip, + }); + setQuizSubmitted(true); // Mark quiz as submitted + + // Automatically show all answers after grading + const initialShowAnswer: { [key: number]: boolean } = {}; + updatedQuestions.forEach((_, index) => { + initialShowAnswer[index] = true; + }); + setShowAnswer(initialShowAnswer); + + } catch (error) { + console.error('Error submitting quiz:', error); + alert('Failed to submit quiz for grading. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( + + + Quiz Generator & Grader + + + {/* Subject Input */} + {!quiz && ( + + + Enter a Quiz Subject + + setSubject(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleGenerateQuiz()} + sx={{ mb: 2 }} + /> + + + )} + + {/* Generated Quiz Display */} + {quiz && ( + + + Quiz on: {quiz.subject} + + {quiz.title && ( + + Topic: {quiz.title} + + )} + + + Your answers may get better grades for broad, in-depth explanations. You can answer in any language you want! + + {quiz.questions.map((q, index) => ( + + + + Question {index + 1}: {q.originalQuestion} + + {quizSubmitted && ( // Only show the eye icon after submission + + handleToggleAnswerVisibility(index)} size="small"> + {showAnswer[index] ? : } + + + )} + + handleUserAnswerChange(index, e.target.value)} + sx={{ mb: 2 }} + disabled={quizSubmitted} // Disable input after submission + /> + + {quizSubmitted && ( + <> + + + Your Grade: + + {/* Optional: Gauge for grade */} + + + + + Tip: + + + + + {q.tip} + + + + {showAnswer[index] && q.correctAnswer && ( + + Correct Answer: + {q.correctAnswer} + + )} + + )} + + ))} + + {!quizSubmitted && ( + + )} + + {quizSubmitted && quiz.finalGrade !== undefined && ( + + + Final Quiz Grade: + + {/* Optional: Gauge for final grade */} + + + Overall Tip: + + + + + {quiz.finalTip} + + + + + )} + + )} + + ); +}; + +export default Quiz; \ No newline at end of file From b9c677681b13cbe4f01192929a4e15e25ba17c72 Mon Sep 17 00:00:00 2001 From: Tal Jacob Date: Sat, 24 May 2025 21:49:04 +0300 Subject: [PATCH 02/11] Showcase More Fields In The Quiz Signed-off-by: Tal Jacob --- nextstep-frontend/src/pages/Quiz.tsx | 153 ++++++++++++++++++++++----- 1 file changed, 129 insertions(+), 24 deletions(-) diff --git a/nextstep-frontend/src/pages/Quiz.tsx b/nextstep-frontend/src/pages/Quiz.tsx index b4941db..199109e 100644 --- a/nextstep-frontend/src/pages/Quiz.tsx +++ b/nextstep-frontend/src/pages/Quiz.tsx @@ -1,3 +1,5 @@ +// src/pages/Quiz.tsx + import React, { useState } from 'react'; import { Container, @@ -11,12 +13,19 @@ import { Paper, Slider, Stack, + Chip, Divider, + Grid, } from '@mui/material'; import { Visibility, VisibilityOff, LightbulbOutlined as LightbulbOutlinedIcon, + WorkOutline as WorkOutlineIcon, // For Job Role + InfoOutlined as InfoOutlinedIcon, // For Content/Process Details + ForumOutlined as ForumOutlinedIcon, // For Interviewer Mindset + BusinessOutlined as BusinessOutlinedIcon, // For Company Name + LocalOfferOutlined as LocalOfferOutlinedIcon, // For Tags/Keywords } from '@mui/icons-material'; import api from '../serverApi'; // Assuming you have a configured axios instance @@ -32,7 +41,7 @@ interface QuizGenerationResponse { content: string; job_role: string; company_name_en: string; - company_name_he: string; + company_name_he: string; // Not used in display, but included for completeness process_details: string; question_list: string[]; answer_list: string[]; @@ -84,11 +93,16 @@ interface QuizState { questions: QuizStateQuestion[]; finalGrade?: number; finalTip?: string; - // Potentially store other fields from the initial generation response if needed for display + // --- Additional fields from QuizGenerationResponse for display --- title?: string; tags?: string[]; content?: string; jobRole?: string; + companyNameEn?: string; + processDetails?: string; + keywords?: string[]; + interviewerMindset?: string; + answer_list?: string[]; // Store the original answer list for display after grading } const Quiz: React.FC = () => { @@ -99,7 +113,7 @@ const Quiz: React.FC = () => { const [quizSubmitted, setQuizSubmitted] = useState(false); // To control UI after submission const handleGenerateQuiz = async () => { - if (!subject) return; + if (!subject.trim()) return; // Ensure subject is not empty or just whitespace setLoading(true); setQuiz(null); // Clear previous quiz setQuizSubmitted(false); // Reset submission status @@ -120,7 +134,11 @@ const Quiz: React.FC = () => { tags: response.data.tags, content: response.data.content, jobRole: response.data.job_role, - // ... include other fields from response.data if you want to display them + companyNameEn: response.data.company_name_en, + processDetails: response.data.process_details, + keywords: response.data.keywords, + interviewerMindset: response.data.interviewer_mindset, + answer_list: response.data.answer_list, // Store for later use as correct answers }); } catch (error) { console.error('Error generating quiz:', error); @@ -149,24 +167,24 @@ const Quiz: React.FC = () => { if (!quiz || quizSubmitted) return; // Prevent multiple submissions setLoading(true); + // Construct the request body as per the UserAnsweredQuiz schema const answeredQuizData: UserAnsweredQuiz = { _id: quiz._id, title: quiz.title || '', tags: quiz.tags || [], content: quiz.content || '', job_role: quiz.jobRole || '', - company_name_en: '', // These are not directly from the quiz state, you might need to fetch/store them - company_name_he: '', // Or adjust your backend to not require them for grading - process_details: '', // Or adjust your backend to not require them for grading + company_name_en: quiz.companyNameEn || '', + company_name_he: '', // Assuming this isn't strictly required for grading or can be empty + process_details: quiz.processDetails || '', question_list: quiz.questions.map(q => q.originalQuestion), - answer_list: quiz.questions.map(q => q.correctAnswer || ''), // Send known correct answers if available, otherwise empty + answer_list: quiz.answer_list || [], // Send the original correct answers if available user_answer_list: quiz.questions.map(q => q.userAnswer), - keywords: [], // These are not directly from the quiz state, you might need to fetch/store them - interviewer_mindset: '', // These are not directly from the quiz state, you might need to fetch/store them + keywords: quiz.keywords || [], + interviewer_mindset: quiz.interviewerMindset || '', }; try { - // Send the entire schema with user_answer_list const response = await api.post('http://localhost:3000/quiz/grade', answeredQuizData); const gradedQuizData = response.data; @@ -176,10 +194,7 @@ const Quiz: React.FC = () => { ...q, grade: gradedAnswer?.grade, tip: gradedAnswer?.tip, - // The correct answer from the initial generation might not be sent back - // with 'graded_answers'. If your backend sends it, update this line. - // For now, we'll assume the original 'answer_list' from generation is the correct answer - correctAnswer: quiz.answer_list?.[index], // Assuming answer_list was stored from generation + correctAnswer: quiz.answer_list?.[index], // Use the stored answer_list from generation }; }); @@ -230,7 +245,7 @@ const Quiz: React.FC = () => { {selectedJob?.jobUrl && ( - +
+ + +
)} diff --git a/nextstep-frontend/src/pages/Quiz.tsx b/nextstep-frontend/src/pages/Quiz.tsx index be0029b..c68af5e 100644 --- a/nextstep-frontend/src/pages/Quiz.tsx +++ b/nextstep-frontend/src/pages/Quiz.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { Container, Box, @@ -102,7 +103,8 @@ interface QuizState { } const Quiz: React.FC = () => { - const [subject, setSubject] = useState(''); + const [searchParams] = useSearchParams(); + const [subject, setSubject] = useState(searchParams.get('subject') || ''); const [quiz, setQuiz] = useState(null); const [loading, setLoading] = useState(false); const [showAnswer, setShowAnswer] = useState<{ [key: number]: boolean }>({}); @@ -218,6 +220,11 @@ const Quiz: React.FC = () => { } }; + const handleEditSubject = (newSubject: string) => { + setSubject(newSubject); + setQuiz(null); // Reset the quiz to allow generating a new one with the updated subject + }; + return ( @@ -254,7 +261,14 @@ const Quiz: React.FC = () => { {quiz && ( - Quiz on: {quiz.subject} + Quiz on: + handleEditSubject(e.target.value)} + variant="outlined" + size="small" + sx={{ ml: 2, width: '50%' }} + /> {/* --- Enhanced Display of Quiz Metadata --- */} From 4ab2d59981a61d633ab18f3b27d9ebb3cde24437 Mon Sep 17 00:00:00 2001 From: lina elman Date: Sun, 25 May 2025 22:25:55 +0300 Subject: [PATCH 10/11] removed unnecessary --- nextstep-frontend/src/components/LinkedInIntegration.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/nextstep-frontend/src/components/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx index 56f30d2..c13e7d5 100644 --- a/nextstep-frontend/src/components/LinkedInIntegration.tsx +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -1,7 +1,6 @@ import React, { useState } from 'react'; import { Box, Typography, Button, Grid, CircularProgress, IconButton, TextField, MenuItem, Select, FormControl, InputLabel, Dialog, DialogTitle, DialogContent, DialogActions, Chip, Stack } from '@mui/material'; import { ExpandLess, LinkedIn, Settings } from '@mui/icons-material'; -import { useNavigate } from 'react-router-dom'; interface Job { position: string; From 41d2330d286cda105ce92fc22b3af470445ff5dc Mon Sep 17 00:00:00 2001 From: lina elman Date: Sun, 25 May 2025 22:38:47 +0300 Subject: [PATCH 11/11] added icon --- nextstep-frontend/src/pages/Quiz.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/nextstep-frontend/src/pages/Quiz.tsx b/nextstep-frontend/src/pages/Quiz.tsx index c68af5e..6847b32 100644 --- a/nextstep-frontend/src/pages/Quiz.tsx +++ b/nextstep-frontend/src/pages/Quiz.tsx @@ -27,6 +27,7 @@ import { BusinessOutlined as BusinessOutlinedIcon, LocalOfferOutlined as LocalOfferOutlinedIcon, } from '@mui/icons-material'; +import SchoolIcon from '@mui/icons-material/School'; // Import graduation hat icon import api from '../serverApi'; import { config } from '../config'; @@ -227,8 +228,22 @@ const Quiz: React.FC = () => { return ( - - Quiz Generator & Grader + + Quiz Generator &{' '} + + + Grader + {/* Subject Input */}