diff --git a/components/EditQuizDialog.tsx b/components/EditQuizDialog.tsx index e6c3b38..598eb2a 100644 --- a/components/EditQuizDialog.tsx +++ b/components/EditQuizDialog.tsx @@ -1,7 +1,7 @@ "use client"; import { QuizQuestion, Quiz } from "@/lib/types"; import { Button } from "@/components/ui/button"; -import { useSession } from "next-auth/react"; +import { useRouter } from "next/router"; import { ScrollArea } from "@/components/ui/scroll-area"; import { DialogContent, @@ -12,7 +12,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Textarea } from "@/components/ui/textarea"; @@ -25,8 +25,14 @@ export function EditQuizDialog({ }) { const [questions, setQuestions] = useState(propQuestions); const [quizName, setQuizName] = useState(propQuizName); - const { data: session } = useSession(); - const courseName = session?.user.courseList[0]; + const [isQuizNew, setIsQuizNew] = useState(true); + const router = useRouter(); + const { courseName } = router.query; + + useEffect(() => { + setQuestions(propQuestions); + setIsQuizNew(false); + }, [propQuestions]); const handleSubmit = async () => { const quiz: Quiz = { diff --git a/components/GradingDialog.tsx b/components/GradingDialog.tsx new file mode 100644 index 0000000..ec5446a --- /dev/null +++ b/components/GradingDialog.tsx @@ -0,0 +1,187 @@ +"use client"; +import { QuizQuestion, Quiz, QuizSubmission } from "@/lib/types"; +import { Button } from "@/components/ui/button"; +import { useRouter } from "next/router"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogClose, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { useEffect, useState } from "react"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Textarea } from "@/components/ui/textarea"; + +export function GradingDialog({ + propQuestions, + studentEmail, +}: { + propQuestions: QuizQuestion[]; + studentEmail: string; +}) { + // const [quizName, setQuizName] = useState(propQuizName); + // const [quizSubmission, setQuizSubmission] = useState(propQuiz.submissions); + + //each question's score that student's got + const [scores, setScores] = useState( + new Array(propQuestions.length).fill(0) + ); + + const [quizSubmission, setQuizSubmission] = useState< + Record + >({ + "ddd@ffse.com": { answers: ["1", "3", "I have no idea"], score: undefined }, + }); + + const router = useRouter(); + const courseName = Array.isArray(router.query.courseName) + ? router.query.courseName[0] + : router.query.courseName || ""; + const quizName = Array.isArray(router.query.quizName) + ? router.query.quizName[0] + : router.query.quizName || ""; + + const handleSubmit = async () => { + const totalScore = scores.reduce((acc, score) => acc + score, 0); + + // Update the student's submission with the total score + setQuizSubmission((current) => ({ + ...current, + [studentEmail]: { + ...current[studentEmail], + score: totalScore, + }, + })); + + const quiz: Quiz = { + name: quizName, + questions: propQuestions, + totalPoints: propQuestions.reduce((acc, q) => acc + q.points, 0), + submissions: { + ...quizSubmission, + [studentEmail]: { + ...quizSubmission[studentEmail], + score: totalScore, + }, + }, + }; + try { + const res = fetch(`/api/${courseName}/saveQuiz`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(quiz), + }); + console.log(await res); + } catch (err) { + console.error(err); + } + if (quizName.length == 0) { + // If creating a new quiz, reset the questions and + // name fields whenever the dialog is closed. + } + }; + + const handleScoreChange = (index: number, newScore: number) => { + setScores((currentScores) => { + const updatedScores = [...currentScores]; + updatedScores[index] = newScore; + return updatedScores; + }); + }; + + return ( + + + Grading Quiz + + Add details and create when your'e done. + + +
{quizName}
+ +
+ {propQuestions.map((q, qIndex) => ( +
+
+ {q.questionNum}. {q.question} +
+
+ points: {q.points} +
+ + {q.questionType === "MCQ" ? ( +
+ {/* Display the options and indicate which one was selected by the student */} + {q.options.map((option, oIndex) => ( +
+ {/* Check if the student's answer matches the option index */} + {q.answer === oIndex ? ( +
+ {oIndex + 1} . {option} +
// This is a checkmark symbol to indicate the selected answer + ) : ( +
+ {oIndex + 1} . {option} +
+ )} +
+ ))} +
+ student's answer : + {quizSubmission[studentEmail]?.answers[qIndex] ?? ""} +
+ + handleScoreChange(qIndex, Number(e.target.value)) + } + placeholder="Enter score" + /> +
+ ) : ( + // For FRQ, display the student's answer directly +
+
+ answer :{q.answer} +
+
+ student's answer :{" "} + {quizSubmission[studentEmail]?.answers[qIndex] ?? ""} +
+ + handleScoreChange(qIndex, Number(e.target.value)) + } + placeholder="Enter score" + /> +
+ )} +
+ ))} +
+
+ + + + + +
+ ); +} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index caa664f..9e01b0b 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -10,7 +10,7 @@ const Navbar = () => { const { data: session } = useSession(); const username = session?.user.name; const imageURL = session?.user.image; - const courseList = session?.user.courseList || ["cs4400"]; + const courseList = session?.user.courseList || []; const pathName = useRouter(); diff --git a/lib/types.ts b/lib/types.ts index f110199..140b287 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -18,6 +18,18 @@ export type User = { courseList: string[]; }; +export type Quiz = { + name: string; + questions: QuizQuestion[]; + totalPoints: number; + submissions: Record; +}; + +export type QuizSubmission = { + answers: string[]; + score: number | undefined; +}; + export type QuizQuestion = { questionNum: number; questionType: "MCQ" | "FRQ"; @@ -26,10 +38,3 @@ export type QuizQuestion = { answer: number | string; points: number; }; - -export type Quiz = { - name: string; - questions: QuizQuestion[]; - totalPoints: number; - submissions: Record; // key: username, value: score -}; diff --git a/pages/[courseName]/lessons/index.tsx b/pages/[courseName]/lessons/index.tsx index 7d92d52..93da109 100644 --- a/pages/[courseName]/lessons/index.tsx +++ b/pages/[courseName]/lessons/index.tsx @@ -138,23 +138,21 @@ export default function Lesson() { if (selectedFile) { try { // first get SAS URL in a GET request to getSAS with the file name as a query param - // const sasURL: string = await getSAS( - // courseName as string, - // selectedFile.name - // ); - // const uploadResponse = await fetch(sasURL, { - // method: "PUT", - // body: selectedFile, // This should be your file - // headers: { - // "x-ms-blob-type": "BlockBlob", - // }, - // }); - - await embeddings(courseName as string, selectedFile.name); - - // if (!uploadResponse.ok) { - // throw new Error("File upload failed"); - // } + const sasURL: string = await getSAS( + courseName as string, + selectedFile.name + ); + const uploadResponse = await fetch(sasURL, { + method: "PUT", + body: selectedFile, // This should be your file + headers: { + "x-ms-blob-type": "BlockBlob", + }, + }); + + if (!uploadResponse.ok) { + throw new Error("File upload failed"); + } console.log("File uploaded successfully"); diff --git a/pages/[courseName]/quizzes/[quizName]/index.tsx b/pages/[courseName]/quizzes/[quizName]/index.tsx index 27bc698..4017f59 100644 --- a/pages/[courseName]/quizzes/[quizName]/index.tsx +++ b/pages/[courseName]/quizzes/[quizName]/index.tsx @@ -1,69 +1,83 @@ "use client"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { Button } from "@/components/ui/button"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; import { Dialog, DialogTrigger } from "@/components/ui/dialog"; import { EditQuizDialog } from "@/components/EditQuizDialog"; -import { QuizQuestion } from "@/lib/types"; +import { Quiz, QuizQuestion, QuizSubmission } from "@/lib/types"; import { useRouter } from "next/router"; +import Link from "next/link"; +import { GradingDialog } from "@/components/GradingDialog"; -export default function Navbar() { +async function getQuiz(courseName: string, quizName: string): Promise { + const response = await fetch( + `/api/${courseName}/getSAS?filename=quiz/${quizName}` + ); + + if (!response.ok) { + throw new Error("File upload failed"); + } + + const sasURL = (await response.json()).sasURL; + const quiz = await fetch(sasURL); + if (!quiz.ok) { + throw new Error("Failed to fetch quiz"); + } + + return await quiz.json(); +} + +export default function Quiz() { const router = useRouter(); - let { courseName, quizName } = router.query; - courseName = courseName as string; - quizName = quizName as string; - const pathName = router.asPath; + const courseName = Array.isArray(router.query.courseName) + ? router.query.courseName[0] + : router.query.courseName || ""; + const quizName = Array.isArray(router.query.quizName) + ? router.query.quizName[0] + : router.query.quizName || ""; + const currentPath = router.asPath; const { data: session } = useSession(); const isTA = session?.user.isAdminFor.includes(courseName); // TA check is disabled while developing + const [currentQuiz, setCurrentQuiz] = useState({ + name: "", + questions: [], + totalPoints: 0, + submissions: {}, + }); + + const [questions, setQuestions] = useState([]); + + const [studentList, setStudentList] = useState([ + "ddd@ffse.com", + "aaa@ffse.com", + "bbb@ffse.com", + ]); const handleButtonClick = () => { - router.push(`${pathName}/take`); + router.push(`${currentPath}/take`); }; - const [questions, setQuestions] = useState([ - { - questionNum: 1, - questionType: "MCQ", - question: "which one is the best cloud service provider", - options: ["AWS", "Azure", "GCP", "Oracle Cloud"], - answer: 2, - points: 10, - }, - { - questionNum: 2, - questionType: "MCQ", - question: "string", - options: ["string1", "string2", "string3", "string4"], - answer: 3, - points: 10, - }, - { - questionNum: 3, - questionType: "MCQ", - question: "string", - options: ["string1", "string2", "string3", "string4"], - answer: 1, - points: 10, - }, - { - questionNum: 4, - questionType: "FRQ", - question: "string", - options: [], - answer: "this is the answer!", - points: 80, - }, - { - questionNum: 5, - questionType: "FRQ", - question: "string", - options: [], - answer: "answer!", - points: 80, - }, - ]); + useEffect(() => { + if (typeof courseName === "string" && typeof quizName === "string") { + getQuiz(courseName, quizName) + .then((quiz) => { + // convert the list of files to a list of myFile[] with name and url + setCurrentQuiz(quiz); + setQuestions(quiz.questions); + }) + .catch((err) => console.error(err)); + } + }, [courseName]); return (
@@ -74,20 +88,59 @@ export default function Navbar() {
- {/* {isTA ? ( */} - - - - - - - {/* ) : ( */} - - {/* )} */} + {isTA ? ( +
+ + + + + + + + + + Submissions + Score + + + + {studentList + ? studentList.map((student: string, idx) => ( + + + + + {student} + + + + + + + {currentQuiz.submissions[student]?.score + ? `${currentQuiz.submissions[student]?.score}/${currentQuiz.totalPoints}` + : "Not Graded"} + + + + )) + : "No files found"} + +
+
+ ) : ( + + )}
); diff --git a/pages/[courseName]/quizzes/[quizName]/take.tsx b/pages/[courseName]/quizzes/[quizName]/take.tsx index 93949cd..13ddcc1 100644 --- a/pages/[courseName]/quizzes/[quizName]/take.tsx +++ b/pages/[courseName]/quizzes/[quizName]/take.tsx @@ -7,56 +7,33 @@ import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useState } from "react"; -import { QuizQuestion } from "@/lib/types"; +import { useEffect, useState } from "react"; +import { Quiz, QuizQuestion } from "@/lib/types"; import { useRouter } from "next/router"; import { string } from "zod"; +async function getQuiz(courseName: string, quizName: string): Promise { + const response = await fetch( + `/api/${courseName}/getSAS?filename=quiz/${quizName}` + ); + + if (!response.ok) { + throw new Error("File upload failed"); + } + + const sasURL = (await response.json()).sasURL; + const quiz = await fetch(sasURL); + if (!quiz.ok) { + throw new Error("Failed to fetch quiz"); + } + + return await quiz.json(); +} + export default function QuizTake() { const router = useRouter(); const { courseName, quizName } = router.query; - const [questions, setQustions] = useState([ - { - questionNum: 1, - questionType: "MCQ", - question: "which one is the best cloud service provider", - options: ["AWS", "Azure", "GCP", "Oracle Cloud"], - answer: 2, - points: 10, - }, - { - questionNum: 2, - questionType: "MCQ", - question: "string", - options: ["string1", "string2", "string3", "string4"], - answer: 3, - points: 10, - }, - { - questionNum: 3, - questionType: "MCQ", - question: "string", - options: ["string1", "string2", "string3", "string4"], - answer: 1, - points: 10, - }, - { - questionNum: 4, - questionType: "FRQ", - question: "string", - options: [], - answer: 0, - points: 80, - }, - { - questionNum: 5, - questionType: "FRQ", - question: "string", - options: [], - answer: 0, - points: 80, - }, - ]); + const [questions, setQuestions] = useState([]); const [studentAnswers, setStudentAnswers] = useState(() => { return questions.map((question) => ({ @@ -93,6 +70,17 @@ export default function QuizTake() { console.log(studentAnswers); }; + useEffect(() => { + if (typeof courseName === "string" && typeof quizName === "string") { + getQuiz(courseName, quizName) + .then((quiz) => { + // convert the list of files to a list of myFile[] with name and url + setQuestions(quiz.questions); + }) + .catch((err) => console.error(err)); + } + }, [courseName]); + return (
diff --git a/pages/[courseName]/quizzes/index.tsx b/pages/[courseName]/quizzes/index.tsx index b01e719..7b7ef41 100644 --- a/pages/[courseName]/quizzes/index.tsx +++ b/pages/[courseName]/quizzes/index.tsx @@ -14,12 +14,48 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { Dialog, DialogTrigger } from "@/components/ui/dialog"; import { EditQuizDialog } from "@/components/EditQuizDialog"; +import { File } from "@/lib/types"; + +async function getQuizzes(courseName: string): Promise { + let res = await fetch("/api/" + courseName + "/listFiles", { + method: "GET", + }); + + if (!res.ok) { + throw new Error("Failed to fetch lessons"); + } + + let filesList = await res.json(); + let quizzesList: File[] = filesList.filter( + (file: File) => file.type === "quiz" + ); + // generate a SAS URL for each file and replace the url with the SAS URL + for (let i = 0; i < quizzesList.length; i++) { + const fileName = quizzesList[i].name; + const sasURL = await getSAS(courseName, fileName); + quizzesList[i].url = sasURL; + } + return await quizzesList; +} + +async function getSAS(courseName: string, fileName: string): Promise { + const response = await fetch( + `/api/${courseName}/getSAS?filename=quiz/${fileName}` + ); + + if (!response.ok) { + throw new Error("File upload failed"); + } + + return (await response.json()).sasURL; +} export default function Quizzes() { const [isTA, setIsTA] = useState(false); // TA check is disabled while developing const router = useRouter(); let { courseName } = router.query; const { data: session } = useSession(); + const [quizList, setQuizList] = useState([]); useEffect(() => { courseName = courseName as string; @@ -28,15 +64,18 @@ export default function Quizzes() { } }); + useEffect(() => { + if (typeof courseName === "string") { + getQuizzes(courseName) + .then((quizList) => { + // convert the list of files to a list of myFile[] with name and url + setQuizList(quizList); + }) + .catch((err) => console.error(err)); + } + }, [courseName]); + const currentPath = router.asPath; - // List of the quizzes' file names. - const [quizList, setQuizList] = useState([ - "Quiz_0", - "Quiz_1", - "Quiz_2", - "Quiz_3", - "Quiz_4", - ]); return (
@@ -56,14 +95,14 @@ export default function Quizzes() { {quizList - ? quizList.map((quizName: string) => ( - + ? quizList.map((quiz: File, idx) => ( + - {quizName} + {quiz.name} diff --git a/pages/api/[containerName]/saveQuiz.ts b/pages/api/[containerName]/saveQuiz.ts index cd04f35..8cc7ca5 100644 --- a/pages/api/[containerName]/saveQuiz.ts +++ b/pages/api/[containerName]/saveQuiz.ts @@ -4,7 +4,7 @@ const { ContainerClient } = require("@azure/storage-blob"); const { DefaultAzureCredential } = require("@azure/identity"); import { Quiz } from "@/lib/types"; -export default async function GET( +export default async function handler( req: typeof NextApiRequest, res: typeof NextApiResponse ) { @@ -13,6 +13,7 @@ export default async function GET( try { // Get the Quiz type from the request body const quiz: Quiz = req.body; + console.log(containerName, quiz); const containerClient = await new ContainerClient( process.env.AZURE_STORAGE_URL + `/${containerName}`, @@ -20,14 +21,14 @@ export default async function GET( ); const blockBlobClient = containerClient.getBlockBlobClient( - "quizzes/" + quiz.name + "quiz/" + quiz.name ); console.log(blockBlobClient); const { requestId } = await blockBlobClient.upload( JSON.stringify(quiz), JSON.stringify(quiz).length ); - res.status(200).json({ requestId: requestId }); + res.status(200).json({ requestId: "requestId" }); } catch (error) { res.status(500).json({ error: "Error creating blob" }); } diff --git a/pages/api/[containerName]/student.ts b/pages/api/[containerName]/student.ts new file mode 100644 index 0000000..28f32ad --- /dev/null +++ b/pages/api/[containerName]/student.ts @@ -0,0 +1,37 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import pool from "@/dbUtils/dbPool"; // Import your database connection pool + +type StudentEmailRow = { + email: string; +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { courseName } = req.query; + + if (req.method === "GET") { + try { + const studentEmailsQuery = ` + SELECT u.email + FROM users u + INNER JOIN course_enrollments ce ON u.email = ce.studentEmail + WHERE ce.courseName = $1; + `; + + const result = await pool.query(studentEmailsQuery, [courseName]); + const studentEmails = result.rows.map( + (row: StudentEmailRow) => row.email + ); + + res.status(200).json({ courseName, studentEmails }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Internal Server Error" }); + } + } else { + res.setHeader("Allow", ["GET"]); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +}