From a87484b0e73444dbb2f79fc17d4a7129f5685cb1 Mon Sep 17 00:00:00 2001 From: inubimambo Date: Tue, 8 Jul 2025 20:46:24 +0800 Subject: [PATCH 1/3] Fix 'Review Answer' button in Quiz doing nothing & improvements to quiz --- server.js | 304 ++++++++++++++++++------------------------------- views/quiz.ejs | 272 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 374 insertions(+), 202 deletions(-) diff --git a/server.js b/server.js index 9c1946d..052a317 100644 --- a/server.js +++ b/server.js @@ -1281,63 +1281,123 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => { - // Call Flowise API - const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { - question: prompt, - history: [] - }); - - - + // Call Flowise API with retry logic for better AI responses let quizData; - try { - const responseText = response.data.text || response.data.answer || response.data; + let retryCount = 0; + const maxRetries = 2; + + while (retryCount <= maxRetries) { + try { + const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { + question: retryCount > 0 ? + `${prompt}\n\nIMPORTANT: Please provide ONLY a valid JSON array of questions. Do not include any explanatory text, greetings, or additional commentary. Start your response directly with [ and end with ].` : + prompt, + history: [] + }); - - // Try to extract JSON from the response - let jsonString = null; - - // First, try to find JSON wrapped in code blocks - const codeBlockMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/); - if (codeBlockMatch) { - jsonString = codeBlockMatch[1]; + const responseText = response.data.text || response.data.answer || response.data; - } else { - // Try to find JSON array by counting brackets - const startIndex = responseText.indexOf('['); - if (startIndex !== -1) { - let bracketCount = 0; - let endIndex = startIndex; + // Check if the AI response looks like it's not following instructions + if (responseText.toLowerCase().includes('do you have a question') || + responseText.toLowerCase().includes('would you like me to help') || + responseText.toLowerCase().includes('something else related') || + responseText.toLowerCase().includes('how can i help') || + (!responseText.includes('[') && !responseText.includes('{'))) { - for (let i = startIndex; i < responseText.length; i++) { - if (responseText[i] === '[') bracketCount++; - if (responseText[i] === ']') bracketCount--; - if (bracketCount === 0) { - endIndex = i; - break; - } - } - - if (bracketCount === 0) { - jsonString = responseText.substring(startIndex, endIndex + 1); - + if (retryCount < maxRetries) { + retryCount++; + console.log(`AI gave improper response, retrying... (attempt ${retryCount + 1})`); + continue; + } else { + throw new Error('The AI is not responding properly to quiz generation requests. Please try again with a more specific topic or try again later.'); } } - } - - if (jsonString) { + + // Try to extract JSON from the response + let jsonString = null; + + // First, try to find JSON wrapped in code blocks + const codeBlockMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/); + if (codeBlockMatch) { + jsonString = codeBlockMatch[1]; + } else { + // Try to find JSON array by counting brackets + const startIndex = responseText.indexOf('['); + if (startIndex !== -1) { + let bracketCount = 0; + let endIndex = startIndex; + + for (let i = startIndex; i < responseText.length; i++) { + if (responseText[i] === '[') bracketCount++; + if (responseText[i] === ']') bracketCount--; + if (bracketCount === 0) { + endIndex = i; + break; + } + } + + if (bracketCount === 0) { + jsonString = responseText.substring(startIndex, endIndex + 1); + } + } + } + + if (!jsonString) { + if (retryCount < maxRetries) { + retryCount++; + console.log(`Could not find JSON in response, retrying... (attempt ${retryCount + 1})`); + continue; + } else { + throw new Error('Could not find valid JSON in the AI response after multiple attempts. Please try generating the quiz again.'); + } + } + quizData = JSON.parse(jsonString); - } else { - quizData = generateFallbackQuiz(topic, questionCount, quizType); + + // Validate the parsed data + if (!Array.isArray(quizData) || quizData.length === 0) { + if (retryCount < maxRetries) { + retryCount++; + console.log(`Invalid quiz format received, retrying... (attempt ${retryCount + 1})`); + continue; + } else { + throw new Error('The AI generated an invalid quiz format after multiple attempts. Please try again.'); + } + } + + // Validate each question has required fields + for (let i = 0; i < quizData.length; i++) { + const q = quizData[i]; + if (!q.question || (!q.correct && !q.answer)) { + if (retryCount < maxRetries) { + retryCount++; + console.log(`Question ${i + 1} missing required fields, retrying... (attempt ${retryCount + 1})`); + continue; + } else { + throw new Error(`Question ${i + 1} is missing required fields after multiple attempts. Please try generating the quiz again.`); + } + } + } + + // If we get here, the quiz is valid + break; + + } catch (parseError) { + if (retryCount < maxRetries) { + retryCount++; + console.log(`Parse error occurred, retrying... (attempt ${retryCount + 1}):`, parseError.message); + continue; + } else { + console.error('Quiz parsing error after retries:', parseError); + console.error('AI Response was:', response?.data?.text || response?.data?.answer || response?.data || 'No response'); + + // Return error instead of fallback + return res.json({ + success: false, + error: parseError.message || 'Failed to parse quiz questions from AI response after multiple attempts. Please try again with a different topic or rephrase your request.' + }); + } } - } catch (parseError) { - console.error('Quiz parsing error:', parseError); - quizData = generateFallbackQuiz(topic, questionCount, quizType); - } - - // Ensure quizData is always defined and is an array - if (!quizData || !Array.isArray(quizData) || quizData.length === 0) { - quizData = generateFallbackQuiz(topic, questionCount, quizType); } @@ -1354,154 +1414,14 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => { console.error('Quiz generation error:', error); console.error('Error stack:', error.stack); - // Return fallback quiz on error - const fallbackQuiz = generateFallbackQuiz(req.body.topic || 'General Knowledge', req.body.questionCount || 5, req.body.quizType || 'multiple-choice'); + // Return error instead of fallback quiz res.json({ - success: true, - quiz: fallbackQuiz, - topic: req.body.topic || 'General Knowledge', - difficulty: req.body.difficulty || 'beginner', - questionCount: req.body.questionCount || 5, - quizType: req.body.quizType || 'multiple-choice' + success: false, + error: 'Failed to generate quiz. Please check your connection and try again, or try a different topic.' }); } }); -function generateFallbackQuiz(topic, questionCount, quizType) { - const questions = []; - - // Generate actual questions based on topic - const topicQuestions = { - 'javascript': [ - { - question: 'What is the correct way to declare a variable in JavaScript?', - options: ['A) var myVar = 5;', 'B) variable myVar = 5;', 'C) declare myVar = 5;', 'D) int myVar = 5;'], - correct: 'A', - explanation: 'The var keyword is used to declare variables in JavaScript.' - }, - { - question: 'Which method is used to add an element to the end of an array?', - options: ['A) push()', 'B) add()', 'C) append()', 'D) insert()'], - correct: 'A', - explanation: 'The push() method adds one or more elements to the end of an array.' - }, - { - question: 'What does === operator do in JavaScript?', - options: ['A) Assigns a value', 'B) Compares values only', 'C) Compares values and types', 'D) Declares a constant'], - correct: 'C', - explanation: 'The === operator compares both value and type without type conversion.' - }, - { - question: 'How do you write a comment in JavaScript?', - options: ['A) ', 'B) // comment', 'C) # comment', 'D) /* comment */'], - correct: 'B', - explanation: 'Single-line comments in JavaScript start with //.' - }, - { - question: 'What is the result of typeof null in JavaScript?', - options: ['A) "null"', 'B) "undefined"', 'C) "object"', 'D) "boolean"'], - correct: 'C', - explanation: 'typeof null returns "object" due to a legacy bug in JavaScript.' - } - ], - 'python': [ - { - question: 'Which keyword is used to create a function in Python?', - options: ['A) function', 'B) def', 'C) create', 'D) func'], - correct: 'B', - explanation: 'The def keyword is used to define functions in Python.' - }, - { - question: 'What is the correct way to create a list in Python?', - options: ['A) list = (1, 2, 3)', 'B) list = {1, 2, 3}', 'C) list = [1, 2, 3]', 'D) list = <1, 2, 3>'], - correct: 'C', - explanation: 'Square brackets [] are used to create lists in Python.' - }, - { - question: 'How do you start a comment in Python?', - options: ['A) //', 'B) #', 'C) /*', 'D) --'], - correct: 'B', - explanation: 'Comments in Python start with the # symbol.' - }, - { - question: 'What does len() function do in Python?', - options: ['A) Returns the length of an object', 'B) Converts to lowercase', 'C) Rounds a number', 'D) Prints output'], - correct: 'A', - explanation: 'The len() function returns the number of items in an object.' - }, - { - question: 'Which of the following is a mutable data type in Python?', - options: ['A) tuple', 'B) string', 'C) list', 'D) integer'], - correct: 'C', - explanation: 'Lists are mutable, meaning they can be changed after creation.' - } - ], - 'math': [ - { - question: 'What is the value of π (pi) approximately?', - options: ['A) 3.14159', 'B) 2.71828', 'C) 1.61803', 'D) 4.66920'], - correct: 'A', - explanation: 'π (pi) is approximately 3.14159, the ratio of circumference to diameter.' - }, - { - question: 'What is the derivative of x²?', - options: ['A) x', 'B) 2x', 'C) x³', 'D) 2x²'], - correct: 'B', - explanation: 'Using the power rule, the derivative of x² is 2x.' - }, - { - question: 'What is the Pythagorean theorem?', - options: ['A) a + b = c', 'B) a² + b² = c²', 'C) a × b = c', 'D) a² - b² = c²'], - correct: 'B', - explanation: 'The Pythagorean theorem states that a² + b² = c² for right triangles.' - }, - { - question: 'What is the factorial of 5?', - options: ['A) 25', 'B) 120', 'C) 60', 'D) 100'], - correct: 'B', - explanation: '5! = 5 × 4 × 3 × 2 × 1 = 120' - }, - { - question: 'What is the square root of 64?', - options: ['A) 6', 'B) 7', 'C) 8', 'D) 9'], - correct: 'C', - explanation: 'The square root of 64 is 8 because 8² = 64.' - } - ] - }; - - // Get appropriate questions for the topic - const availableQuestions = topicQuestions[topic.toLowerCase()] || topicQuestions['math']; - - for (let i = 0; i < questionCount; i++) { - const questionIndex = i % availableQuestions.length; - const baseQuestion = availableQuestions[questionIndex]; - - if (quizType === 'multiple-choice') { - questions.push({ - question: baseQuestion.question, - options: baseQuestion.options, - correct: baseQuestion.correct, - explanation: baseQuestion.explanation - }); - } else if (quizType === 'true-false') { - questions.push({ - question: baseQuestion.question.replace(/Which|What|How/, 'Is it true that'), - correct: Math.random() > 0.5 ? 'True' : 'False', - explanation: baseQuestion.explanation - }); - } else { - questions.push({ - question: baseQuestion.question, - answer: baseQuestion.correct, - keywords: [baseQuestion.correct.toLowerCase(), topic.toLowerCase()] - }); - } - } - - return questions; -} - app.post('/api/submit-quiz', requireAuth, async (req, res) => { try { const { answers, quiz, topic, difficulty, quizType } = req.body; diff --git a/views/quiz.ejs b/views/quiz.ejs index 4ee6392..e6a1ec9 100644 --- a/views/quiz.ejs +++ b/views/quiz.ejs @@ -143,7 +143,7 @@
-
+

Quiz Results

@@ -166,15 +166,6 @@
- -
-
-
-
Performance
-
-
-
-
@@ -339,6 +330,14 @@ document.addEventListener('DOMContentLoaded', function() { resetQuiz(); }); } + + // Add event listener for review button + const reviewBtn = document.getElementById('review-btn'); + if (reviewBtn) { + reviewBtn.addEventListener('click', () => { + reviewAnswers(); + }); + } } function displayQuestion(index) { @@ -556,6 +555,9 @@ document.addEventListener('DOMContentLoaded', function() { } function displayResults(results) { + // Store results globally for review functionality + window.lastQuizResults = results; + quizContainer.classList.add('d-none'); resultsContainer.classList.remove('d-none'); @@ -636,6 +638,256 @@ document.addEventListener('DOMContentLoaded', function() { quizForm.reset(); } + + function reviewAnswers() { + // Switch from results view back to quiz view for review + resultsContainer.classList.add('d-none'); + quizContainer.classList.remove('d-none'); + + // Set to review mode and start from first question + currentQuestion = 0; + displayQuestionInReviewMode(currentQuestion); + displayOverviewInReviewMode(); + + // Update navigation for review mode + const prevBtn = document.getElementById('prev-btn'); + const nextBtn = document.getElementById('next-btn'); + const submitBtn = document.getElementById('submit-btn'); + + // Remove existing event listeners by cloning elements + if (prevBtn) { + const newPrevBtn = prevBtn.cloneNode(true); + prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn); + newPrevBtn.onclick = () => { + if (currentQuestion > 0) { + currentQuestion--; + displayQuestionInReviewMode(currentQuestion); + } + }; + } + + if (nextBtn) { + const newNextBtn = nextBtn.cloneNode(true); + nextBtn.parentNode.replaceChild(newNextBtn, nextBtn); + newNextBtn.onclick = () => { + if (currentQuestion < currentQuiz.length - 1) { + currentQuestion++; + displayQuestionInReviewMode(currentQuestion); + } + }; + } + + // Hide submit button completely in review mode + if (submitBtn) { + submitBtn.classList.add('d-none'); + } + + // Add a "Back to Results" button + const cardBody = submitBtn.parentElement; + let backToResultsBtn = document.getElementById('back-to-results-btn'); + if (!backToResultsBtn) { + backToResultsBtn = document.createElement('button'); + backToResultsBtn.id = 'back-to-results-btn'; + backToResultsBtn.className = 'btn btn-info'; + backToResultsBtn.innerHTML = 'Back to Results'; + backToResultsBtn.onclick = () => { + quizContainer.classList.add('d-none'); + resultsContainer.classList.remove('d-none'); + // Remove the back button + backToResultsBtn.remove(); + }; + cardBody.appendChild(backToResultsBtn); + } + } + + function displayQuestionInReviewMode(index) { + if (!currentQuiz || index >= currentQuiz.length) { + console.error('Invalid question index or quiz not loaded'); + return; + } + + const question = currentQuiz[index]; + const questionsDiv = document.getElementById('quiz-questions'); + + if (!questionsDiv) { + console.error('Questions div not found!'); + return; + } + + // Get the quiz results from the last submission + const resultForThisQuestion = window.lastQuizResults ? + window.lastQuizResults.results[index] : null; + + let html = ` +
+
+ Question ${index + 1} + ${resultForThisQuestion ? + ` + ${resultForThisQuestion.isCorrect ? 'Correct' : 'Incorrect'} + ` : '' + } +
+

${escapeHtml(question.question)}

+
+ `; + + if (question.options) { + // Multiple choice - show options with user's answer and correct answer highlighted + html += '
'; + question.options.forEach((option, i) => { + const optionLetter = option.charAt(0); + const userSelected = userAnswers[index] === optionLetter; + const isCorrect = question.correct === optionLetter; + + let className = 'form-check mb-2'; + let labelClass = 'form-check-label'; + + if (userSelected && isCorrect) { + className += ' bg-success bg-opacity-10 border border-success rounded p-2'; + labelClass += ' text-success fw-bold'; + } else if (userSelected && !isCorrect) { + className += ' bg-danger bg-opacity-10 border border-danger rounded p-2'; + labelClass += ' text-danger fw-bold'; + } else if (!userSelected && isCorrect) { + className += ' bg-warning bg-opacity-10 border border-warning rounded p-2'; + labelClass += ' text-warning fw-bold'; + } + + html += ` +
+ + +
+ `; + }); + html += '
'; + } else if (question.correct === 'True' || question.correct === 'False') { + // True/False + const userSelected = userAnswers[index]; + const correctAnswer = question.correct; + + ['True', 'False'].forEach(option => { + const userSelectedThis = userSelected === option; + const isCorrect = correctAnswer === option; + + let className = 'form-check mb-2'; + let labelClass = 'form-check-label'; + + if (userSelectedThis && isCorrect) { + className += ' bg-success bg-opacity-10 border border-success rounded p-2'; + labelClass += ' text-success fw-bold'; + } else if (userSelectedThis && !isCorrect) { + className += ' bg-danger bg-opacity-10 border border-danger rounded p-2'; + labelClass += ' text-danger fw-bold'; + } else if (!userSelectedThis && isCorrect) { + className += ' bg-warning bg-opacity-10 border border-warning rounded p-2'; + labelClass += ' text-warning fw-bold'; + } + + html += ` +
+ + +
+ `; + }); + } else { + // Short answer + const userAnswer = userAnswers[index] || ''; + const correctAnswer = question.correct || ''; + + html += ` +
+ +
+ ${userAnswer ? escapeHtml(userAnswer) : 'No answer provided'} +
+ +
+ ${escapeHtml(correctAnswer)} +
+
+ `; + } + + // Add explanation if available + if (resultForThisQuestion && resultForThisQuestion.explanation) { + html += ` +
+ Explanation: +

${escapeHtml(resultForThisQuestion.explanation)}

+
+ `; + } + + questionsDiv.innerHTML = html; + + // Update progress + const progressEl = document.getElementById('quiz-progress'); + if (progressEl) { + progressEl.textContent = `Review: Question ${index + 1} of ${currentQuiz.length}`; + } + + // Update navigation buttons for review mode + const prevBtn = document.getElementById('prev-btn'); + const nextBtn = document.getElementById('next-btn'); + const submitBtn = document.getElementById('submit-btn'); + + if (prevBtn) prevBtn.disabled = index === 0; + if (nextBtn) { + nextBtn.disabled = index === currentQuiz.length - 1; + nextBtn.classList.remove('d-none'); + } + // Keep submit button hidden in review mode + if (submitBtn) { + submitBtn.classList.add('d-none'); + } + } + + function displayOverviewInReviewMode() { + const overviewDiv = document.getElementById('quiz-overview'); + let html = '
'; + + currentQuiz.forEach((_, i) => { + const resultForQuestion = window.lastQuizResults ? + window.lastQuizResults.results[i] : null; + const isCurrent = i === currentQuestion; + + let btnClass = 'btn btn-sm w-100 '; + if (isCurrent) { + btnClass += 'btn-primary'; + } else if (resultForQuestion) { + btnClass += resultForQuestion.isCorrect ? 'btn-success' : 'btn-danger'; + } else { + btnClass += 'btn-outline-secondary'; + } + + html += ` +
+ +
+ `; + }); + + html += '
'; + overviewDiv.innerHTML = html; + } + + window.goToQuestionInReview = function(index) { + currentQuestion = index; + displayQuestionInReviewMode(currentQuestion); + }; }); -- 2.49.1 From 9644f62dc5a264939b5186c135c2174db9b74c1d Mon Sep 17 00:00:00 2001 From: inubimambo Date: Tue, 8 Jul 2025 21:00:17 +0800 Subject: [PATCH 2/3] Improve text extraction in dashboard --- package-lock.json | 928 +++++++++++++++++++++++++++++++++++++++++++- package.json | 3 + server.js | 206 +++++++++- views/dashboard.ejs | 78 +++- 4 files changed, 1193 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index ded8a6b..1b02546 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,17 +16,49 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "ejs": "^3.1.10", + "exceljs": "^4.4.0", "express": "^4.19.2", "express-session": "^1.18.0", "form-data": "^4.0.3", "fs-extra": "^11.2.0", + "mammoth": "^1.9.1", "multer": "^2.0.0", + "pdf-parse": "^1.1.1", "uuid": "^10.0.0" }, "devDependencies": { "nodemon": "^3.0.1" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -47,6 +79,21 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -151,6 +198,75 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "license": "ISC" }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -165,6 +281,15 @@ "node": ">=10" } }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -200,6 +325,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -214,6 +359,28 @@ "node": ">= 10.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -227,6 +394,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -274,12 +458,62 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -329,6 +563,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -418,6 +664,21 @@ "node": ">= 0.8" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -489,6 +750,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -502,6 +769,37 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -554,6 +852,12 @@ "node": ">=8" } }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -566,6 +870,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -580,6 +893,45 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -616,6 +968,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -676,6 +1037,35 @@ "node": ">= 0.6" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -756,6 +1146,19 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -871,6 +1274,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -930,6 +1339,35 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1167,6 +1605,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1174,6 +1632,12 @@ "dev": true, "license": "ISC" }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1255,6 +1719,12 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -1285,6 +1755,195 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -1309,6 +1968,30 @@ "semver": "bin/semver.js" } }, + "node_modules/mammoth": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.9.1.tgz", + "integrity": "sha512-4S2v1eP4Yo4so0zGNicJKcP93su3wDPcUk+xvkjSG75nlNjSkDJu8BhWQ+e54BROM0HfA6nPzJn12S6bq2Ko6w==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1484,6 +2167,12 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, + "node_modules/node-ensure": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", + "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==", + "license": "MIT" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -1600,7 +2289,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1670,6 +2358,18 @@ "wrappy": "1" } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1694,6 +2394,34 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pdf-parse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz", + "integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==", + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "node-ensure": "^0.0.0" + }, + "engines": { + "node": ">=6.8.1" + } + }, + "node_modules/pdf-parse/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/pdf-parse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1707,6 +2435,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1795,6 +2529,36 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1850,6 +2614,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -1922,6 +2698,12 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2019,6 +2801,12 @@ "node": ">=10" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2100,6 +2888,22 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -2112,6 +2916,15 @@ "node": ">=10" } }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2150,6 +2963,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2188,6 +3010,12 @@ "dev": true, "license": "MIT" }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -2206,6 +3034,54 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2274,6 +3150,21 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -2288,6 +3179,41 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } } } } diff --git a/package.json b/package.json index 971155f..00b96f4 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,14 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "ejs": "^3.1.10", + "exceljs": "^4.4.0", "express": "^4.19.2", "express-session": "^1.18.0", "form-data": "^4.0.3", "fs-extra": "^11.2.0", + "mammoth": "^1.9.1", "multer": "^2.0.0", + "pdf-parse": "^1.1.1", "uuid": "^10.0.0" }, "devDependencies": { diff --git a/server.js b/server.js index 052a317..c002880 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,116 @@ require('dotenv').config(); // Document processing utilities const crypto = require('crypto'); +const mammoth = require('mammoth'); // For .docx files +const pdfParse = require('pdf-parse'); // For PDF files +const ExcelJS = require('exceljs'); // For Excel files + +// Helper function to extract text from various document formats +async function extractTextFromDocument(filePath, fileExtension) { + try { + const extension = fileExtension.toLowerCase(); + + switch (extension) { + case '.docx': + // Extract text from Word documents + const docxBuffer = await fs.readFile(filePath); + const docxResult = await mammoth.extractRawText({ buffer: docxBuffer }); + return { + success: true, + text: docxResult.value, + extractedLength: docxResult.value.length, + method: 'mammoth' + }; + + case '.pdf': + // Extract text from PDF documents + const pdfBuffer = await fs.readFile(filePath); + const pdfResult = await pdfParse(pdfBuffer); + return { + success: true, + text: pdfResult.text, + extractedLength: pdfResult.text.length, + method: 'pdf-parse', + pages: pdfResult.numpages + }; + + case '.xlsx': + case '.xls': + // Extract text from Excel files using ExcelJS + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.readFile(filePath); + let excelText = ''; + let sheetCount = 0; + + workbook.eachSheet((worksheet, sheetId) => { + sheetCount++; + excelText += `Sheet: ${worksheet.name}\n`; + + worksheet.eachRow((row, rowNumber) => { + const rowText = []; + row.eachCell((cell, colNumber) => { + // Extract cell value as text + let cellValue = cell.value; + if (cellValue !== null && cellValue !== undefined) { + // Handle different cell types + if (typeof cellValue === 'object' && cellValue.text) { + // Rich text object + cellValue = cellValue.text; + } else if (typeof cellValue === 'object' && cellValue.result) { + // Formula result + cellValue = cellValue.result; + } + rowText.push(String(cellValue)); + } + }); + if (rowText.length > 0) { + excelText += rowText.join('\t') + '\n'; + } + }); + excelText += '\n'; + }); + + return { + success: true, + text: excelText, + extractedLength: excelText.length, + method: 'exceljs', + sheets: sheetCount + }; + + case '.txt': + case '.md': + case '.json': + case '.js': + case '.html': + case '.css': + case '.xml': + case '.csv': + // Read text files directly + const textContent = await fs.readFile(filePath, 'utf-8'); + return { + success: true, + text: textContent, + extractedLength: textContent.length, + method: 'direct' + }; + + default: + return { + success: false, + error: `Unsupported file type: ${extension}`, + text: null + }; + } + } catch (error) { + console.error('Error extracting text from document:', error); + return { + success: false, + error: error.message, + text: null + }; + } +} // Document chunking configuration const CHUNK_SIZE = 20000; // Larger chunks for FormData uploads (20KB) @@ -1017,20 +1127,94 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => { return res.status(404).json({ success: false, error: 'File not found' }); } - // Read file content const filePath = path.join(__dirname, file.path); - const fileContent = await fs.readFile(filePath, 'utf-8'); + const fileExtension = path.extname(file.originalName).toLowerCase(); - res.json({ - success: true, - file: { - id: file.id, - originalName: file.originalName, - size: file.size, - uploadDate: file.uploadDate, - content: fileContent + // Try to extract text from the document + const extractionResult = await extractTextFromDocument(filePath, fileExtension); + + if (extractionResult.success) { + // Successfully extracted text + const extractedText = extractionResult.text; + + // Limit preview to first 5000 characters to avoid huge responses + const previewContent = extractedText.length > 5000 ? + extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' : + extractedText; + + res.json({ + success: true, + file: { + id: file.id, + originalName: file.originalName, + size: file.size, + uploadDate: file.uploadDate, + content: previewContent, + previewType: 'extracted-text', + extractionInfo: { + method: extractionResult.method, + totalLength: extractionResult.extractedLength, + pages: extractionResult.pages || null, + sheets: extractionResult.sheets || null, + truncated: extractedText.length > 5000 + }, + message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}` + } + }); + } else { + // Failed to extract text, fall back to file type detection + const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv']; + const binaryFormats = ['.pdf', '.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt']; + + if (textFormats.includes(fileExtension)) { + // Try reading as plain text (should have been handled by extraction, but fallback) + try { + const fileContent = await fs.readFile(filePath, 'utf-8'); + const previewContent = fileContent.length > 5000 ? + fileContent.substring(0, 5000) + '\n\n... (truncated)' : + fileContent; + + res.json({ + success: true, + file: { + id: file.id, + originalName: file.originalName, + size: file.size, + uploadDate: file.uploadDate, + content: previewContent, + previewType: 'text' + } + }); + } catch (readError) { + res.json({ + success: true, + file: { + id: file.id, + originalName: file.originalName, + size: file.size, + uploadDate: file.uploadDate, + content: 'File preview not available', + previewType: 'error', + message: `Error reading ${fileExtension.toUpperCase()} file: ${readError.message}` + } + }); + } + } else { + // Binary format that couldn't be processed + res.json({ + success: true, + file: { + id: file.id, + originalName: file.originalName, + size: file.size, + uploadDate: file.uploadDate, + content: 'Text extraction failed', + previewType: 'extraction-failed', + message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.` + } + }); } - }); + } } catch (error) { console.error('Error previewing file:', error); res.status(500).json({ diff --git a/views/dashboard.ejs b/views/dashboard.ejs index c47d1ed..31e1cf1 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -181,10 +181,8 @@ function previewFile(fileId) { const file = result.file; let content = ''; - // Check file type and format content accordingly - const fileExtension = file.originalName.split('.').pop().toLowerCase(); - - if (['txt', 'md', 'json', 'js', 'html', 'css', 'py', 'java', 'cpp', 'c'].includes(fileExtension)) { + // Handle different preview types + if (file.previewType === 'text') { // Text-based files - show with syntax highlighting content = `
@@ -198,8 +196,35 @@ function previewFile(fileId) {
${escapeHtml(file.content)}
`; - } else if (['pdf', 'doc', 'docx'].includes(fileExtension)) { - // Document files - show basic info and content preview + } else if (file.previewType === 'extracted-text') { + // Successfully extracted text from document + const extractionInfo = file.extractionInfo || {}; + const infoText = []; + + if (extractionInfo.pages) infoText.push(`${extractionInfo.pages} pages`); + if (extractionInfo.sheets) infoText.push(`${extractionInfo.sheets} sheets`); + if (extractionInfo.totalLength) infoText.push(`${extractionInfo.totalLength} characters extracted`); + + content = ` +
+
${file.originalName}
+ + Size: ${Math.round(file.size / 1024)} KB | + Uploaded: ${new Date(file.uploadDate).toLocaleDateString()} + ${infoText.length > 0 ? ' | ' + infoText.join(', ') : ''} + +
+
+ + ${file.message || 'Text successfully extracted from document'} +
+
+
${escapeHtml(file.content)}
+
+ ${extractionInfo.truncated ? 'Full document content is available for AI processing' : ''} + `; + } else if (file.previewType === 'binary') { + // Binary files - show info message content = `
${file.originalName}
@@ -210,14 +235,47 @@ function previewFile(fileId) {
- Document preview: First few lines of extracted text + ${file.message || 'This is a binary file that has been processed for AI use.'}
-
-
${escapeHtml(file.content.substring(0, 1000))}${file.content.length > 1000 ? '...' : ''}
+ ${file.content && file.content !== 'File preview not available' ? ` +
+ Extracted content preview: +
${escapeHtml(file.content)}
+
+ ` : ''} + `; + } else if (file.previewType === 'extraction-failed') { + // Failed to extract text + content = ` +
+
${file.originalName}
+ + Size: ${Math.round(file.size / 1024)} KB | + Uploaded: ${new Date(file.uploadDate).toLocaleDateString()} + +
+
+ + ${file.message || 'Could not extract text from this file.'} +
+ `; + } else if (file.previewType === 'error') { + // Error reading file + content = ` +
+
${file.originalName}
+ + Size: ${Math.round(file.size / 1024)} KB | + Uploaded: ${new Date(file.uploadDate).toLocaleDateString()} + +
+
+ + ${file.message || 'Error reading file.'}
`; } else { - // Other files - show basic info + // Fallback for unknown preview types content = `
${file.originalName}
-- 2.49.1 From bcd5a52995d5003ed1e7c3021a268e8bed4599ff Mon Sep 17 00:00:00 2001 From: inubimambo Date: Tue, 8 Jul 2025 23:14:31 +0800 Subject: [PATCH 3/3] Improve dashboard UI and some fixes --- public/css/style.css | 279 ++++++++++++++++++++++++--- server.js | 439 ++++++++++++++++++++++++++++++++++++++----- views/dashboard.ejs | 136 +++++++++++++- views/quiz.ejs | 10 +- views/revise.ejs | 151 +++++++++++++-- 5 files changed, 918 insertions(+), 97 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 5b66cff..9a91bdc 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -156,8 +156,72 @@ body { } } -.file-icon { - font-size: 1.2rem; +.dashboard-file-icon { + font-size: 1.25rem !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + min-width: 50px !important; + width: 50px !important; + height: 50px !important; + flex-shrink: 0 !important; +} + +.dashboard-file-icon i { + font-size: 1.25rem !important; + line-height: 1 !important; + margin: 0 !important; + padding: 0 !important; + vertical-align: middle !important; +} + +/* Force proper icon rendering to prevent compression */ +.revised-files-section .file-icon { + display: flex !important; + align-items: center !important; + justify-content: center !important; + border-radius: 50% !important; + overflow: hidden; + position: relative; +} + +.revised-files-section .file-icon::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50%; + background: inherit; + z-index: -1; +} + +/* AI text icon styling */ +.revised-files-section .ai-icon { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important; + color: white !important; + font-weight: bold !important; + font-size: 1.2rem !important; + letter-spacing: 1px !important; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) !important; + border: 2px solid rgba(255, 255, 255, 0.2) !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; +} + +/* Ensure icon font doesn't get compressed */ +.revised-files-section .fa-brain { + transform: none !important; + font-weight: 900 !important; + font-family: "Font Awesome 5 Free" !important; + vertical-align: middle !important; + display: inline-block !important; + font-style: normal !important; + font-variant: normal !important; + text-rendering: auto !important; + line-height: 1 !important; + margin: 0 !important; + padding: 0 !important; } .progress-bar-animated { @@ -190,50 +254,203 @@ body { margin-top: auto; } -/* Responsive adjustments */ +/* Revised Files Section Styling */ +.revised-files-section .card { + transition: all 0.3s ease; +} + +.revised-files-section .card:hover { + transform: translateY(-2px); + box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.15) !important; +} + +.revised-files-section .file-icon { + min-width: 50px !important; + width: 50px !important; + height: 50px !important; + flex-shrink: 0 !important; + font-size: 1.25rem !important; + line-height: 1 !important; +} + +.revised-files-section .file-icon i { + font-size: 1.25rem !important; + line-height: 1 !important; +} + +.revised-files-section .text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.revised-files-section .min-w-0 { + min-width: 0; +} + +.revised-files-section .card-body { + overflow: hidden; +} + +.revised-files-section .btn-group { + width: 100%; +} + +.revised-files-section .btn-group .btn { + flex: 1; +} + +.revised-files-section .btn-group .btn:last-child { + flex: 0 0 auto; + min-width: 40px; +} + +/* File name truncation for long names */ +.file-name-truncate { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + vertical-align: bottom; +} + +/* Badge styling improvements */ +.revised-files-section .badge { + font-size: 0.7rem !important; + padding: 0.25em 0.5em; + white-space: nowrap; +} + +/* Button spacing and sizing improvements */ +.revised-files-section .d-grid .btn { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Ensure cards have consistent height */ +.revised-files-section .card { + height: 100%; + display: flex; + flex-direction: column; +} + +.revised-files-section .card-body { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +/* Responsive adjustments for small screens */ @media (max-width: 768px) { - .hero-section { - min-height: 50vh; - text-align: center; + .revised-files-section .file-name-truncate { + max-width: 150px; } - .hero-section .display-4 { - font-size: 2rem; - } - - .message-content { - max-width: 85% !important; - } - - .avatar { - width: 40px !important; - height: 40px !important; - min-width: 40px !important; - } - - .avatar i { + .revised-files-section .file-icon { + width: 45px !important; + height: 45px !important; + min-width: 45px !important; font-size: 1rem !important; } - .message-bubble { - padding: 0.75rem !important; - font-size: 0.9rem; + .revised-files-section .ai-icon { + font-size: 1rem !important; + letter-spacing: 0.5px !important; } - .card-body { - padding: 1.5rem; + .revised-files-section .file-icon i { + font-size: 1.1rem !important; } - #chat-container { - height: 400px !important; - padding: 1rem !important; + .revised-files-section .btn-group .btn { + font-size: 0.8rem; + padding: 0.375rem 0.5rem; + } +} + +/* Mobile responsive file icons */ +@media (max-width: 768px) { + .dashboard-file-icon { + min-width: 45px !important; + width: 45px !important; + height: 45px !important; + font-size: 1.1rem !important; } - .chat-message { - margin-bottom: 1rem; + .dashboard-file-icon i { + font-size: 1.1rem !important; } } +/* Dashboard section spacing */ +.dashboard-section { + margin-bottom: 2rem; +} + +.dashboard-section h3 { + font-weight: 600; + color: var(--dark-color); +} + +/* Enhanced visual separation between sections */ +.ai-revised-section { + background: linear-gradient(135deg, rgba(40, 167, 69, 0.05) 0%, rgba(40, 167, 69, 0.02) 100%); + border-radius: 1rem; + padding: 1.5rem; + margin-top: 2rem; +} + +/* Badge positioning fixes for dashboard cards */ +.card .badge { + position: relative; + z-index: 1; + display: inline-flex !important; + align-items: center !important; + white-space: nowrap !important; +} + +.card .text-end { + text-align: right !important; + position: relative; + z-index: 1; +} + +/* Ensure card body contains all content properly */ +.card-body { + position: relative; + overflow: hidden; + padding: 1rem !important; +} + +/* Prevent badge overflow */ +.card .d-flex .text-end { + flex-shrink: 0; + min-width: fit-content; +} + +.card .d-flex .flex-grow-1 { + overflow: hidden; + min-width: 0; +} + +/* Dashboard card container fixes */ +.dashboard-section .card { + position: relative; + overflow: visible; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.375rem; +} + +.dashboard-section .card-body { + display: flex; + flex-direction: column; + height: 100%; + position: relative; +} + /* Loading spinner */ .spinner-border { width: 3rem; diff --git a/server.js b/server.js index c002880..61f5622 100644 --- a/server.js +++ b/server.js @@ -868,13 +868,41 @@ app.get('/revise/:fileId', requireAuth, async (req, res) => { }); } - // Read file content - const fileContent = await fs.readFile(file.path, 'utf-8'); + const filePath = path.join(__dirname, file.path); + const fileExtension = path.extname(file.originalName).toLowerCase(); + + // Extract text from the document using the same method as file preview + const extractionResult = await extractTextFromDocument(filePath, fileExtension); + + let fileContent = ''; + let extractionInfo = null; + let extractionError = null; + + if (extractionResult.success) { + fileContent = extractionResult.text; + extractionInfo = { + method: extractionResult.method, + totalLength: extractionResult.extractedLength, + pages: extractionResult.pages || null, + sheets: extractionResult.sheets || null + }; + } else { + // Try to read as plain text if extraction failed + try { + fileContent = await fs.readFile(filePath, 'utf-8'); + extractionInfo = { method: 'fallback-text', totalLength: fileContent.length }; + } catch (readError) { + extractionError = `Failed to extract or read file: ${extractionResult.error}`; + fileContent = `Error: Unable to read file content. ${extractionResult.error}`; + } + } res.render('revise', { title: 'Revise Notes - EduCat', file: file, - content: fileContent + content: fileContent, + extractionInfo: extractionInfo, + extractionError: extractionError }); } catch (error) { console.error('Error loading file:', error); @@ -899,10 +927,9 @@ app.post('/api/revise', requireAuth, async (req, res) => { if (await fs.pathExists(documentPath)) { const documentData = await fs.readJSON(documentPath); contextContent = documentData.content; - } } catch (error) { - + console.log('Could not load stored document, using provided content'); } } @@ -932,13 +959,236 @@ app.post('/api/revise', requireAuth, async (req, res) => { }); } catch (error) { console.error('Revision error:', error); - res.status(500).json({ - error: 'Failed to revise notes', + res.json({ + success: false, + error: 'Failed to revise notes. Please try again.', details: error.message }); } }); +// Save revised notes endpoint +app.post('/api/save-revised', requireAuth, async (req, res) => { + try { + const { fileId, revisedContent, revisionType } = req.body; + + if (!fileId || !revisedContent) { + return res.json({ + success: false, + error: 'File ID and revised content are required' + }); + } + + // Find the original file + const files = req.session.uploadedFiles || []; + const originalFile = files.find(f => f.id === fileId); + + if (!originalFile) { + return res.json({ + success: false, + error: 'Original file not found' + }); + } + + // Create revised notes directory if it doesn't exist + const revisedNotesDir = path.join(__dirname, 'uploads', 'revised-notes'); + await fs.ensureDir(revisedNotesDir); + + // Create revised file info + const timestamp = Date.now(); + const originalExt = path.extname(originalFile.originalName); + const fileName = `${path.parse(originalFile.originalName).name}_${revisionType}_revised_${timestamp}.txt`; + const filePath = path.join(revisedNotesDir, fileName); + + const revisedFileInfo = { + id: uuidv4(), + originalName: fileName, + filename: fileName, + path: filePath, + size: Buffer.byteLength(revisedContent, 'utf8'), + mimetype: 'text/plain', + uploadDate: new Date().toISOString(), + userId: req.session.userId, + status: 'processed', + isRevised: true, + originalFileId: fileId, + revisionType: revisionType, + originalFileName: originalFile.originalName + }; + + // Save revised content to file + await fs.writeFile(revisedFileInfo.path, revisedContent, 'utf8'); + + // Don't add revised files to regular uploadedFiles - store separately + if (!req.session.revisedFiles) { + req.session.revisedFiles = []; + } + req.session.revisedFiles.push(revisedFileInfo); + + // Save to persistent storage in a separate category + await saveRevisedFile(req.session.userId, revisedFileInfo); + + res.json({ + success: true, + message: 'Revised notes saved successfully!', + fileInfo: revisedFileInfo + }); + + } catch (error) { + console.error('Save revised notes error:', error); + res.json({ + success: false, + error: 'Failed to save revised notes', + details: error.message + }); + } +}); + +// Download revised notes endpoint +app.get('/api/download-revised/:fileId', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const { content } = req.query; + + if (!content) { + return res.status(400).json({ + success: false, + error: 'No content provided for download' + }); + } + + // Find the original file to get naming info + const files = req.session.uploadedFiles || []; + const originalFile = files.find(f => f.id === fileId); + + // Get the revision type from the query if available + const revisionType = req.query.revisionType || 'revised'; + + const fileName = originalFile + ? `${path.parse(originalFile.originalName).name}_${revisionType}.txt` + : `revised_notes_${Date.now()}.txt`; + + // Set headers for file download + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`); + res.setHeader('Content-Length', Buffer.byteLength(content, 'utf8')); + + // Send the content + res.send(content); + + } catch (error) { + console.error('Download revised notes error:', error); + res.status(500).json({ + success: false, + error: 'Failed to download revised notes', + details: error.message + }); + } +}); + +// Serve revised files for direct download +app.get('/uploads/revised-notes/:filename', requireAuth, (req, res) => { + try { + const filename = req.params.filename; + const filePath = path.join(__dirname, 'uploads', 'revised-notes', filename); + + // Check if file exists + if (!fs.existsSync(filePath)) { + return res.status(404).json({ + success: false, + error: 'File not found' + }); + } + + // Serve the file + res.download(filePath, filename); + } catch (error) { + console.error('Error serving revised file:', error); + res.status(500).json({ + success: false, + error: 'Failed to serve file' + }); + } +}); + +// Delete revised file endpoint +app.delete('/api/revised-files/:fileId', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const revisedFiles = req.session.revisedFiles || []; + const fileIndex = revisedFiles.findIndex(f => f.id === fileId); + + if (fileIndex === -1) { + return res.status(404).json({ success: false, error: 'File not found' }); + } + + const file = revisedFiles[fileIndex]; + + // Delete the physical file + if (await fs.pathExists(file.path)) { + await fs.unlink(file.path); + } + + // Remove from session + req.session.revisedFiles.splice(fileIndex, 1); + + // Remove from persistent storage + await removeRevisedFile(req.session.userId, fileId); + + res.json({ + success: true, + message: 'Revised file deleted successfully' + }); + } catch (error) { + console.error('Error deleting revised file:', error); + res.status(500).json({ + success: false, + error: 'Failed to delete file', + details: error.message + }); + } +}); + +// Get revised file info endpoint +app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const revisedFiles = req.session.revisedFiles || []; + const file = revisedFiles.find(f => f.id === fileId); + + if (!file) { + // Try to load from persistent storage + const persistentRevisedFiles = await loadRevisedFiles(req.session.userId); + const persistentFile = persistentRevisedFiles.find(f => f.id === fileId); + + if (!persistentFile) { + return res.status(404).json({ + success: false, + error: 'File not found' + }); + } + + return res.json({ + success: true, + file: persistentFile + }); + } + + res.json({ + success: true, + file: file + }); + } catch (error) { + console.error('Error getting revised file info:', error); + res.status(500).json({ + success: false, + error: 'Failed to get file info', + details: error.message + }); + } +}); + +// ChatGPT integration routes app.get('/chat', requireAuth, (req, res) => { // Initialize chat history if it doesn't exist if (!req.session.chatHistory) { @@ -1089,6 +1339,9 @@ app.get('/dashboard', requireAuth, async (req, res) => { // Load persistent files for this user const persistentFiles = await loadUserFiles(req.session.userId); + // Load revised files separately + const revisedFiles = await loadRevisedFiles(req.session.userId); + // Merge with session files (in case there are newly uploaded files not yet saved) const sessionFiles = req.session.uploadedFiles || []; const allFiles = [...persistentFiles]; @@ -1100,18 +1353,30 @@ app.get('/dashboard', requireAuth, async (req, res) => { } }); + // Merge revised files from session + const sessionRevisedFiles = req.session.revisedFiles || []; + const allRevisedFiles = [...revisedFiles]; + sessionRevisedFiles.forEach(sessionFile => { + if (!revisedFiles.find(f => f.id === sessionFile.id)) { + allRevisedFiles.push(sessionFile); + } + }); + // Update session with merged files for current session use req.session.uploadedFiles = allFiles; + req.session.revisedFiles = allRevisedFiles; res.render('dashboard', { title: 'Dashboard - EduCat', - files: allFiles + files: allFiles, + revisedFiles: allRevisedFiles }); } catch (error) { console.error('Error loading dashboard:', error); res.render('dashboard', { title: 'Dashboard - EduCat', - files: req.session.uploadedFiles || [] + files: req.session.uploadedFiles || [], + revisedFiles: [] }); } }); @@ -1454,6 +1719,7 @@ app.post('/api/generate-quiz', requireAuth, async (req, res) => { { "question": "What is the process by which plants make their own food?", "answer": "Photosynthesis", + "explanation": "Photosynthesis is the process by which plants use sunlight, water, and carbon dioxide to produce glucose and oxygen.", "keywords": ["photosynthesis", "sunlight", "chlorophyll"] } ] @@ -1615,13 +1881,15 @@ app.post('/api/submit-quiz', requireAuth, async (req, res) => { quiz.forEach((question, index) => { const userAnswer = answers[index]; - const isCorrect = userAnswer === question.correct; + // Handle different question types - short-answer uses 'answer', others use 'correct' + const correctAnswer = question.answer || question.correct; + const isCorrect = userAnswer === correctAnswer; if (isCorrect) score++; results.push({ question: question.question, userAnswer: userAnswer, - correctAnswer: question.correct, + correctAnswer: correctAnswer, isCorrect: isCorrect, explanation: question.explanation || '', options: question.options || null // Include options for multiple choice @@ -1732,53 +2000,128 @@ async function removeUserFile(userId, fileId) { await saveUserFiles(userId, filteredFiles); } -// Quiz results persistence -const QUIZ_RESULTS_FILE = path.join(__dirname, 'data', 'quiz-results.json'); +// Revised files storage persistence +const REVISED_FILES_DIR = path.join(__dirname, 'data', 'revised-files'); -// Ensure data directory exists -async function ensureDataDirectory() { - await fs.ensureDir(path.join(__dirname, 'data')); +// Ensure revised files directory exists +async function ensureRevisedFilesDirectory() { + await fs.ensureDir(REVISED_FILES_DIR); } -// Load quiz results from file +// Save revised files to persistent storage +async function saveRevisedFiles(userId, files) { + try { + await ensureRevisedFilesDirectory(); + const revisedFilePath = path.join(REVISED_FILES_DIR, `user-${userId}-revised.json`); + await fs.writeJSON(revisedFilePath, files, { spaces: 2 }); + } catch (error) { + console.error('Error saving revised files:', error); + } +} + +// Load revised files from persistent storage +async function loadRevisedFiles(userId) { + try { + await ensureRevisedFilesDirectory(); + const revisedFilePath = path.join(REVISED_FILES_DIR, `user-${userId}-revised.json`); + if (await fs.pathExists(revisedFilePath)) { + const files = await fs.readJSON(revisedFilePath); + return files || []; + } + return []; + } catch (error) { + console.error('Error loading revised files:', error); + return []; + } +} + +// Add a revised file to user's persistent storage +async function saveRevisedFile(userId, fileInfo) { + const revisedFiles = await loadRevisedFiles(userId); + revisedFiles.push(fileInfo); + await saveRevisedFiles(userId, revisedFiles); +} + +// Remove a revised file from user's persistent storage +async function removeRevisedFile(userId, fileId) { + const revisedFiles = await loadRevisedFiles(userId); + const filteredFiles = revisedFiles.filter(f => f.id !== fileId); + await saveRevisedFiles(userId, filteredFiles); +} + +// Quiz results storage persistence +const QUIZ_RESULTS_DIR = path.join(__dirname, 'data', 'quiz-results'); + +// Ensure quiz results directory exists +async function ensureQuizResultsDirectory() { + await fs.ensureDir(QUIZ_RESULTS_DIR); +} + +// Save quiz results to persistent storage +async function saveQuizResults(allResults) { + try { + await ensureQuizResultsDirectory(); + const resultsPath = path.join(QUIZ_RESULTS_DIR, 'quiz-results.json'); + await fs.writeJSON(resultsPath, allResults, { spaces: 2 }); + } catch (error) { + console.error('Error saving quiz results:', error); + throw error; + } +} + +// Load all quiz results from persistent storage async function loadQuizResults() { try { - await ensureDataDirectory(); - if (await fs.pathExists(QUIZ_RESULTS_FILE)) { - const data = await fs.readJSON(QUIZ_RESULTS_FILE); - return data || {}; + await ensureQuizResultsDirectory(); + const resultsPath = path.join(QUIZ_RESULTS_DIR, 'quiz-results.json'); + + if (await fs.pathExists(resultsPath)) { + return await fs.readJSON(resultsPath); + } else { + return {}; } - return {}; } catch (error) { console.error('Error loading quiz results:', error); return {}; } } -// Save quiz results to file -async function saveQuizResults(results) { - try { - await ensureDataDirectory(); - await fs.writeJSON(QUIZ_RESULTS_FILE, results, { spaces: 2 }); - } catch (error) { - console.error('Error saving quiz results:', error); - } -} - -// Add quiz result for user -async function addQuizResult(userId, quizResult) { - const allResults = await loadQuizResults(); - if (!allResults[userId]) { - allResults[userId] = []; - } - allResults[userId].push(quizResult); - await saveQuizResults(allResults); -} - -// Get quiz results for user +// Get quiz results for a specific user async function getUserQuizResults(userId) { - const allResults = await loadQuizResults(); - return allResults[userId] || []; + try { + const allResults = await loadQuizResults(); + return allResults[userId] || []; + } catch (error) { + console.error('Error loading user quiz results:', error); + return []; + } +} + +// Add a quiz result to user's persistent storage +async function addQuizResult(userId, quizResult) { + try { + const allResults = await loadQuizResults(); + if (!allResults[userId]) { + allResults[userId] = []; + } + allResults[userId].push(quizResult); + await saveQuizResults(allResults); + } catch (error) { + console.error('Error adding quiz result:', error); + throw error; + } +} + +// Clear quiz results for a specific user +async function clearUserQuizResults(userId) { + try { + const allResults = await loadQuizResults(); + allResults[userId] = []; + await saveQuizResults(allResults); + } catch (error) { + console.error('Error clearing user quiz results:', error); + throw error; + } } // Error handling middleware @@ -1814,11 +2157,9 @@ app.get('/quiz-history', requireAuth, async (req, res) => { app.delete('/api/quiz-history', requireAuth, async (req, res) => { try { const userId = req.session.userId; - const allResults = await loadQuizResults(); - // Clear quiz history for this user - allResults[userId] = []; - await saveQuizResults(allResults); + // Clear quiz history for this user using the dedicated function + await clearUserQuizResults(userId); diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 31e1cf1..adc90e9 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -31,7 +31,7 @@
-
+
@@ -109,6 +109,73 @@ <% }); %>
<% } %> + + + <% if (typeof revisedFiles !== 'undefined' && revisedFiles.length > 0) { %> +
+
+

AI-Revised Notes

+ + <%= revisedFiles.length %> revised file<%= revisedFiles.length !== 1 ? 's' : '' %> + +
+
+ <% revisedFiles.forEach(function(file, index) { %> +
+
+
+
+
+ AI +
+
+
+ <%= file.originalName %> +
+
+ <%= Math.round(file.size / 1024) %> KB + + <%= file.revisionType.charAt(0).toUpperCase() + file.revisionType.slice(1) %> + +
+
+
+ +
+
+ + <%= new Date(file.uploadDate).toLocaleDateString() %> +
+ <% if (file.originalFileName) { %> +
+ + From: <%= file.originalFileName %> +
+ <% } %> +
+ +
+
+ +
+ + Download + + +
+
+
+
+
+
+ <% }); %> +
+
+ <% } %>
@@ -806,6 +873,73 @@ document.addEventListener('DOMContentLoaded', function() { }, 10000); } }); + +// Preview revised file function +async function previewRevisedFile(fileId) { + try { + // Get file information by making an API call instead of using template data + const response = await fetch(`/api/revised-files/${fileId}/info`); + if (!response.ok) { + throw new Error('Failed to get file info'); + } + + const fileInfo = await response.json(); + if (!fileInfo.success) { + throw new Error(fileInfo.error || 'Failed to get file info'); + } + + const file = fileInfo.file; + + // Download the file content + const contentResponse = await fetch(`/uploads/revised-notes/${file.filename}`); + if (!contentResponse.ok) { + throw new Error('Failed to load file content'); + } + + const content = await contentResponse.text(); + const modal = new bootstrap.Modal(document.getElementById('previewModal')); + + document.getElementById('preview-content').innerHTML = ` +
+ + AI-Revised Content • ${file.revisionType} • From: ${file.originalFileName || 'Unknown'} +
+
+
${escapeHtml(content)}
+
+ `; + + modal.show(); + } catch (error) { + console.error('Error previewing revised file:', error); + alert('Failed to preview file: ' + error.message); + } +} + +// Delete revised file function +async function deleteRevisedFile(fileId) { + if (!confirm('Are you sure you want to delete this revised file? This action cannot be undone.')) { + return; + } + + try { + const response = await fetch(`/api/revised-files/${fileId}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + // Reload the page to update the file list + location.reload(); + } else { + alert('Error deleting file: ' + (result.error || 'Unknown error')); + } + } catch (error) { + console.error('Error deleting revised file:', error); + alert('Network error: ' + error.message); + } +} <%- include('partials/footer') %> diff --git a/views/quiz.ejs b/views/quiz.ejs index e6a1ec9..fa2fde8 100644 --- a/views/quiz.ejs +++ b/views/quiz.ejs @@ -377,8 +377,8 @@ document.addEventListener('DOMContentLoaded', function() { `; }); html += '
'; - } else if (question.correct === 'True' || question.correct === 'False') { - // True/False + } else if ((question.correct === 'True' || question.correct === 'False') && !question.answer) { + // True/False (only if it's not a short-answer question) html += `
@@ -766,8 +766,8 @@ document.addEventListener('DOMContentLoaded', function() { `; }); html += '
'; - } else if (question.correct === 'True' || question.correct === 'False') { - // True/False + } else if ((question.correct === 'True' || question.correct === 'False') && !question.answer) { + // True/False (only if it's not a short-answer question) const userSelected = userAnswers[index]; const correctAnswer = question.correct; @@ -803,7 +803,7 @@ document.addEventListener('DOMContentLoaded', function() { } else { // Short answer const userAnswer = userAnswers[index] || ''; - const correctAnswer = question.correct || ''; + const correctAnswer = question.answer || question.correct || ''; html += `
diff --git a/views/revise.ejs b/views/revise.ejs index b0d16d7..a6c89d0 100644 --- a/views/revise.ejs +++ b/views/revise.ejs @@ -8,11 +8,30 @@

Revise: <%= file.originalName %>

+ <% if (extractionError) { %> +
+ + <%= extractionError %> +
+ <% } else if (extractionInfo) { %> +
+ + Text extracted using <%= extractionInfo.method %> + <% if (extractionInfo.pages) { %> + | <%= extractionInfo.pages %> pages + <% } %> + <% if (extractionInfo.sheets) { %> + | <%= extractionInfo.sheets %> sheets + <% } %> + | <%= extractionInfo.totalLength %> characters +
+ <% } %> +
Original Notes
-
<%= content %>
+
<%= content %>
@@ -58,7 +77,32 @@
  • Name: <%= file.originalName %>
  • Size: <%= Math.round(file.size / 1024) %> KB
  • Uploaded: <%= new Date(file.uploadDate).toLocaleDateString() %>
  • + <% if (extractionInfo) { %> +
  • Extraction: <%= extractionInfo.method %>
  • + <% if (extractionInfo.pages) { %> +
  • Pages: <%= extractionInfo.pages %>
  • + <% } %> + <% if (extractionInfo.sheets) { %> +
  • Sheets: <%= extractionInfo.sheets %>
  • + <% } %> +
  • Characters: <%= extractionInfo.totalLength.toLocaleString() %>
  • + <% } %> + + <% if (file.status) { %> +
    + Processing Status: + <% if (file.status === 'processed') { %> + Processed + <% } else if (file.status === 'processing') { %> + Processing + <% } else if (file.status === 'failed') { %> + Failed + <% } else { %> + <%= file.status %> + <% } %> +
    + <% } %>
    @@ -96,15 +140,20 @@ document.addEventListener('DOMContentLoaded', function() { const saveBtn = document.getElementById('save-btn'); const downloadBtn = document.getElementById('download-btn'); + const fileId = '<%= file.id %>'; + const content = <%- JSON.stringify(content) %>; + let currentRevisedContent = ''; + let currentRevisionType = ''; + reviseBtn.addEventListener('click', async function() { const type = revisionType.value; - const content = `<%= content.replace(/"/g, '\\"').replace(/\n/g, '\\n') %>`; console.log('Revise button clicked with type:', type); + console.log('File ID:', fileId); reviseBtn.disabled = true; revisionProgress.classList.remove('d-none'); - revisedContent.innerHTML = '

    Processing...

    '; + revisedContent.innerHTML = '
    Processing...

    AI is processing your notes...

    '; try { console.log('Sending request to /api/revise'); @@ -115,34 +164,114 @@ document.addEventListener('DOMContentLoaded', function() { }, body: JSON.stringify({ content: content, - revisionType: type + revisionType: type, + fileId: fileId }) }); console.log('Response status:', response.status); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const result = await response.json(); console.log('Revision result:', result); if (result.success) { - revisedContent.innerHTML = '
    ' + result.revisedContent + '
    '; + revisedContent.innerHTML = '
    ' + escapeHtml(result.revisedContent) + '
    '; + currentRevisedContent = result.revisedContent; + currentRevisionType = type; saveBtn.disabled = false; downloadBtn.disabled = false; } else { - revisedContent.innerHTML = '
    Error: ' + (result.error || 'Unknown error') + '
    '; + revisedContent.innerHTML = '
    Error: ' + escapeHtml(result.error || 'Unknown error occurred') + '
    '; } } catch (error) { console.error('Revision error:', error); - revisedContent.innerHTML = '
    Error: ' + error.message + '
    '; + revisedContent.innerHTML = '
    Network Error: ' + escapeHtml(error.message) + '
    Please check your connection and try again.
    '; } finally { reviseBtn.disabled = false; revisionProgress.classList.add('d-none'); } }); + + // Save revised notes button + saveBtn.addEventListener('click', async function() { + if (!currentRevisedContent) { + alert('No revised content to save. Please revise the notes first.'); + return; + } + + saveBtn.disabled = true; + const originalText = saveBtn.innerHTML; + saveBtn.innerHTML = 'Saving...'; + + try { + const response = await fetch('/api/save-revised', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fileId: fileId, + revisedContent: currentRevisedContent, + revisionType: currentRevisionType + }) + }); + + const result = await response.json(); + + if (result.success) { + // Show success message + const successAlert = document.createElement('div'); + successAlert.className = 'alert alert-success alert-dismissible fade show mt-3'; + successAlert.innerHTML = ` + + Success! Revised notes saved as "${result.fileInfo.originalName}" + + `; + document.querySelector('.card-body').appendChild(successAlert); + + // Auto-hide after 5 seconds + setTimeout(() => { + successAlert.remove(); + }, 5000); + } else { + alert('Error saving revised notes: ' + (result.error || 'Unknown error')); + } + } catch (error) { + console.error('Save error:', error); + alert('Network error while saving. Please try again.'); + } finally { + saveBtn.disabled = false; + saveBtn.innerHTML = originalText; + } + }); + + // Download revised notes button + downloadBtn.addEventListener('click', function() { + if (!currentRevisedContent) { + alert('No revised content to download. Please revise the notes first.'); + return; + } + + // Create download URL with content and revision type as query parameters + const encodedContent = encodeURIComponent(currentRevisedContent); + const encodedRevisionType = encodeURIComponent(currentRevisionType); + const downloadUrl = `/api/download-revised/${fileId}?content=${encodedContent}&revisionType=${encodedRevisionType}`; + + // Create temporary link and trigger download + const link = document.createElement('a'); + link.href = downloadUrl; + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }); + + // Helper function to escape HTML + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } }); -- 2.49.1