From 79aff4b67b3b0e73d45596ed33f46c642329e699 Mon Sep 17 00:00:00 2001 From: Prateek Date: Thu, 26 Feb 2026 00:26:17 +0530 Subject: [PATCH 1/9] feat: full UI/UX redesign + new pages and backend fixes - Landing.jsx: hero with animated orbs, features grid, HOW IT WORKS section, CTA + footer - Header.jsx: sticky glassmorphism nav with mobile hamburger menu - Text_Input.jsx: drag-and-drop zone, inline type/mode/difficulty selectors, clipboard paste - InteractiveQuiz.jsx: progress bar, option badges, correct/incorrect feedback, score + completion screen - Previous.jsx: card-based history list with empty state, difficulty color coding - StaticQuiz.jsx: renamed from Output.jsx, removed inline logo header, updated to new design system - Question_Type.jsx: 2x2 card grid with icons, animated selection state, disabled CTA when unselected - Upload.jsx, QuizModeWrapper.jsx: new routing pages - App.js: AppLayout wrapper to conditionally show Header (hidden on Landing) - backend/server.py: safe NLTK downloads, GoogleDocsService try/except, HF cache env support - backend/Generator/mcq.py, question_filters.py: quiet NLTK download calls --- backend/Generator/mcq.py | 14 +- backend/Generator/question_filters.py | 18 +- backend/server.py | 42 +- eduaid_web/src/App.js | 31 +- eduaid_web/src/components/Header.jsx | 95 ++++ eduaid_web/src/pages/InteractiveQuiz.jsx | 170 +++++++ eduaid_web/src/pages/Landing.jsx | 125 ++++++ eduaid_web/src/pages/Previous.jsx | 168 +++---- eduaid_web/src/pages/Question_Type.jsx | 187 +++++--- eduaid_web/src/pages/QuizModeWrapper.jsx | 17 + .../src/pages/{Output.jsx => StaticQuiz.jsx} | 44 +- eduaid_web/src/pages/Text_Input.jsx | 421 ++++++++++-------- eduaid_web/src/pages/Upload.jsx | 9 + 13 files changed, 960 insertions(+), 381 deletions(-) create mode 100644 eduaid_web/src/components/Header.jsx create mode 100644 eduaid_web/src/pages/InteractiveQuiz.jsx create mode 100644 eduaid_web/src/pages/Landing.jsx create mode 100644 eduaid_web/src/pages/QuizModeWrapper.jsx rename eduaid_web/src/pages/{Output.jsx => StaticQuiz.jsx} (90%) create mode 100644 eduaid_web/src/pages/Upload.jsx diff --git a/backend/Generator/mcq.py b/backend/Generator/mcq.py index e2c82954..982e1ddb 100644 --- a/backend/Generator/mcq.py +++ b/backend/Generator/mcq.py @@ -8,9 +8,17 @@ from sense2vec import Sense2Vec from similarity.normalized_levenshtein import NormalizedLevenshtein -nltk.download('brown') -nltk.download('stopwords') -nltk.download('popular') +def _safe_nltk_download(pkg): + try: + nltk.data.find(pkg) + except LookupError: + try: + nltk.download(pkg, quiet=True, raise_on_error=False) + except Exception: + pass + +_safe_nltk_download('corpora/brown') +_safe_nltk_download('corpora/stopwords') def is_word_available(word, s2v_model): word = word.replace(" ", "_") diff --git a/backend/Generator/question_filters.py b/backend/Generator/question_filters.py index 39424f62..4f5e5923 100644 --- a/backend/Generator/question_filters.py +++ b/backend/Generator/question_filters.py @@ -6,10 +6,20 @@ # Initialize NLTK resources import nltk -nltk.download('punkt', quiet=True) -nltk.download('averaged_perceptron_tagger_eng', quiet=True) -nltk.download('wordnet', quiet=True) -nltk.download('stopwords', quiet=True) + +def _safe_nltk_download(pkg): + try: + nltk.data.find(pkg) + except LookupError: + try: + nltk.download(pkg.split('/')[-1], quiet=True, raise_on_error=False) + except Exception: + pass + +_safe_nltk_download('tokenizers/punkt') +_safe_nltk_download('taggers/averaged_perceptron_tagger_eng') +_safe_nltk_download('corpora/wordnet') +_safe_nltk_download('corpora/stopwords') class QuestionEnhancer: def __init__(self): diff --git a/backend/server.py b/backend/server.py index 1c9efaa4..04753999 100644 --- a/backend/server.py +++ b/backend/server.py @@ -8,8 +8,17 @@ from sklearn.metrics.pairwise import cosine_similarity from sklearn.feature_extraction.text import TfidfVectorizer -nltk.download("stopwords") -nltk.download('punkt_tab') +def _safe_nltk_download(pkg): + try: + nltk.data.find(pkg) + except LookupError: + try: + nltk.download(pkg, quiet=True, raise_on_error=False) + except Exception: + pass + +_safe_nltk_download('corpora/stopwords') +_safe_nltk_download('tokenizers/punkt_tab') from Generator import main from Generator.question_filters import make_question_harder import re @@ -38,7 +47,11 @@ BoolQGen = main.BoolQGenerator() ShortQGen = main.ShortQGenerator() qg = main.QuestionGenerator() -docs_service = main.GoogleDocsService(SERVICE_ACCOUNT_FILE, SCOPES) +try: + docs_service = main.GoogleDocsService(SERVICE_ACCOUNT_FILE, SCOPES) +except Exception as e: + print(f"Warning: GoogleDocsService unavailable (invalid service account key): {e}") + docs_service = None file_processor = main.FileProcessor() mediawikiapi = MediaWikiAPI() qa_model = pipeline("question-answering") @@ -187,7 +200,7 @@ def get_content(): if not document_url: return jsonify({'error': 'Document URL is required'}), 400 - text = docs_service.get_document_content(document_url) + text = docs_service.get_document_content(document_url) if docs_service else (_ for _ in ()).throw(Exception("Google Docs service is not configured. Please provide a valid service account key.")) return jsonify(text) except ValueError as e: return jsonify({'error': str(e)}), 400 @@ -421,19 +434,36 @@ def get_boolq_hard(): @app.route('/upload', methods=['POST']) def upload_file(): + # Debug logging to help diagnose upload issues + try: + print("/upload called. request.files keys:", list(request.files.keys())) + except Exception as e: + print("Could not read request.files:", e) + if 'file' not in request.files: + print("Upload error: no 'file' in request.files") return jsonify({"error": "No file part"}), 400 file = request.files['file'] + print(f"Received file object: filename={getattr(file, 'filename', None)}, content_type={getattr(file, 'content_type', None)}") if file.filename == '': + print("Upload error: empty filename") return jsonify({"error": "No selected file"}), 400 - content = file_processor.process_file(file) - + try: + content = file_processor.process_file(file) + except Exception as e: + # Log exception and return useful JSON for debugging + print("Exception while processing file:") + import traceback + traceback.print_exc() + return jsonify({"error": "Exception processing file", "detail": str(e)}), 500 + if content: return jsonify({"content": content}) else: + print("File processing returned no content or unsupported type") return jsonify({"error": "Unsupported file type or error processing file"}), 400 @app.route("/", methods=["GET"]) diff --git a/eduaid_web/src/App.js b/eduaid_web/src/App.js index e9eef0a0..275a039f 100644 --- a/eduaid_web/src/App.js +++ b/eduaid_web/src/App.js @@ -1,23 +1,42 @@ import "./App.css"; -import { Routes, Route, HashRouter } from "react-router-dom"; +import { Routes, Route, HashRouter, useLocation } from "react-router-dom"; import Home from "./pages/Home"; +import Landing from "./pages/Landing"; import Question_Type from "./pages/Question_Type"; import Text_Input from "./pages/Text_Input"; -import Output from "./pages/Output"; +import StaticQuiz from "./pages/StaticQuiz"; import Previous from "./pages/Previous"; import NotFound from "./pages/PageNotFound"; +import Upload from "./pages/Upload"; +import Header from "./components/Header"; +import QuizModeWrapper from "./pages/QuizModeWrapper"; -function App() { +// Show the sticky header on all pages except the landing page (which has its own full-screen hero) +const AppLayout = () => { + const { pathname } = useLocation(); + const showHeader = pathname !== "/"; return ( - + <> + {showHeader &&
} - } /> + } /> + } /> } /> } /> - } /> + } /> + } /> + } /> } /> } /> + + ); +}; + +function App() { + return ( + + ); } diff --git a/eduaid_web/src/components/Header.jsx b/eduaid_web/src/components/Header.jsx new file mode 100644 index 00000000..94330a4b --- /dev/null +++ b/eduaid_web/src/components/Header.jsx @@ -0,0 +1,95 @@ +import React, { useState } from "react"; +import { Link, useLocation } from "react-router-dom"; +import "../index.css"; +import logo from "../assets/aossie_logo_transparent.png"; + +const navLinks = [ + { to: "/", label: "Home" }, + { to: "/upload", label: "Generate" }, + { to: "/history", label: "History" }, +]; + +const Header = () => { + const location = useLocation(); + const [open, setOpen] = useState(false); + + const isActive = (path) => + path === "/" ? location.pathname === "/" : location.pathname.startsWith(path); + + return ( +
+ + + {/* Mobile Menu */} +
+
+ {navLinks.map(({ to, label }) => ( + setOpen(false)} + className={`px-4 py-3 rounded-xl text-sm font-semibold transition-all ${ + isActive(to) + ? "bg-[#7600F2]/20 text-[#c084fc]" + : "text-[#a0aec0] hover:text-white hover:bg-white/[0.06]" + }`} + > + {label} + + ))} + setOpen(false)}> + + +
+
+
+ ); +}; + +export default Header; diff --git a/eduaid_web/src/pages/InteractiveQuiz.jsx b/eduaid_web/src/pages/InteractiveQuiz.jsx new file mode 100644 index 00000000..4ccf3723 --- /dev/null +++ b/eduaid_web/src/pages/InteractiveQuiz.jsx @@ -0,0 +1,170 @@ +import React, { useState } from "react"; +import { useLocation, useNavigate, Link } from "react-router-dom"; +import logo from "../assets/aossie_logo_transparent.png"; + +const InteractiveQuiz = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { questions = [] } = location.state || {}; + + const [currentIndex, setCurrentIndex] = useState(0); + const [selected, setSelected] = useState(null); + const [submitted, setSubmitted] = useState(false); + const [score, setScore] = useState(0); + const [finished, setFinished] = useState(false); + + if (questions.length === 0) { + return ( +
+
+
+

No Questions Found

+

There are no questions available for this quiz.

+ +
+
+ ); + } + + if (finished) { + const pct = Math.round((score / questions.length) * 100); + return ( +
+
+
+
+
+
+
{pct >= 80 ? "" : pct >= 50 ? "" : ""}
+

Quiz Complete!

+

Here's how you did:

+
+
+
{score}/{questions.length}
+
Correct
+
+
+
+
{pct}%
+
Score
+
+
+ {/* Progress bar */} +
+
+
+
+ + +
+
+
+ ); + } + + const q = questions[currentIndex]; + const options = q.options ? [...q.options] : []; + if (q.answer && !options.includes(q.answer)) options.push(q.answer); + const correctIdx = options.indexOf(q.answer ?? options[q.correctAnswerIndex]); + const progress = ((currentIndex) / questions.length) * 100; + + const handleSubmit = () => { + if (selected === null) return; + setSubmitted(true); + if (selected === correctIdx) setScore((s) => s + 1); + }; + + const handleNext = () => { + setSelected(null); + setSubmitted(false); + if (currentIndex + 1 >= questions.length) setFinished(true); + else setCurrentIndex((i) => i + 1); + }; + + return ( +
+
+
+
+
+ +
+ {/* Top bar */} +
+ + EduAid + EduAid + +
+ Question + {currentIndex + 1} / {questions.length} +
{score} pts
+
+
+ + {/* Progress bar */} +
+
+
+ + {/* Question card */} +
+
Question {currentIndex + 1}
+

{q.question || q.question_statement}

+
+ + {/* Options */} +
+ {options.map((opt, i) => { + const isCorrect = i === correctIdx; + const isSelected = i === selected; + let cls = "p-4 rounded-xl border text-left text-sm sm:text-base font-medium transition-all duration-200 cursor-pointer "; + if (!submitted) { + cls += isSelected + ? "bg-[#7600F2]/20 border-[#7600F2] text-white" + : "bg-white/[0.03] border-white/[0.08] text-[#a0aec0] hover:bg-white/[0.07] hover:border-white/20 hover:text-white"; + } else { + if (isCorrect) cls += "bg-green-500/20 border-green-500 text-green-300"; + else if (isSelected) cls += "bg-red-500/20 border-red-500 text-red-300"; + else cls += "bg-white/[0.02] border-white/[0.05] text-[#4a5568]"; + } + return ( + + ); + })} +
+ + {/* Feedback + Actions */} + {submitted && ( +
+ {selected === correctIdx ? "" : ""} + {selected === correctIdx ? "Correct! Well done." : `Incorrect. The correct answer is: "${options[correctIdx]}"`} +
+ )} + +
+ {!submitted ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default InteractiveQuiz; diff --git a/eduaid_web/src/pages/Landing.jsx b/eduaid_web/src/pages/Landing.jsx new file mode 100644 index 00000000..ae7deaaf --- /dev/null +++ b/eduaid_web/src/pages/Landing.jsx @@ -0,0 +1,125 @@ +import React, { useEffect, useRef } from "react"; +import { Link } from "react-router-dom"; +import "../index.css"; +import logo from "../assets/aossie_logo_transparent.png"; + +const features = [ + { icon: "", title: "PDF & Documents", desc: "Upload PDFs, Word docs, or plain text and instantly generate quiz questions from any content." }, + { icon: "", title: "AI-Powered Questions", desc: "Our T5-based models craft MCQs, True/False, and short-answer questions with high accuracy." }, + { icon: "", title: "Interactive Quiz Mode", desc: "Take quizzes one question at a time with instant feedback and a live score tracker." }, + { icon: "", title: "Export & Share", desc: "Download questions as PDF or generate a shareable Google Form with one click." }, + { icon: "", title: "Quiz History", desc: "Revisit any of your recent quizzes — your last sessions are always saved locally." }, + { icon: "", title: "Google Docs Support", desc: "Paste a Google Docs link and generate questions directly from your shared documents." }, +]; + +const steps = [ + { num: "01", label: "Upload Content", desc: "Add a PDF, paste text, or link a Google Doc." }, + { num: "02", label: "Configure Quiz", desc: "Choose difficulty, question count, and quiz mode." }, + { num: "03", label: "Generate & Learn", desc: "Get AI-generated questions instantly and start learning." }, +]; + +const Landing = () => { + return ( +
+ {/* Ambient orbs */} +
+
+
+
+
+ + {/* HERO */} +
+
+ + AI-Powered Quiz Generator +
+
+ EduAid + EduAid +
+

+ Turn Any Document Into{" "} + Smart Quizzes +

+

+ Upload a PDF, paste text, or link a Google Doc. EduAid instantly generates MCQs, True/False, and short-answer questions — ready to study, export, or share. +

+
+ + + + + + +
+
+ {["No account needed", "Export to PDF & Google Forms", "MCQ True/False Short Answer", "Interactive quiz mode"].map((t) => ( + + + {t} + + ))} +
+
+ + {/* HOW IT WORKS */} +
+
+

Simple Process

+

Three steps to smarter studying

+
+
+ {steps.map((s, i) => ( +
+
{s.num}
+

{s.label}

+

{s.desc}

+
+ ))} +
+
+ + {/* FEATURES */} +
+
+

Features

+

Everything you need to learn faster

+
+
+ {features.map((f, i) => ( +
+
{f.icon}
+

{f.title}

+

{f.desc}

+
+ ))} +
+
+ + {/* BOTTOM CTA */} +
+
+

Ready to generate your first quiz?

+

Upload any document and get AI-generated questions in seconds.

+ + + +
+
+ + {/* FOOTER */} +
+

2026 EduAid Built with for learners everywhere

+
+
+ ); +}; + +export default Landing; diff --git a/eduaid_web/src/pages/Previous.jsx b/eduaid_web/src/pages/Previous.jsx index 404f11fb..6db2ded7 100644 --- a/eduaid_web/src/pages/Previous.jsx +++ b/eduaid_web/src/pages/Previous.jsx @@ -1,110 +1,116 @@ -import React from "react"; -import "../index.css"; +import React from "react"; +import { useNavigate } from "react-router-dom"; import logoPNG from "../assets/aossie_logo_transparent.png"; -import stars from "../assets/stars.png"; -import { FaArrowRight } from "react-icons/fa"; import { Link } from "react-router-dom"; -import { useNavigate } from 'react-router-dom'; + +const difficultyColor = (d = "") => { + if (!d) return "text-[#718096]"; + const l = d.toLowerCase(); + if (l.includes("easy")) return "text-green-400"; + if (l.includes("medium")) return "text-yellow-400"; + if (l.includes("hard")) return "text-red-400"; + return "text-[#c084fc]"; +}; + +const modeIcon = (mode) => (mode === "interactive" ? "" : ""); const Previous = () => { const navigate = useNavigate(); - const getQuizzesFromLocalStorage = () => { - const quizzes = localStorage.getItem("last5Quizzes"); - return quizzes ? JSON.parse(quizzes) : []; + const getQuizzes = () => { + try { return JSON.parse(localStorage.getItem("last5Quizzes")) || []; } + catch { return []; } }; - const [quizzes, setQuizzes] = React.useState(getQuizzesFromLocalStorage()); + const [quizzes, setQuizzes] = React.useState(getQuizzes); const handleQuizClick = (quiz) => { - localStorage.setItem("qaPairs", JSON.stringify(quiz.qaPair)); - navigate('/output'); + navigate("/quiz", { state: { mode: quiz.mode || "static", questions: quiz.qaPair } }); }; - const handleClearQuizzes = () => { + const handleClear = () => { localStorage.removeItem("last5Quizzes"); setQuizzes([]); }; - const handleBack = () => { - navigate('/'); - }; - return ( -
-
- {/* Header */} - - logo -
- - Edu - - - Aid - -
- - - {/* Titles */} -
-
Quiz Dashboard
-
- Your{" "} - - Generated Quizzes - - stars -
-
+
+ {/* Orbs */} +
+
+
+
- {/* Subheading */} -
- - Your Quizzes - +
+ {/* Page Header */} +
+

Your Activity

+

Quiz History

+

Revisit and re-take your recently generated quizzes.

- {/* Quiz List */} -
- {quizzes.length === 0 ? ( -
No quizzes available
- ) : ( -
    - {quizzes.map((quiz, index) => ( -
  • +
    +

    No quizzes yet

    +

    Generate your first quiz to see it here.

    + + + +
+ ) : ( + <> +
+ {quizzes.map((quiz, i) => ( + ))} - - )} -
+
- {/* Buttons */} -
- - -
+
+

{quizzes.length} quiz{quizzes.length !== 1 ? "zes" : ""} saved

+ +
+ + )}
); diff --git a/eduaid_web/src/pages/Question_Type.jsx b/eduaid_web/src/pages/Question_Type.jsx index 8cd586fb..8464ace9 100644 --- a/eduaid_web/src/pages/Question_Type.jsx +++ b/eduaid_web/src/pages/Question_Type.jsx @@ -1,15 +1,42 @@ -import React, { useState } from "react"; +import React, { useState } from "react"; import "../index.css"; -import logo_trans from "../assets/aossie_logo_transparent.png" import { Link } from "react-router-dom"; +import { FiList, FiToggleRight, FiEdit3, FiGrid } from "react-icons/fi"; + +const QUESTION_TYPES = [ + { + id: "get_mcq", + label: "Multiple Choice", + desc: "Four options per question — classic quiz format", + Icon: FiList, + gradient: "from-[#7600F2] to-[#00CBE7]", + }, + { + id: "get_boolq", + label: "True / False", + desc: "Binary answers, great for quick knowledge checks", + Icon: FiToggleRight, + gradient: "from-[#FF005C] to-[#7600F2]", + }, + { + id: "get_shortq", + label: "Short Answer", + desc: "Open-ended responses that test deeper understanding", + Icon: FiEdit3, + gradient: "from-[#00CBE7] to-[#7600F2]", + }, + { + id: "get_problems", + label: "All Types", + desc: "A mix of every question type for comprehensive practice", + Icon: FiGrid, + gradient: "from-[#FF005C] via-[#7600F2] to-[#00CBE7]", + }, +]; const Question_Type = () => { const [selectedOption, setSelectedOption] = useState(null); - const handleOptionClick = (option) => { - setSelectedOption(option); - }; - const handleSaveToLocalStorage = () => { if (selectedOption) { localStorage.setItem("selectedQuestionType", selectedOption); @@ -17,90 +44,104 @@ const Question_Type = () => { }; return ( -
-
- {/* Header */} - - logo -
- - Edu - - - Aid - -
- +
+ {/* Ambient orbs */} +
+
+
+
+
- {/* Title */} -
-

- What’s on your Mind? -

-

- Choose one +

+ {/* Header text */} +
+
+ Step 1 of 2 +
+

+ What type of quiz? +

+

+ Pick the question format that works best for your learning goal.

- {/* Options */} -
- {[ - { id: "get_shortq", label: "Short-Answer Type Questions" }, - { id: "get_mcq", label: "Multiple Choice Questions" }, - { id: "get_boolq", label: "True/False Questions" }, - { id: "get_problems", label: "All Questions" }, - ].map((option) => ( -
handleOptionClick(option.id)} - className={`w-full max-w-xl flex items-center gap-6 px-6 py-5 rounded-xl cursor-pointer bg-[#202838] bg-opacity-50 hover:bg-opacity-70 transition-all duration-200 ${ - selectedOption === option.id ? "ring-2 ring-[#00CBE7]" : "" - }`} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === "Enter") handleOptionClick(option.id); - }} - > -
+ {QUESTION_TYPES.map((option) => { + const isSelected = selectedOption === option.id; + return ( +
-
- {option.label} -
-
- ))} + > + {isSelected && ( +
+ )} + +
+
+ +
+ +
+
+ {option.label} +
+
+ {option.desc} +
+
+ +
+ {isSelected && ( + + + + )} +
+
+ + ); + })}
- {/* Action Button */} -
+ {/* CTA */} +
{selectedOption ? ( - - ) : ( )}
+ + + ← Back to input +
); diff --git a/eduaid_web/src/pages/QuizModeWrapper.jsx b/eduaid_web/src/pages/QuizModeWrapper.jsx new file mode 100644 index 00000000..50543b27 --- /dev/null +++ b/eduaid_web/src/pages/QuizModeWrapper.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import InteractiveQuiz from './InteractiveQuiz'; +import StaticQuiz from './StaticQuiz'; + +const QuizModeWrapper = () => { + const location = useLocation(); + const { mode, questions } = location.state || { mode: 'static', questions: [] }; + + if (mode === 'interactive') { + return ; + } + + return ; +}; + +export default QuizModeWrapper; diff --git a/eduaid_web/src/pages/Output.jsx b/eduaid_web/src/pages/StaticQuiz.jsx similarity index 90% rename from eduaid_web/src/pages/Output.jsx rename to eduaid_web/src/pages/StaticQuiz.jsx index 7add8fe1..f45b20c5 100644 --- a/eduaid_web/src/pages/Output.jsx +++ b/eduaid_web/src/pages/StaticQuiz.jsx @@ -1,7 +1,5 @@ import React, { useState, useEffect, useMemo } from "react"; import "../index.css"; -import logoPNG from "../assets/aossie_logo_transparent.png"; -import { Link } from "react-router-dom"; import apiClient from "../utils/apiClient"; import { FiShuffle, FiEdit2, FiCheck, FiX } from "react-icons/fi"; @@ -206,39 +204,26 @@ const Output = () => { }; return ( -
-
-
- {/* Header - Responsive logo and title */} - -
- logo -
- - Edu - - - Aid - -
-
- +
+ {/* Ambient orbs */} +
+
+
+
+
+
{/* Title and Shuffle Button */} -
-
+
+
Generated Questions
-