From 17807b5150a2827025f16b2e2a472d53df74cb73 Mon Sep 17 00:00:00 2001 From: Nandani Singh Date: Mon, 8 Jun 2026 22:07:18 +0530 Subject: [PATCH] feat: generalized chat quiz generator with PDF and Word downloads --- frontend/package-lock.json | 174 +++++++++++++++++ frontend/package.json | 1 + .../components/ChatPanel/MessageBubble.jsx | 40 ++++ frontend/src/utils/quizExporter.js | 184 ++++++++++++++++++ rag-service/main.py | 112 +++++++---- src/data/users.json | 11 +- validators/schemas.js | 2 +- 7 files changed, 486 insertions(+), 38 deletions(-) create mode 100644 frontend/src/utils/quizExporter.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b4e35405..b9e7ff7f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "axios": "^1.11.0", "bootstrap": "^5.3.7", "file-saver": "^2.0.5", + "jspdf": "^4.2.1", "papaparse": "^5.5.3", "react": "^18.3.1", "react-bootstrap": "^2.10.10", @@ -4599,6 +4600,12 @@ "@types/node": "*" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4629,6 +4636,13 @@ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "license": "MIT" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -6063,6 +6077,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.30", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", @@ -6426,6 +6450,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", @@ -7066,6 +7110,16 @@ "postcss": "^8.4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -7833,6 +7887,16 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -9065,6 +9129,17 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fast-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", @@ -9111,6 +9186,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -10189,6 +10270,20 @@ } } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -10501,6 +10596,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/ipaddr.js": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", @@ -12274,6 +12375,23 @@ "node": ">=0.10.0" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -13858,6 +13976,12 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/papaparse": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", @@ -16538,6 +16662,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -17338,6 +17472,16 @@ "node": ">=8" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -17751,6 +17895,16 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "license": "MIT" }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -18172,6 +18326,16 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -18872,6 +19036,16 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index b8a907b1..c7ca6adf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "axios": "^1.11.0", "bootstrap": "^5.3.7", "file-saver": "^2.0.5", + "jspdf": "^4.2.1", "papaparse": "^5.5.3", "react": "^18.3.1", "react-bootstrap": "^2.10.10", diff --git a/frontend/src/components/ChatPanel/MessageBubble.jsx b/frontend/src/components/ChatPanel/MessageBubble.jsx index 7aab1455..de6ed7dc 100644 --- a/frontend/src/components/ChatPanel/MessageBubble.jsx +++ b/frontend/src/components/ChatPanel/MessageBubble.jsx @@ -2,6 +2,7 @@ import React from "react"; import ReactMarkdown from "react-markdown"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import { exportQuizToPdf, exportQuizToWord } from "../../utils/quizExporter"; // Strict allowlist for AI-generated markdown content. // @@ -44,6 +45,7 @@ const MODE_BADGE = { socratic: { label: "Socratic", bg: "rgba(139,92,246,0.15)", color: "#8B5CF6" }, eli5: { label: "Simple", bg: "rgba(34,197,94,0.15)", color: "#22C55E" }, concise: { label: "Concise", bg: "rgba(249,115,22,0.15)", color: "#F97316" }, + quiz: { label: "Quiz", bg: "rgba(139,92,246,0.15)", color: "#8B5CF6" }, }; const MessageBubble = ({ msg, darkMode, onOpenSource }) => { @@ -109,6 +111,44 @@ const MessageBubble = ({ msg, darkMode, onOpenSource }) => { {msg.text} )} + {msg.role === "bot" && !msg.streaming && (msg.mode === "quiz" || msg.text.includes("# Quiz") || msg.text.toLowerCase().includes("quiz:")) && ( +
+ + +
+ )} + {msg.role === "bot" && msg.mode && msg.mode !== "default" && (() => { const badge = MODE_BADGE[msg.mode] || MODE_BADGE.default; return ( diff --git a/frontend/src/utils/quizExporter.js b/frontend/src/utils/quizExporter.js new file mode 100644 index 00000000..a60e830d --- /dev/null +++ b/frontend/src/utils/quizExporter.js @@ -0,0 +1,184 @@ +import { jsPDF } from "jspdf"; +import { saveAs } from "file-saver"; + +/** + * Parses markdown-style quiz text and converts it to HTML for Word export. + */ +const parseMarkdownToHtml = (markdown) => { + return markdown + .split("\n") + .map((line) => { + const trimmed = line.trim(); + if (!trimmed) { + return "
"; + } + + // Headers + if (line.startsWith("# ")) { + return `

${line.substring(2)}

`; + } + if (line.startsWith("## ")) { + return `

${line.substring(3)}

`; + } + + // Question line (e.g., "1. **Question:** What is...") + if (trimmed.match(/^\d+\./)) { + const formatted = trimmed.replace(/\*\*(.*?)\*\*/g, "$1"); + return `

${formatted}

`; + } + + // Option line (e.g., "- A) Option text") + if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) { + const optionText = trimmed.substring(2); + return `

${optionText}

`; + } + + // Correct Answer line + if (trimmed.includes("Correct Answer:")) { + const formatted = trimmed.replace(/\*\*(.*?)\*\*/g, "$1"); + return `

${formatted}

`; + } + + // Default line formatting + const formatted = trimmed.replace(/\*\*(.*?)\*\*/g, "$1"); + return `

${formatted}

`; + }) + .join("\n"); +}; + +/** + * Generates and downloads a Word Document (.doc) from the markdown quiz text. + */ +export const exportQuizToWord = (quizText, filenamePrefix = "quiz") => { + const parsedHtml = parseMarkdownToHtml(quizText); + + const header = ` + + + + AI Generated Quiz + + + + ${parsedHtml} + + + `.trim(); + + const blob = new Blob([header], { type: "application/msword;charset=utf-8" }); + const filename = `${filenamePrefix.replace(/\s+/g, "_")}_quiz.doc`; + saveAs(blob, filename); +}; + +/** + * Generates and downloads a beautifully styled PDF from the markdown quiz text. + */ +export const exportQuizToPdf = (quizText, filenamePrefix = "quiz") => { + const doc = new jsPDF({ + orientation: "portrait", + unit: "mm", + format: "a4", + }); + + const lines = quizText.split("\n"); + let y = 25; // Vertical cursor + const pageHeight = doc.internal.pageSize.height; + const margin = 20; + const contentWidth = doc.internal.pageSize.width - 2 * margin; + + // Header styling + doc.setFont("Helvetica", "bold"); + doc.setFontSize(22); + doc.setTextColor(139, 92, 246); // Brand purple color + + lines.forEach((line) => { + const trimmed = line.trim(); + if (!trimmed) { + y += 4; + return; + } + + let text = trimmed; + let isTitle = false; + let isQuestion = false; + let isOption = false; + let isAnswer = false; + + if (line.startsWith("# ")) { + isTitle = true; + text = line.substring(2); + } else if (trimmed.match(/^\d+\./)) { + isQuestion = true; + text = trimmed.replace(/\*\*/g, ""); // Remove bold markdown formatting + } else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) { + isOption = true; + text = " \u2022 " + trimmed.substring(2); // Bullet list format + } else if (trimmed.includes("Correct Answer:")) { + isAnswer = true; + text = " " + trimmed.replace(/\*\*/g, ""); // Indented answer line + } else { + text = trimmed.replace(/\*\*/g, ""); + } + + // Set font style and size based on the type of line + if (isTitle) { + doc.setFont("Helvetica", "bold"); + doc.setFontSize(20); + doc.setTextColor(139, 92, 246); + } else if (isQuestion) { + doc.setFont("Helvetica", "bold"); + doc.setFontSize(12); + doc.setTextColor(30, 41, 59); // Slate-800 + y += 4; // Extra spacing before a question + } else if (isOption) { + doc.setFont("Helvetica", "normal"); + doc.setFontSize(11); + doc.setTextColor(75, 85, 99); // Gray-600 + } else if (isAnswer) { + doc.setFont("Helvetica", "bold"); + doc.setFontSize(11); + doc.setTextColor(16, 185, 129); // Green-500 + } else { + doc.setFont("Helvetica", "normal"); + doc.setFontSize(11); + doc.setTextColor(31, 41, 55); // Gray-800 + } + + // Wrap text to fit page width + const splitText = doc.splitTextToSize(text, contentWidth); + + splitText.forEach((textLine) => { + // Check for page break + if (y > pageHeight - margin) { + doc.addPage(); + y = 25; // Reset top margin for new page + } + doc.text(textLine, margin, y); + y += isTitle ? 12 : 7; + }); + }); + + const filename = `${filenamePrefix.replace(/\s+/g, "_")}_quiz.pdf`; + doc.save(filename); +}; diff --git a/rag-service/main.py b/rag-service/main.py index eb0cf100..9e8f61ae 100644 --- a/rag-service/main.py +++ b/rag-service/main.py @@ -990,6 +990,8 @@ def detect_question_intent(question): normalized_question = question.lower() terms = tokenize_text(normalized_question) + if "quiz" in normalized_question or "test" in normalized_question or "questions" in normalized_question or "question paper" in normalized_question: + return "quiz" if "what is this document about" in normalized_question or "what are these documents about" in normalized_question: return "overview" if "how is" in normalized_question and terms.intersection(RELATIONSHIP_QUERY_TERMS): @@ -1308,6 +1310,8 @@ def synthesize_with_ollama(prompt: str) -> Optional[str]: def build_answer_from_documents(question, documents, intent, source_id_by_key=None): + if intent == "quiz": + return None if not has_grounded_keyword_overlap(question, documents) and intent != "overview": return INSUFFICIENT_CONTEXT_MESSAGE if intent == "relationship": @@ -2060,7 +2064,7 @@ def validate_uploaded_pdf(file_path: str) -> str: return trusted_path -VALID_MODES = {"default", "tutor", "socratic", "eli5", "concise"} +VALID_MODES = {"default", "tutor", "socratic", "eli5", "concise", "quiz"} class Question(BaseModel): question: str = Field(..., min_length=1, description="Question cannot be empty") @@ -2797,24 +2801,42 @@ def ask_question(data: Question): }) return result - prompt = ( - "You are a careful assistant answering questions over one or more uploaded PDF documents. " - "Use only the provided context. The context may include excerpts from multiple PDFs. " - "When the question asks for a relationship, comparison, or synthesis, connect the relevant facts across documents. " - "If the context does not contain enough information, say that briefly and do not invent details.\n\n" - - "Reference the provided source numbers naturally whenever the answer is directly supported by the context.\n" - "Cite sources using formats like 'According to Source 1' or 'Source 2 explains that...'\n" - - "You are a helpful AI assistant.\n" - "Give clear, conversational, human-friendly answers.\n" - "Do not return raw PDF text or chunks.\n" - "Summarize properly in readable sentences.\n\n" - - f"Context:\n{context}\n\n" - f"Question: {question}\n" - "Answer:" - ) + if intent == "quiz" or mode == "quiz": + prompt = ( + "You are an expert educator. Based on the provided document context, generate a quiz with 5 multiple-choice questions. " + "Each question must have 4 options (A, B, C, D) and specify the correct answer. " + "Format the output exactly as follows in clean markdown. Do not include any introductory or concluding text, only the quiz itself:\n\n" + "# Quiz: [Quiz Title]\n\n" + "1. **Question:** [Question text]\n" + " - A) [Option A]\n" + " - B) [Option B]\n" + " - C) [Option C]\n" + " - D) [Option D]\n" + " **Correct Answer:** [Correct Option, e.g., A]\n\n" + "2. ...\n\n" + f"Context:\n{context}\n\n" + f"Question/Request: {question}\n" + "Quiz:" + ) + else: + prompt = ( + "You are a careful assistant answering questions over one or more uploaded PDF documents. " + "Use only the provided context. The context may include excerpts from multiple PDFs. " + "When the question asks for a relationship, comparison, or synthesis, connect the relevant facts across documents. " + "If the context does not contain enough information, say that briefly and do not invent details.\n\n" + + "Reference the provided source numbers naturally whenever the answer is directly supported by the context.\n" + "Cite sources using formats like 'According to Source 1' or 'Source 2 explains that...'\n" + + "You are a helpful AI assistant.\n" + "Give clear, conversational, human-friendly answers.\n" + "Do not return raw PDF text or chunks.\n" + "Summarize properly in readable sentences.\n\n" + + f"Context:\n{context}\n\n" + f"Question: {question}\n" + "Answer:" + ) logger.info( "Executing query session_id=%s retrieved_chunks=%s sources=%s", @@ -2864,7 +2886,7 @@ def ask_question(data: Question): logger.info("Falling back to HuggingFace generate_response session_id=%s", session_id) answer = generate_response( prompt, - max_new_tokens=256 + max_new_tokens=512 if (intent == "quiz" or mode == "quiz") else 256 ) framed = apply_mode_framing(answer, question, mode, docs, context) @@ -3047,21 +3069,39 @@ def _grounded_stream(): # LLM generation path — run in a background thread so we can stream tokens # back to the caller as they are produced rather than waiting for the full # completion before sending anything. - prompt = ( - "You are a careful assistant answering questions over one or more uploaded PDF documents. " - "Use only the provided context. The context may include excerpts from multiple PDFs. " - "When the question asks for a relationship, comparison, or synthesis, connect the relevant facts across documents. " - "If the context does not contain enough information, say that briefly and do not invent details.\n\n" - "Reference the provided source numbers naturally whenever the answer is directly supported by the context.\n" - "Cite sources using formats like 'According to Source 1' or 'Source 2 explains that...'\n" - "You are a helpful AI assistant.\n" - "Give clear, conversational, human-friendly answers.\n" - "Do not return raw PDF text or chunks.\n" - "Summarize properly in readable sentences.\n\n" - f"Context:\n{context}\n\n" - f"Question: {question}\n" - "Answer:" - ) + if intent == "quiz" or mode == "quiz": + prompt = ( + "You are an expert educator. Based on the provided document context, generate a quiz with 5 multiple-choice questions. " + "Each question must have 4 options (A, B, C, D) and specify the correct answer. " + "Format the output exactly as follows in clean markdown. Do not include any introductory or concluding text, only the quiz itself:\n\n" + "# Quiz: [Quiz Title]\n\n" + "1. **Question:** [Question text]\n" + " - A) [Option A]\n" + " - B) [Option B]\n" + " - C) [Option C]\n" + " - D) [Option D]\n" + " **Correct Answer:** [Correct Option, e.g., A]\n\n" + "2. ...\n\n" + f"Context:\n{context}\n\n" + f"Question/Request: {question}\n" + "Quiz:" + ) + else: + prompt = ( + "You are a careful assistant answering questions over one or more uploaded PDF documents. " + "Use only the provided context. The context may include excerpts from multiple PDFs. " + "When the question asks for a relationship, comparison, or synthesis, connect the relevant facts across documents. " + "If the context does not contain enough information, say that briefly and do not invent details.\n\n" + "Reference the provided source numbers naturally whenever the answer is directly supported by the context.\n" + "Cite sources using formats like 'According to Source 1' or 'Source 2 explains that...'\n" + "You are a helpful AI assistant.\n" + "Give clear, conversational, human-friendly answers.\n" + "Do not return raw PDF text or chunks.\n" + "Summarize properly in readable sentences.\n\n" + f"Context:\n{context}\n\n" + f"Question: {question}\n" + "Answer:" + ) logger.info( "Stream executing query session_id=%s retrieved_chunks=%s", @@ -3088,7 +3128,7 @@ def _generate_and_stream(): generate_kwargs = { **encoded, - "max_new_tokens": 256, + "max_new_tokens": 512 if (intent == "quiz" or mode == "quiz") else 256, "do_sample": False, "pad_token_id": pad_token_id, "streamer": streamer, diff --git a/src/data/users.json b/src/data/users.json index 0637a088..bb2a7a66 100644 --- a/src/data/users.json +++ b/src/data/users.json @@ -1 +1,10 @@ -[] \ No newline at end of file +[ + { + "email": "testuser-1780934845930@example.com", + "password": "$2b$10$9AAsvzxfwNp9haLU2oKNMuMTeY3icNBzPYcPHTx3Wv4w78ZUVD7Ry" + }, + { + "email": "testuser2-1780934846010@example.com", + "password": "$2b$10$6oXLfmhPAQzfCVLwS5iy/Oqoi5THfbxdKVMjxpWIB/29FwmLnPcTC" + } +] \ No newline at end of file diff --git a/validators/schemas.js b/validators/schemas.js index be966907..d5434ecc 100644 --- a/validators/schemas.js +++ b/validators/schemas.js @@ -42,7 +42,7 @@ const questionSchema = z.preprocess( const modeSchema = z.preprocess( (val) => (typeof val === "string" ? val : "default"), - z.enum(["default", "tutor", "socratic", "eli5", "concise"]).default("default") + z.enum(["default", "tutor", "socratic", "eli5", "concise", "quiz"]).default("default") ); const sessionSecretSchema = z.preprocess(