diff --git a/backend/._s2v_old b/backend/._s2v_old new file mode 100644 index 0000000..79bd482 Binary files /dev/null and b/backend/._s2v_old differ diff --git a/backend/server.py b/backend/server.py index 3286c32..adb8ca7 100644 --- a/backend/server.py +++ b/backend/server.py @@ -2,9 +2,11 @@ from flask_cors import CORS from pprint import pprint import nltk +import requests import subprocess import os import glob +import logging from sklearn.metrics.pairwise import cosine_similarity from sklearn.feature_extraction.text import TfidfVectorizer @@ -32,6 +34,12 @@ SERVICE_ACCOUNT_FILE = './service_account_key.json' SCOPES = ['https://www.googleapis.com/auth/documents.readonly'] +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +CANVAS_TOKEN = "enter your token here" # Hardcoded for demo +CANVAS_URL = "https://k12.instructure.com" + MCQGen = main.MCQGenerator() answer = main.AnswerPredictor() BoolQGen = main.BoolQGenerator() @@ -459,6 +467,98 @@ def get_transcript(): return jsonify({"transcript": transcript_text}) +@app.route('/export/canvas', methods=['POST']) +def export_to_canvas(): + try: + data = request.json + logger.debug(f"Received data: {data}") + course_id = data.get("course_id") + quiz_data = data.get("quiz") + + # Validate required fields + if not course_id or not quiz_data: + logger.error("Missing course_id or quiz data") + return jsonify({"error": "Missing course_id or quiz data"}), 400 + if not quiz_data.get("title"): + logger.error("Missing quiz title") + return jsonify({"error": "Missing quiz title"}), 400 + if not quiz_data.get("questions"): + logger.error("Missing quiz questions") + return jsonify({"error": "Missing quiz questions"}), 400 + + headers = {"Authorization": f"Bearer {CANVAS_TOKEN}"} + logger.debug(f"Headers: {headers}") + + # Step 1: Create the quiz in Canvas + quiz_payload = { + "quiz[title]": quiz_data["title"], + "quiz[description]": quiz_data.get("description", "Generated by EduAid"), # Use provided description or default + "quiz[quiz_type]": "assignment", + "quiz[published]": True + } + quiz_url = f"{CANVAS_URL}/api/v1/courses/{course_id}/quizzes" + logger.debug(f"Creating quiz at: {quiz_url}") + quiz_response = requests.post(quiz_url, headers=headers, data=quiz_payload) + + if quiz_response.status_code not in (200, 201): + logger.error(f"Failed to create quiz: {quiz_response.status_code} - {quiz_response.text}") + return jsonify({"error": "Failed to create quiz in Canvas", "details": quiz_response.text}), 500 + + quiz_id = quiz_response.json()["id"] + logger.debug(f"Quiz created with ID: {quiz_id}") + + # Step 2: Add questions to the quiz + question_url = f"{CANVAS_URL}/api/v1/courses/{course_id}/quizzes/{quiz_id}/questions" + for q in quiz_data["questions"]: + logger.debug(f"Processing question: {q}") + if q["question_type"] == "MCQ": + options = q.get("options", []) + if q["answer"] not in options: + options.append(q["answer"]) # Ensure answer is in options + question_payload = { + "question[question_text]": q["question"], + "question[question_type]": "multiple_choice_question", + "question[points_possible]": 1, + } + # Dynamically add all options + for i, option in enumerate(options): + question_payload[f"question[answers][{i}][text]"] = option + question_payload[f"question[answers][{i}][weight]"] = 100 if option == q["answer"] else 0 + + elif q["question_type"] == "Boolean": + question_payload = { + "question[question_text]": q["question"], + "question[question_type]": "true_false_question", + "question[points_possible]": 1, + "question[answers][0][text]": "True", + "question[answers][0][weight]": 100 if q["answer"].lower() == "true" else 0, + "question[answers][1][text]": "False", + "question[answers][1][weight]": 100 if q["answer"].lower() == "false" else 0, + } + elif q["question_type"] == "Short": + question_payload = { + "question[question_text]": q["question"], + "question[question_type]": "essay_question", + "question[points_possible]": 1 + } + else: + logger.warning(f"Skipping unsupported question type: {q['question_type']}") + continue + + logger.debug(f"Posting question: {question_payload}") + question_response = requests.post(question_url, headers=headers, data=question_payload) + if question_response.status_code not in (200, 201): + logger.error(f"Failed to add question: {question_response.status_code} - {question_response.text}") + return jsonify({"error": "Failed to add question", "details": question_response.text}), 500 + + quiz_link = f"{CANVAS_URL}/courses/{course_id}/quizzes/{quiz_id}" + logger.info(f"Quiz exported successfully: {quiz_link}") + return jsonify({"message": "Quiz exported to Canvas", "url": quiz_link}) + + except Exception as e: + logger.exception("An error occurred in export_to_canvas") + return jsonify({"error": "Server error", "details": str(e)}), 500 + if __name__ == "__main__": os.makedirs("subtitles", exist_ok=True) app.run() diff --git a/eduaid_web/package-lock.json b/eduaid_web/package-lock.json index 265257c..584eec7 100644 --- a/eduaid_web/package-lock.json +++ b/eduaid_web/package-lock.json @@ -11,6 +11,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "dotenv": "^16.4.7", "pdf-lib": "^1.17.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -7136,11 +7137,14 @@ } }, "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { @@ -15121,6 +15125,14 @@ } } }, + "node_modules/react-scripts/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/react-switch": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", diff --git a/eduaid_web/src/pages/Output.jsx b/eduaid_web/src/pages/Output.jsx index 28d91cd..c9c95b3 100644 --- a/eduaid_web/src/pages/Output.jsx +++ b/eduaid_web/src/pages/Output.jsx @@ -4,26 +4,51 @@ import "../index.css"; import logo from "../assets/aossie_logo.png"; import logoPNG from "../assets/aossie_logo_transparent.png"; - const Output = () => { const [qaPairs, setQaPairs] = useState([]); + const [editingIndex, setEditingIndex] = useState(null); // Track which question is being edited +const [editedPairs, setEditedPairs] = useState([]); // Store edited versions of qaPairs +const [newQuestion, setNewQuestion] = useState({ + question: "", + question_type: "Short", + options: [], + answer: "", + context: "", +}); const [questionType, setQuestionType] = useState( localStorage.getItem("selectedQuestionType") ); + //Canvas export form state + const [canvasForm, setCanvasForm] = useState({ + courseId: "", + title: "EduAid Generated Quiz", + description: "Generated by EduAid", + }); + const [quizUrl, setQuizUrl] = useState(""); const [pdfMode, setPdfMode] = useState("questions"); + const [courseId, setCourseId] = useState(""); // New state for Canvas course ID + const [exportStatus, setExportStatus] = useState(""); // Status feedback for Canvas export + + const handleCanvasFormChange = (e) => { + const { name, value } = e.target; + setCanvasForm((prev) => ({ ...prev, [name]: value })); + }; useEffect(() => { const handleClickOutside = (event) => { - const dropdown = document.getElementById('pdfDropdown'); - if (dropdown && !dropdown.contains(event.target) && - !event.target.closest('button')) { - dropdown.classList.add('hidden'); - } + const dropdown = document.getElementById("pdfDropdown"); + if ( + dropdown && + !dropdown.contains(event.target) && + !event.target.closest("button") + ) { + dropdown.classList.add("hidden"); + } }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); -}, []); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { @@ -96,20 +121,83 @@ const Output = () => { } setQaPairs(combinedQaPairs); + setEditedPairs([...combinedQaPairs]); } }, []); + const startEditing = (index) => { + setEditingIndex(index); + }; + + const saveEdit = (index) => { + setEditingIndex(null); + setQaPairs([...editedPairs]); // Sync qaPairs with editedPairs for export + localStorage.setItem("qaPairs", JSON.stringify(editedPairs)); // Update localStorage + }; + + const handleEditChange = (index, field, value) => { + const updatedPairs = [...editedPairs]; + updatedPairs[index] = { ...updatedPairs[index], [field]: value }; + setEditedPairs(updatedPairs); + }; + + const handleOptionChange = (index, optionIdx, value) => { + const updatedPairs = [...editedPairs]; + const options = [...updatedPairs[index].options]; + options[optionIdx] = value; + updatedPairs[index].options = options; + setEditedPairs(updatedPairs); + }; + + const deleteQuestion = (index) => { + const updatedPairs = editedPairs.filter((_, i) => i !== index); + setEditedPairs(updatedPairs); + setQaPairs(updatedPairs); + localStorage.setItem("qaPairs", JSON.stringify(updatedPairs)); + }; + + const addOption = (index) => { + const updatedPairs = [...editedPairs]; + updatedPairs[index].options = [...(updatedPairs[index].options || []), ""]; + setEditedPairs(updatedPairs); + }; + + const removeOption = (index, optionIdx) => { + const updatedPairs = [...editedPairs]; + updatedPairs[index].options = updatedPairs[index].options.filter((_, i) => i !== optionIdx); + setEditedPairs(updatedPairs); + }; + + const handleNewQuestionChange = (field, value) => { + setNewQuestion((prev) => ({ ...prev, [field]: value })); + }; + + const addNewQuestion = () => { + if (!newQuestion.question || !newQuestion.answer) { + alert("Please fill in the question and answer!"); + return; + } + const updatedPairs = [...editedPairs, { ...newQuestion }]; + setEditedPairs(updatedPairs); + setQaPairs(updatedPairs); + localStorage.setItem("qaPairs", JSON.stringify(updatedPairs)); + setNewQuestion({ question: "", question_type: "Short", options: [], answer: "", context: "" }); + }; + const generateGoogleForm = async () => { - const response = await fetch(`${process.env.REACT_APP_BASE_URL}/generate_gform`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - qa_pairs: qaPairs, - question_type: questionType, - }), - }); + const response = await fetch( + `${process.env.REACT_APP_BASE_URL}/generate_gform`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + qa_pairs: qaPairs, + question_type: questionType, + }), + } + ); if (response.ok) { const result = await response.json(); @@ -126,7 +214,7 @@ const Output = () => { const arrayBuffer = await response.arrayBuffer(); return new Uint8Array(arrayBuffer); } catch (error) { - console.error('Error loading logo:', error); + console.error("Error loading logo:", error); return null; } }; @@ -135,321 +223,547 @@ const Output = () => { const pageWidth = 595.28; const pageHeight = 841.89; const margin = 50; - const maxContentWidth = pageWidth - (2 * margin); - const maxContentHeight = pageHeight - (2 * margin); - + const maxContentWidth = pageWidth - 2 * margin; + const maxContentHeight = pageHeight - 2 * margin; + const pdfDoc = await PDFDocument.create(); let page = pdfDoc.addPage([pageWidth, pageHeight]); const d = new Date(Date.now()); - // Load and embed logo const logoBytes = await loadLogoAsBytes(); let logoImage; if (logoBytes) { try { logoImage = await pdfDoc.embedPng(logoBytes); - const logoDims = logoImage.scale(0.2); // Scale down the logo + const logoDims = logoImage.scale(0.2); page.drawImage(logoImage, { x: margin, y: pageHeight - margin - 30, width: logoDims.width, height: logoDims.height, }); - // Adjust title position to be next to the logo - page.drawText('EduAid generated Quiz', { + page.drawText("EduAid generated Quiz", { x: margin + logoDims.width + 10, y: pageHeight - margin, - size: 20 + size: 20, }); - page.drawText('Created On: ' + d.toString(), { + page.drawText("Created On: " + d.toString(), { x: margin + logoDims.width + 10, y: pageHeight - margin - 30, - size: 10 + size: 10, }); } catch (error) { - console.error('Error embedding logo:', error); - // Fallback to text-only header if logo embedding fails - page.drawText('EduAid generated Quiz', { + console.error("Error embedding logo:", error); + page.drawText("EduAid generated Quiz", { x: margin, y: pageHeight - margin, - size: 20 + size: 20, }); - page.drawText('Created On: ' + d.toString(), { + page.drawText("Created On: " + d.toString(), { x: margin, y: pageHeight - margin - 30, - size: 10 + size: 10, }); } } - - + const form = pdfDoc.getForm(); let y = pageHeight - margin - 70; let questionIndex = 1; const createNewPageIfNeeded = (requiredHeight) => { - if (y - requiredHeight < margin) { - page = pdfDoc.addPage([pageWidth, pageHeight]); - y = pageHeight - margin; - return true; - } - return false; + if (y - requiredHeight < margin) { + page = pdfDoc.addPage([pageWidth, pageHeight]); + y = pageHeight - margin; + return true; + } + return false; }; const wrapText = (text, maxWidth) => { - const words = text.split(' '); + const words = text.split(" "); const lines = []; - let currentLine = ''; - - words.forEach(word => { - const testLine = currentLine ? `${currentLine} ${word}` : word; - - // Adjust the multiplier to reflect a more realistic line width based on font size - const testWidth = testLine.length * 6; // Update the multiplier for better wrapping. - - if (testWidth > maxWidth) { - lines.push(currentLine); - currentLine = word; - } else { - currentLine = testLine; - } + let currentLine = ""; + + words.forEach((word) => { + const testLine = currentLine ? `${currentLine} ${word}` : word; + const testWidth = testLine.length * 6; + if (testWidth > maxWidth) { + lines.push(currentLine); + currentLine = word; + } else { + currentLine = testLine; + } }); - + if (currentLine) { - lines.push(currentLine); + lines.push(currentLine); } - + return lines; - }; - + }; qaPairs.forEach((qaPair) => { - let requiredHeight = 60; - const questionLines = wrapText(qaPair.question, maxContentWidth); - requiredHeight += questionLines.length * 20; - - if (mode !== 'answers') { - if (qaPair.question_type === "Boolean") { - requiredHeight += 60; - } else if (qaPair.question_type === "MCQ" || qaPair.question_type === "MCQ_Hard") { - const optionsCount = qaPair.options ? qaPair.options.length + 1 : 1; - requiredHeight += optionsCount * 25; - } else { - requiredHeight += 40; - } - } + let requiredHeight = 60; + const questionLines = wrapText(qaPair.question, maxContentWidth); + requiredHeight += questionLines.length * 20; - if (mode === 'answers' || mode === 'questions_answers') { - requiredHeight += 40; + if (mode !== "answers") { + if (qaPair.question_type === "Boolean") { + requiredHeight += 60; + } else if ( + qaPair.question_type === "MCQ" || + qaPair.question_type === "MCQ_Hard" + ) { + const optionsCount = qaPair.options ? qaPair.options.length + 1 : 1; + requiredHeight += optionsCount * 25; + } else { + requiredHeight += 40; } + } - createNewPageIfNeeded(requiredHeight); + if (mode === "answers" || mode === "questions_answers") { + requiredHeight += 40; + } - if (mode !== 'answers') { - questionLines.forEach((line, lineIndex) => { - const textToDraw = lineIndex === 0 - ? `Q${questionIndex}) ${line}` // First line includes question number - : ` ${line}`; // Subsequent lines are indented - page.drawText(textToDraw, { - x: margin, - y: y - (lineIndex * 20), - size: 12, - maxWidth: maxContentWidth - }); + createNewPageIfNeeded(requiredHeight); + + if (mode !== "answers") { + questionLines.forEach((line, lineIndex) => { + const textToDraw = + lineIndex === 0 + ? `Q${questionIndex}) ${line}` + : ` ${line}`; + page.drawText(textToDraw, { + x: margin, + y: y - lineIndex * 20, + size: 12, + maxWidth: maxContentWidth, }); - y -= (questionLines.length * 20 + 20); - - if (mode === 'questions') { - if (qaPair.question_type === "Boolean") { - const radioGroup = form.createRadioGroup(`question${questionIndex}_answer`); - ['True', 'False'].forEach((option) => { - const radioOptions = { - x: margin + 20, - y, - width: 15, - height: 15, - }; - radioGroup.addOptionToPage(option, page, radioOptions); - page.drawText(option, { x: margin + 40, y: y + 2, size: 12 }); - y -= 20; - }); - } else if (qaPair.question_type === "MCQ" || qaPair.question_type === "MCQ_Hard") { - const allOptions = [...(qaPair.options || [])]; - if (qaPair.answer && !allOptions.includes(qaPair.answer)) { - allOptions.push(qaPair.answer); - } - const shuffledOptions = shuffleArray([...allOptions]); - - const radioGroup = form.createRadioGroup(`question${questionIndex}_answer`); - shuffledOptions.forEach((option, index) => { - const radioOptions = { - x: margin + 20, - y, - width: 15, - height: 15, - }; - radioGroup.addOptionToPage(`option${index}`, page, radioOptions); - const optionLines = wrapText(option, maxContentWidth - 60); - optionLines.forEach((line, lineIndex) => { - page.drawText(line, { - x: margin + 40, - y: y + 2 - (lineIndex * 15), - size: 12 - }); - }); - y -= Math.max(25, optionLines.length * 20); - }); - } else if (qaPair.question_type === "Short") { - const answerField = form.createTextField(`question${questionIndex}_answer`); - answerField.setText(""); - answerField.addToPage(page, { - x: margin, - y: y - 20, - width: maxContentWidth, - height: 20 - }); - y -= 40; - } + }); + y -= questionLines.length * 20 + 20; + + if (mode === "questions") { + if (qaPair.question_type === "Boolean") { + const radioGroup = form.createRadioGroup( + `question${questionIndex}_answer` + ); + ["True", "False"].forEach((option) => { + const radioOptions = { + x: margin + 20, + y, + width: 15, + height: 15, + }; + radioGroup.addOptionToPage(option, page, radioOptions); + page.drawText(option, { x: margin + 40, y: y + 2, size: 12 }); + y -= 20; + }); + } else if ( + qaPair.question_type === "MCQ" || + qaPair.question_type === "MCQ_Hard" + ) { + const allOptions = [...(qaPair.options || [])]; + if (qaPair.answer && !allOptions.includes(qaPair.answer)) { + allOptions.push(qaPair.answer); } - } + const shuffledOptions = shuffleArray([...allOptions]); - if (mode === 'answers' || mode === 'questions_answers') { - const answerText = `Answer ${questionIndex}: ${qaPair.answer}`; - const answerLines = wrapText(answerText, maxContentWidth); - answerLines.forEach((line, lineIndex) => { + const radioGroup = form.createRadioGroup( + `question${questionIndex}_answer` + ); + shuffledOptions.forEach((option, index) => { + const radioOptions = { + x: margin + 20, + y, + width: 15, + height: 15, + }; + radioGroup.addOptionToPage(`option${index}`, page, radioOptions); + const optionLines = wrapText(option, maxContentWidth - 60); + optionLines.forEach((line, lineIndex) => { page.drawText(line, { - x: margin, - y: y - (lineIndex * 15), - size: 12, - color: rgb(0, 0.5, 0) + x: margin + 40, + y: y + 2 - lineIndex * 15, + size: 12, }); + }); + y -= Math.max(25, optionLines.length * 20); }); - y -= answerLines.length * 20; + } else if (qaPair.question_type === "Short") { + const answerField = form.createTextField( + `question${questionIndex}_answer` + ); + answerField.setText(""); + answerField.addToPage(page, { + x: margin, + y: y - 20, + width: maxContentWidth, + height: 20, + }); + y -= 40; + } } + } - y -= 20; - questionIndex += 1; + if (mode === "answers" || mode === "questions_answers") { + const answerText = `Answer ${questionIndex}: ${qaPair.answer}`; + const answerLines = wrapText(answerText, maxContentWidth); + answerLines.forEach((line, lineIndex) => { + page.drawText(line, { + x: margin, + y: y - lineIndex * 15, + size: 12, + color: rgb(0, 0.5, 0), + }); + }); + y -= answerLines.length * 20; + } + + y -= 20; + questionIndex += 1; }); const pdfBytes = await pdfDoc.save(); - const blob = new Blob([pdfBytes], { type: 'application/pdf' }); - const link = document.createElement('a'); + const blob = new Blob([pdfBytes], { type: "application/pdf" }); + const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "generated_questions.pdf"; document.body.appendChild(link); link.click(); document.body.removeChild(link); - document.getElementById('pdfDropdown').classList.add('hidden'); -}; + document.getElementById("pdfDropdown").classList.add("hidden"); + }; + + // New function to export quiz to Canvas + const generateCanvasQuiz = async () => { + const { courseId, title, description } = canvasForm; + if (!courseId) { + setExportStatus("Please enter a Canvas Course ID."); + return; + } + + setExportStatus("Exporting to Canvas..."); + try { + const response = await fetch(`${process.env.REACT_APP_BASE_URL}/export/canvas`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + course_id: courseId, + quiz: { + title, + description, + questions: qaPairs, + }, + }), + }); + const result = await response.json(); + if (response.ok) { + setExportStatus("Success! Quiz exported:"); + setQuizUrl(result.url); + window.open(result.url, "_blank"); + } else { + setExportStatus(`Error: ${result.error}`); + } + } catch (error) { + setExportStatus("Failed to connect to server."); + console.error("Canvas export error:", error); + } + }; return ( -
Question {index + 1}
+{qaPair.question}
+ {qaPair.question_type !== "Boolean" && ( + <> +Answer
+{qaPair.answer}
+ {qaPair.options && qaPair.options.length > 0 && ( ++ Option {idx + 1}: {option} +
+ ))} ++ {exportStatus}{" "} + {quizUrl && ( + + {quizUrl} + + )} +
+)} + +