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/LinkedInIntegration.tsx b/nextstep-frontend/src/components/LinkedInIntegration.tsx index a5e0929..c13e7d5 100644 --- a/nextstep-frontend/src/components/LinkedInIntegration.tsx +++ b/nextstep-frontend/src/components/LinkedInIntegration.tsx @@ -68,6 +68,12 @@ const LinkedInIntegration: React.FC = ({ setJobDetails(null); }; + const handleGenerateQuiz = (job: Job) => { + const subject = `${job.position} at ${job.company}`; + const quizUrl = `/quiz?subject=${encodeURIComponent(subject)}`; + window.open(quizUrl, '_blank'); + }; + return ( @@ -281,14 +287,22 @@ const LinkedInIntegration: React.FC = ({ Close {selectedJob?.jobUrl && ( - +
+ + +
)} 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..6847b32 --- /dev/null +++ b/nextstep-frontend/src/pages/Quiz.tsx @@ -0,0 +1,528 @@ +import React, { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { + Container, + Box, + Typography, + TextField, + Button, + CircularProgress, + IconButton, + Tooltip, + Paper, + Slider, + Stack, + Chip, + Divider, + Grid, + Avatar, +} from '@mui/material'; +import { + Visibility, + VisibilityOff, + LightbulbOutlined as LightbulbOutlinedIcon, + WorkOutline as WorkOutlineIcon, + InfoOutlined as InfoOutlinedIcon, + ForumOutlined as ForumOutlinedIcon, + 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'; + +// Define interfaces for the API response schemas +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[]; + 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; + grade?: number; + tip?: string; +} + +interface QuizState { + _id: string; + subject: string; + questions: QuizStateQuestion[]; + finalGrade?: number; + finalTip?: string; + // --- 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 = () => { + 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 }>({}); + const [quizSubmitted, setQuizSubmitted] = useState(false); + + const handleGenerateQuiz = async () => { + if (!subject.trim()) return; + setLoading(true); + setQuiz(null); + setQuizSubmitted(false); + setShowAnswer({}); + try { + const response = await api.post(`${config.app.backend_url()}/quiz/generate`, { subject }); + + const generatedQuestions: QuizStateQuestion[] = response.data.question_list.map((q: string, idx: number) => ({ + originalQuestion: q, + userAnswer: '', + correctAnswer: response.data.answer_list[idx], // Populate correct answer immediately + })); + + setQuiz({ + _id: response.data._id, + subject: subject, + questions: generatedQuestions, + title: response.data.title, + tags: response.data.tags, + content: response.data.content, + jobRole: response.data.job_role, + 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, + }); + + } 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; + setLoading(true); + + const answeredQuizData: UserAnsweredQuiz = { + _id: quiz._id, + title: quiz.title || '', + tags: quiz.tags || [], + content: quiz.content || '', + job_role: quiz.jobRole || '', + company_name_en: quiz.companyNameEn || '', + company_name_he: '', + process_details: quiz.processDetails || '', + question_list: quiz.questions.map(q => q.originalQuestion), + answer_list: quiz.answer_list || [], + user_answer_list: quiz.questions.map(q => q.userAnswer), + keywords: quiz.keywords || [], + interviewer_mindset: quiz.interviewerMindset || '', + }; + + try { + const response = await api.post(`${config.app.backend_url()}/quiz/grade`, answeredQuizData); + + const gradedQuizData = response.data; + const updatedQuestions = quiz.questions.map((q, _) => { + const gradedAnswer = gradedQuizData.graded_answers.find(ga => ga.question === q.originalQuestion); + return { + ...q, + grade: gradedAnswer?.grade, + tip: gradedAnswer?.tip, + // correctAnswer is already present from generation + }; + }); + + setQuiz({ + ...quiz, + questions: updatedQuestions, + finalGrade: gradedQuizData.final_quiz_grade, + finalTip: gradedQuizData.final_summary_tip, + }); + setQuizSubmitted(true); + + // After submission, automatically show all correct answers and grades + 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); + } + }; + + const handleEditSubject = (newSubject: string) => { + setSubject(newSubject); + setQuiz(null); // Reset the quiz to allow generating a new one with the updated subject + }; + + return ( + + + Quiz Generator &{' '} + + + Grader + + + + {/* Subject Input */} + {!quiz && ( + + + + setSubject(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleGenerateQuiz()} + sx={{ mb: 2 }} + /> + + + )} + + {/* Generated Quiz Display */} + {quiz && ( + + + Quiz on: + handleEditSubject(e.target.value)} + variant="outlined" + size="small" + sx={{ ml: 2, width: '50%' }} + /> + + + {/* --- Enhanced Display of Quiz Metadata --- */} + + + {quiz.title && ( + + + + Quiz Title: {quiz.title} + + + )} + + {quiz.jobRole && ( + + + + Job Role: {quiz.jobRole} + + + )} + + {quiz.companyNameEn && ( + + + + Company: {quiz.companyNameEn} + + + )} + + {quiz.tags && quiz.tags.length > 0 && ( + + + + Tags: + + + {quiz.tags.map((tag, i) => ( + + ))} + + + )} + + {quiz.keywords && quiz.keywords.length > 0 && ( + + + + Keywords: + + + {quiz.keywords.map((keyword, i) => ( + + ))} + + + )} + + {quiz.processDetails && ( + + + + Process Details: + + + {quiz.processDetails} + + + )} + + {quiz.content && ( + + + + Context/Content: + + + {quiz.content} + + + )} + + {quiz.interviewerMindset && ( + + + + Interviewer Mindset: + + + "{quiz.interviewerMindset}" + + + )} + + + + {/* --- End Enhanced Display of Quiz Metadata --- */} + + + Your answers may get better grades for broad, in-depth explanations. You can answer in any language you want! + + {quiz.questions.map((q, index) => ( + + + {/* Circled Numbering (Option 2) */} + + {index + 1} + + {/* Question Text */} + + {q.originalQuestion} + + {/* Blinking Eye Icon (now always visible if answer exists) */} + {q.correctAnswer && ( + + handleToggleAnswerVisibility(index)} size="small" sx={{ flexShrink: 0, ml: 1 }}> + {showAnswer[index] ? : } + + + )} + + handleUserAnswerChange(index, e.target.value)} + sx={{ mb: 2 }} + disabled={quizSubmitted} + /> + + {/* Grade and Tip are still shown only after submission */} + {quizSubmitted && ( + <> + + + Your Grade: + + + + + + Tip: + + + + + {q.tip} + + + + + )} + {/* Correct answer display is now independent of quizSubmitted for showing */} + {showAnswer[index] && q.correctAnswer && ( + + Correct Answer: + {q.correctAnswer} + + )} + + ))} + + {!quizSubmitted && ( + + )} + + {quizSubmitted && quiz.finalGrade !== undefined && ( + + + Final Quiz Grade: + + + + Overall Tip: + + + + + {quiz.finalTip} + + + + + )} + + )} + + ); +}; + +export default Quiz; \ No newline at end of file