commit f5f5189fa086719648c73650f4c27799329fa511 Author: inubimambo Date: Sat Jul 5 23:37:31 2025 +0800 EduCat with Flowise integration - complete implementation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5251493 --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +# Environment variables +.env +.env.local +.env.production + +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Uploads and user data +uploads/ +data/ + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +tmp/ +temp/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..833eef0 --- /dev/null +++ b/README.md @@ -0,0 +1,210 @@ +# EduCat - AI-Powered Note Revision Platform + +EduCat is a modern web application that helps students improve their study notes using AI. Built with Node.js, Express, and EJS, it features file upload capabilities, AI-powered note revision, and an interactive chatbot. + +## Features + +- � **User Authentication**: Secure login and registration system +- �📁 **File Upload**: Upload notes in various formats (PDF, DOC, TXT, images) +- 🤖 **AI-Powered Revision**: Automatically improve, summarize, and generate study questions +- 💬 **Interactive Chatbot**: Chat with AI for study assistance +- 📊 **Dashboard**: Manage and track your uploaded notes +- 🎨 **Modern UI**: Beautiful, responsive design with Bootstrap +- 🔗 **Flowise Integration**: Connected to Flowise at https://flowise.suika.cc/ + +## Technologies Used + +- **Backend**: Node.js, Express.js +- **Frontend**: EJS templates, Bootstrap 5, Font Awesome +- **Authentication**: bcrypt for password hashing, express-session +- **File Handling**: Multer for file uploads +- **AI Integration**: Flowise API integration +- **Session Management**: Express Session with flash messages +- **Styling**: Custom CSS with Bootstrap + +## Installation + +1. **Clone the repository**: + ```bash + git clone + cd EduCat + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Configure environment variables**: + - Copy `.env.example` to `.env` (if exists) or create a new `.env` file + - Update the following variables: + ``` + PORT=3000 + SESSION_SECRET=your-secret-key-here + FLOWISE_API_URL=https://flowise.suika.cc/api/v1/prediction + FLOWISE_CHATFLOW_ID=your-chatflow-id-here + ``` + +4. **Set up your Flowise chatflow**: + - Go to https://flowise.suika.cc/ + - Create or find your chatflow + - Copy the chatflow ID and update it in the `.env` file + +5. **Start the application**: + ```bash + npm start + ``` + + For development with auto-reload: + ```bash + npm run dev + ``` + +6. **Access the application**: + - Open your browser and navigate to `http://localhost:3000` + +## Usage + +### Authentication +1. Visit `http://localhost:3000` +2. Click "Login" or "Register" to create an account +3. Use demo accounts: + - **Admin**: username `admin`, password `password` + - **Student**: username `student`, password `password` + +### Uploading Notes +1. After logging in, click "Upload Notes" in the navigation +2. Drag and drop your file or click to browse +3. Select a file (PDF, DOC, TXT, or image) +4. Click "Upload & Process" + +### Revising Notes +1. Go to your Dashboard to see uploaded files +2. Click "Revise with AI" on any file +3. Choose revision type: + - **Improve & Enhance**: Makes notes more comprehensive + - **Summarize**: Creates concise summaries + - **Generate Questions**: Creates study questions +4. Click "Revise with AI" to process + +### Using the Chatbot +1. Navigate to the "Chat" section +2. Type your questions about study materials or academic topics +3. Get instant AI-powered responses + +## Project Structure + +``` +EduCat/ +├── public/ +│ ├── css/ +│ │ └── style.css +│ ├── js/ +│ │ └── main.js +│ └── images/ +│ └── logo.png +├── views/ +│ ├── partials/ +│ │ ├── header.ejs +│ │ └── footer.ejs +│ ├── index.ejs +│ ├── upload.ejs +│ ├── revise.ejs +│ ├── chat.ejs +│ ├── dashboard.ejs +│ └── error.ejs +├── uploads/ +├── server.js +├── package.json +└── .env +``` + +## API Endpoints + +- `GET /` - Home page +- `GET /upload` - File upload page +- `POST /upload` - Handle file uploads +- `GET /revise/:fileId` - Note revision page +- `POST /api/revise` - AI revision endpoint +- `GET /chat` - Chat interface +- `POST /api/chat` - Chat API endpoint +- `GET /dashboard` - User dashboard + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `PORT` | Server port | `3000` | +| `SESSION_SECRET` | Session encryption key | `educat-secret-key` | +| `FLOWISE_API_URL` | Flowise API base URL | `https://flowise.suika.cc/api/v1/prediction` | +| `FLOWISE_CHATFLOW_ID` | Your Flowise chatflow ID | Required | + +### File Upload Settings + +- **Maximum file size**: 10MB +- **Allowed formats**: PDF, DOC, DOCX, TXT, JPG, JPEG, PNG, GIF +- **Upload directory**: `uploads/` + +## Customization + +### Styling +- Edit `public/css/style.css` to customize the appearance +- The design uses Bootstrap 5 with custom CSS variables + +### AI Integration +- Modify the Flowise API calls in `server.js` +- Update prompts in the `/api/revise` endpoint +- Customize chat responses in the `/api/chat` endpoint + +### Adding Features +- Add new routes in `server.js` +- Create corresponding EJS templates in `views/` +- Add client-side JavaScript in `public/js/main.js` + +## Troubleshooting + +### Common Issues + +1. **File upload fails**: + - Check file size (max 10MB) + - Verify file format is supported + - Ensure `uploads/` directory exists + +2. **AI responses don't work**: + - Verify Flowise API URL is correct + - Check if your chatflow ID is valid + - Ensure Flowise server is accessible + +3. **Session issues**: + - Verify SESSION_SECRET is set + - Check if sessions are properly configured + +### Development + +To run in development mode with auto-reload: +```bash +npm install -g nodemon +npm run dev +``` + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## License + +This project is licensed under the MIT License. + +## Support + +For support or questions, please contact the EduCat development team. + +--- + +**Made with ❤️ by the EduCat Team** diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ded8a6b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2293 @@ +{ + "name": "educat-ai-notes", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "educat-ai-notes", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "^1.7.7", + "bcrypt": "^5.1.1", + "body-parser": "^1.20.2", + "connect-flash": "^0.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "ejs": "^3.1.10", + "express": "^4.19.2", + "express-session": "^1.18.0", + "form-data": "^4.0.3", + "fs-extra": "^11.2.0", + "multer": "^2.0.0", + "uuid": "^10.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "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", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "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", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/connect-flash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", + "integrity": "sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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/filelist/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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "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" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..971155f --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "educat-ai-notes", + "version": "1.0.0", + "description": "EduCat - AI-powered note revision platform for students", + "main": "server.js", + "scripts": { + "start": "node --no-deprecation server.js", + "dev": "nodemon --no-deprecation server.js" + }, + "dependencies": { + "axios": "^1.7.7", + "bcrypt": "^5.1.1", + "body-parser": "^1.20.2", + "connect-flash": "^0.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "ejs": "^3.1.10", + "express": "^4.19.2", + "express-session": "^1.18.0", + "form-data": "^4.0.3", + "fs-extra": "^11.2.0", + "multer": "^2.0.0", + "uuid": "^10.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "keywords": [ + "education", + "ai", + "notes", + "revision", + "chatbot" + ], + "author": "EduCat Team", + "license": "MIT" +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..5b66cff --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,399 @@ +:root { + --primary-color: #007bff; + --secondary-color: #6c757d; + --success-color: #28a745; + --info-color: #17a2b8; + --warning-color: #ffc107; + --danger-color: #dc3545; + --light-color: #f8f9fa; + --dark-color: #343a40; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f8f9fa; +} + +.hero-section { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); + min-height: 70vh; + display: flex; + align-items: center; +} + +.bg-gradient-primary { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); +} + +.navbar-brand img { + border-radius: 50%; +} + +.upload-dropzone { + transition: all 0.3s ease; + cursor: pointer; +} + +.upload-dropzone:hover { + background-color: #e7f3ff; + border-color: #0056b3; +} + +.upload-dropzone.dragover { + background-color: #e7f3ff; + border-color: #0056b3; + transform: scale(1.02); +} + +.feature-icon { + font-size: 1.5rem; +} + +.step-number { + font-size: 1.2rem; + font-weight: bold; +} + +.card { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.chat-message { + margin-bottom: 1.5rem; + opacity: 0; + animation: slideInMessage 0.3s ease-out forwards; +} + +@keyframes slideInMessage { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message-bubble { + max-width: 100%; + word-wrap: break-word; + position: relative; + transition: all 0.2s ease; +} + +.message-bubble:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important; +} + +.user-message .message-bubble { + background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%) !important; + color: white; + border: none !important; +} + +.bot-message .message-bubble { + background-color: #ffffff; + border: 1px solid #e9ecef; + color: #495057; +} + +.avatar { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: all 0.2s ease; +} + +.avatar:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.bot-message .avatar { + background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%) !important; +} + +.user-message .avatar { + background: linear-gradient(135deg, var(--secondary-color) 0%, #495057 100%) !important; +} + +.typing-dots { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 0; +} + +.typing-dots span { + width: 10px; + height: 10px; + border-radius: 50%; + background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%); + animation: typing 1.4s infinite ease-in-out; +} + +.typing-dots span:nth-child(1) { + animation-delay: -0.32s; +} + +.typing-dots span:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes typing { + 0%, 80%, 100% { + transform: scale(0.8); + opacity: 0.4; + } + 40% { + transform: scale(1.2); + opacity: 1; + } +} + +.file-icon { + font-size: 1.2rem; +} + +.progress-bar-animated { + animation: progress-bar-stripes 1s linear infinite; +} + +@keyframes progress-bar-stripes { + 0% { + background-position: 1rem 0; + } + 100% { + background-position: 0 0; + } +} + +.btn { + transition: all 0.3s ease; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.navbar { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.footer { + margin-top: auto; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .hero-section { + min-height: 50vh; + text-align: center; + } + + .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 { + font-size: 1rem !important; + } + + .message-bubble { + padding: 0.75rem !important; + font-size: 0.9rem; + } + + .card-body { + padding: 1.5rem; + } + + #chat-container { + height: 400px !important; + padding: 1rem !important; + } + + .chat-message { + margin-bottom: 1rem; + } +} + +/* Loading spinner */ +.spinner-border { + width: 3rem; + height: 3rem; +} + +/* Custom scrollbar for chat */ +#chat-container { + scrollbar-width: thin; + scrollbar-color: #c1c1c1 #f1f1f1; +} + +#chat-container::-webkit-scrollbar { + width: 8px; +} + +#chat-container::-webkit-scrollbar-track { + background: #f8f9fa; + border-radius: 4px; +} + +#chat-container::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #c1c1c1 0%, #a8a8a8 100%); + border-radius: 4px; +} + +#chat-container::-webkit-scrollbar-thumb:hover { + background: linear-gradient(135deg, #a8a8a8 0%, #888888 100%); +} + +/* File upload styling */ +.upload-area { + border: 2px dashed #dee2e6; + border-radius: 8px; + padding: 2rem; + text-align: center; + background-color: #f8f9fa; + transition: all 0.3s ease; +} + +.upload-area:hover { + border-color: var(--primary-color); + background-color: #e7f3ff; +} + +.upload-area.dragover { + border-color: var(--primary-color); + background-color: #e7f3ff; + transform: scale(1.02); +} + +/* Success states */ +.upload-success { + border-color: var(--success-color); + background-color: #d4edda; +} + +/* Error states */ +.upload-error { + border-color: var(--danger-color); + background-color: #f8d7da; +} + +/* Animation for page transitions */ +.fade-in { + animation: fadeIn 0.5s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Quiz Section Styles */ +.bg-gradient-warning { + background: linear-gradient(135deg, #ffc107 0%, #ff9800 100%); +} + +.quiz-preview { + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } +} + +.quiz-features .fas { + font-size: 1.1rem; +} + +.quiz-question { + border-left: 4px solid var(--primary-color); + padding-left: 1rem; +} + +.quiz-options .form-check-label { + cursor: pointer; + padding: 0.5rem 0; +} + +.quiz-options .form-check-input:checked + .form-check-label { + font-weight: bold; + color: var(--success-color); +} + +/* Chat input styling */ +#chat-input { + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; +} + +#chat-input:focus { + box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2) !important; + border-color: var(--primary-color) !important; + transform: translateY(-1px); +} + +#send-btn { + background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%); + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +#send-btn:hover { + background: linear-gradient(135deg, #0056b3 0%, #004085 100%); + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +#send-btn:active { + transform: translateY(0); +} + +/* Chat container enhancements */ +#chat-container { + background: linear-gradient(to bottom, #ffffff 0%, #f8f9fa 100%); + border-radius: 0; +} + +.chat .card-header { + background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%) !important; + border-radius: 0.375rem 0.375rem 0 0 !important; +} + +/* Enhance chat card styling */ +.chat .card { + border: none; + box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.1) !important; + border-radius: 0.75rem; + overflow: hidden; +} diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..154e727 Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..0667f63 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,518 @@ +// Main JavaScript file for EduCat + +document.addEventListener('DOMContentLoaded', function() { + // Initialize tooltips + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); + + // Initialize file upload functionality + initializeFileUpload(); + + // Initialize drag and drop + initializeDragAndDrop(); + + // Add fade-in animation to cards + animateCards(); +}); + +function initializeFileUpload() { + const fileInput = document.getElementById('noteFile'); + const uploadBtn = document.getElementById('upload-btn'); + const fileInfo = document.getElementById('file-info'); + const fileName = document.getElementById('file-name'); + const fileSize = document.getElementById('file-size'); + const uploadProgress = document.getElementById('upload-progress'); + const uploadResult = document.getElementById('upload-result'); + const uploadArea = document.getElementById('upload-area'); + + if (!fileInput) return; + + // Remove any existing event listeners to prevent duplicates + fileInput.removeEventListener('change', handleFileChange); + fileInput.addEventListener('change', handleFileChange); + + if (uploadBtn) { + uploadBtn.removeEventListener('click', handleUploadClick); + uploadBtn.addEventListener('click', handleUploadClick); + } + + function handleFileChange(e) { + const file = e.target.files[0]; + if (file) { + showFileInfo(file); + uploadBtn.disabled = false; + } + } + + function handleUploadClick(e) { + e.preventDefault(); + uploadFile(); + } + + function showFileInfo(file) { + if (fileName && fileSize && fileInfo) { + fileName.textContent = file.name; + fileSize.textContent = `(${formatFileSize(file.size)})`; + fileInfo.classList.remove('d-none'); + } + } + + function uploadFile() { + const file = fileInput.files[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('noteFile', file); + + uploadBtn.disabled = true; + uploadProgress.classList.remove('d-none'); + uploadResult.innerHTML = ''; + + fetch('/upload', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(result => { + uploadProgress.classList.add('d-none'); + + if (result.success) { + if (result.processing) { + // Show processing status + uploadResult.innerHTML = ` +
+ + Upload Successful! ${result.message} +
+
+ Processing document chunks for AI analysis... +
+
+
+ This may take a few moments depending on document size. +
+
+
+ + + View Dashboard + +
+
+ `; + + // Start polling for status updates + startStatusPolling(result.fileInfo.id); + } else { + // Immediate success (shouldn't happen with new flow) + uploadResult.innerHTML = ` +
+ + Success! ${result.message} + +
+ `; + } + + // Reset form + fileInput.value = ''; + fileInfo.classList.add('d-none'); + uploadBtn.disabled = true; + + // Add success animation + uploadArea.classList.add('upload-success'); + setTimeout(() => { + uploadArea.classList.remove('upload-success'); + }, 2000); + } else { + uploadResult.innerHTML = ` +
+ + Error! ${result.error} + ${result.details ? `
${result.details}` : ''} +
+ `; + uploadArea.classList.add('upload-error'); + setTimeout(() => { + uploadArea.classList.remove('upload-error'); + }, 2000); + } + }) + .catch(error => { + uploadProgress.classList.add('d-none'); + uploadResult.innerHTML = ` +
+ + Error! ${error.message} +
+ `; + uploadArea.classList.add('upload-error'); + setTimeout(() => { + uploadArea.classList.remove('upload-error'); + }, 2000); + }) + .finally(() => { + uploadBtn.disabled = false; + }); + } +} + +function initializeDragAndDrop() { + const uploadArea = document.getElementById('upload-area'); + const fileInput = document.getElementById('noteFile'); + + if (!uploadArea || !fileInput) return; + + uploadArea.addEventListener('click', function(e) { + // Only trigger file input if clicking on the area itself, not the button + if (e.target === uploadArea || e.target.tagName === 'I' || e.target.tagName === 'H5' || e.target.tagName === 'P') { + fileInput.click(); + } + }); + + uploadArea.addEventListener('dragover', function(e) { + e.preventDefault(); + uploadArea.classList.add('dragover'); + }); + + uploadArea.addEventListener('dragleave', function(e) { + e.preventDefault(); + uploadArea.classList.remove('dragover'); + }); + + uploadArea.addEventListener('drop', function(e) { + e.preventDefault(); + uploadArea.classList.remove('dragover'); + + const files = e.dataTransfer.files; + if (files.length > 0) { + fileInput.files = files; + fileInput.dispatchEvent(new Event('change')); + } + }); +} + +function animateCards() { + const cards = document.querySelectorAll('.card'); + cards.forEach((card, index) => { + card.style.opacity = '0'; + card.style.transform = 'translateY(20px)'; + + setTimeout(() => { + card.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; + card.style.opacity = '1'; + card.style.transform = 'translateY(0)'; + }, index * 100); + }); +} + +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +// Utility functions +function showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.className = `toast align-items-center text-bg-${type} border-0`; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + + // Add to page + let toastContainer = document.querySelector('.toast-container'); + if (!toastContainer) { + toastContainer = document.createElement('div'); + toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3'; + document.body.appendChild(toastContainer); + } + + toastContainer.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); + + // Remove after hiding + toast.addEventListener('hidden.bs.toast', function() { + toast.remove(); + }); +} + +function showLoading(element) { + element.innerHTML = ` +
+
+ Loading... +
+ Loading... +
+ `; +} + +function hideLoading(element, originalContent) { + element.innerHTML = originalContent; +} + +// Error handling +window.addEventListener('error', function(e) { + console.error('Global error:', e.error); + showToast('An unexpected error occurred. Please try again.', 'danger'); +}); + +// Handle unhandled promise rejections +window.addEventListener('unhandledrejection', function(e) { + console.error('Unhandled promise rejection:', e.reason); + showToast('An unexpected error occurred. Please try again.', 'danger'); +}); + +// Add smooth scrolling for anchor links (but not for dropdown triggers) +document.querySelectorAll('a[href^="#"]').forEach(anchor => { + const href = anchor.getAttribute('href'); + // Skip empty anchors and dropdown triggers + if (href === '#' || anchor.hasAttribute('data-bs-toggle')) { + return; + } + + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const target = document.querySelector(href); + if (target) { + target.scrollIntoView({ + behavior: 'smooth' + }); + } + }); +}); + +// Add loading states to buttons (but not for login/register/quiz forms) +document.querySelectorAll('.btn').forEach(button => { + if (button.type === 'submit' || button.hasAttribute('data-loading')) { + // Skip login, register, and quiz form buttons + const form = button.closest('form'); + if (form && (form.action.includes('/login') || form.action.includes('/register') || form.id === 'quizForm')) { + return; + } + + button.addEventListener('click', function() { + if (!this.disabled) { + const originalText = this.innerHTML; + this.innerHTML = ` + + Loading... + `; + this.disabled = true; + + // Re-enable after 5 seconds (fallback) + setTimeout(() => { + this.innerHTML = originalText; + this.disabled = false; + }, 5000); + } + }); + } +}); + +// Status polling for document processing +let statusPollingInterval = null; + +function startStatusPolling(fileId) { + // Clear any existing polling + if (statusPollingInterval) { + clearInterval(statusPollingInterval); + } + + // Poll every 3 seconds + statusPollingInterval = setInterval(() => { + checkUploadStatus(fileId, true); + }, 3000); +} + +function stopStatusPolling() { + if (statusPollingInterval) { + clearInterval(statusPollingInterval); + statusPollingInterval = null; + } +} + +async function checkUploadStatus(fileId, isPolling = false) { + try { + const response = await fetch(`/api/files/${fileId}/status`); + const result = await response.json(); + + if (result.success) { + const file = result.file; + const uploadResult = document.getElementById('upload-result'); + + if (file.status === 'processed') { + // Processing completed successfully + if (uploadResult) { + uploadResult.innerHTML = ` +
+ + Processing Complete! Your document has been processed and is ready for AI analysis. +
+
+
+ + + ${file.processingResult ? `${file.processingResult.successfulChunks}/${file.processingResult.totalChunks} chunks processed` : 'Processed successfully'} + +
+
+ + + ${file.processingResult && file.processingResult.processedAt ? new Date(file.processingResult.processedAt).toLocaleTimeString() : 'Just now'} + +
+
+
+ +
+ `; + } + stopStatusPolling(); + + } else if (file.status === 'failed') { + // Processing failed + if (uploadResult) { + uploadResult.innerHTML = ` +
+ + Processing Failed! There was an error processing your document. + ${file.processingError ? `
Error: ${file.processingError}` : ''} +
+ + + View Dashboard + +
+
+ `; + } + stopStatusPolling(); + + } else if (file.status === 'processing' && !isPolling) { + // Still processing, manual check + let progressInfo = ''; + if (file.processingResult) { + progressInfo = `
Progress: ${file.processingResult.successfulChunks || 0}/${file.processingResult.totalChunks || '?'} chunks`; + } + + if (uploadResult) { + uploadResult.innerHTML = ` +
+ + Still Processing... Your document is being processed for AI analysis. + ${progressInfo} +
+ + + View Dashboard + +
+
+ `; + } + + // Start polling if not already polling + if (!statusPollingInterval) { + startStatusPolling(fileId); + } + } + + } else { + console.error('Error checking status:', result.error); + if (!isPolling) { + alert('Error checking status: ' + result.error); + } + } + } catch (error) { + console.error('Error checking upload status:', error); + if (!isPolling) { + alert('Error checking upload status: ' + error.message); + } + } +} + +async function retryDocumentProcessing(fileId) { + try { + const response = await fetch(`/api/files/${fileId}/retry`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + const result = await response.json(); + + if (result.success) { + // Update UI to show retry started + const uploadResult = document.getElementById('upload-result'); + if (uploadResult) { + uploadResult.innerHTML = ` +
+ + Retry Started! Processing your document again... +
+ + + View Dashboard + +
+
+ `; + } + + // Start polling again + startStatusPolling(fileId); + } else { + alert('Error retrying processing: ' + result.error); + } + } catch (error) { + console.error('Error retrying processing:', error); + alert('Error retrying processing: ' + error.message); + } +} + +// Clean up polling when page unloads +window.addEventListener('beforeunload', function() { + stopStatusPolling(); +}); diff --git a/server.js b/server.js new file mode 100644 index 0000000..f87576f --- /dev/null +++ b/server.js @@ -0,0 +1,1628 @@ +const express = require('express'); +const multer = require('multer'); +const axios = require('axios'); +const bodyParser = require('body-parser'); +const cors = require('cors'); +const session = require('express-session'); +const { v4: uuidv4 } = require('uuid'); +const fs = require('fs-extra'); +const path = require('path'); +const bcrypt = require('bcrypt'); +const flash = require('connect-flash'); +require('dotenv').config(); + +// Document processing utilities +const crypto = require('crypto'); + +// Document chunking configuration +const CHUNK_SIZE = 20000; // Larger chunks for FormData uploads (20KB) +const CHUNK_OVERLAP = 200; // Overlap between chunks +const MAX_DOCUMENT_SIZE = 1000000; // 1MB limit for document content + +// Flowise document store configuration +const FLOWISE_BASE_URL = 'https://flowise.suika.cc'; +const FLOWISE_DOCUMENT_STORE_ID = 'e293dc23-7cb2-4522-a057-4f0c4b0a444c'; +const FLOWISE_API_KEY = process.env.FLOWISE_API_KEY || null; + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(express.static('public')); +app.use(session({ + secret: process.env.SESSION_SECRET || 'educat-secret-key', + resave: false, + saveUninitialized: false, + cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } // 24 hours +})); +app.use(flash()); + +// Set EJS as view engine +app.set('view engine', 'ejs'); +app.set('views', './views'); + +// Ensure uploads directory exists +const uploadsDir = path.join(__dirname, 'uploads'); +fs.ensureDirSync(uploadsDir); + +// Simple user storage (in production, use a proper database) +const users = [ + { + id: 1, + username: 'admin', + email: 'admin@educat.com', + password: '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + name: 'Admin User' + }, + { + id: 2, + username: 'student', + email: 'student@educat.com', + password: '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + name: 'Student User' + } +]; + +// Initialize demo users with proper password hashing +async function initializeDemoUsers() { + try { + const hashedPassword = await bcrypt.hash('password', 10); + + users[0].password = hashedPassword; + users[1].password = hashedPassword; + } catch (error) { + console.error('Error initializing demo users:', error); + } +} + +// Initialize demo users on startup +initializeDemoUsers(); + +// Authentication middleware +const requireAuth = (req, res, next) => { + if (req.session.userId) { + next(); + } else { + req.flash('error', 'Please log in to access this page'); + res.redirect('/login'); + } +}; + +// Add user info to all templates +app.use((req, res, next) => { + res.locals.user = req.session.userId ? users.find(u => u.id === req.session.userId) : null; + res.locals.messages = req.flash(); + next(); +}); + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, 'uploads/'); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: 10 * 1024 * 1024 // 10MB limit + }, + fileFilter: (req, file, cb) => { + // Allow text files, PDFs, and images + const allowedTypes = /jpeg|jpg|png|gif|pdf|txt|doc|docx/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + + if (mimetype && extname) { + return cb(null, true); + } else { + cb(new Error('Only text files, PDFs, and images are allowed!')); + } + } +}); + +// Flowise API configuration +const FLOWISE_API_URL = process.env.FLOWISE_API_URL || 'https://flowise.suika.cc/api/v1/prediction'; +const FLOWISE_CHATFLOW_ID = process.env.FLOWISE_CHATFLOW_ID || 'your-chatflow-id'; + +// Helper function to chunk text into smaller pieces +function chunkText(text, chunkSize = CHUNK_SIZE, overlap = CHUNK_OVERLAP) { + const chunks = []; + let start = 0; + + while (start < text.length) { + const end = Math.min(start + chunkSize, text.length); + const chunk = text.substring(start, end); + + // Only add non-empty chunks + if (chunk.trim().length > 0) { + chunks.push({ + content: chunk.trim(), + metadata: { + chunkIndex: chunks.length, + startIndex: start, + endIndex: end, + totalLength: text.length + } + }); + } + + // Move start position, accounting for overlap + start = end - overlap; + + // Break if we're at the end + if (end >= text.length) break; + } + + return chunks; +} + +// Helper function to clean and validate document content +function validateAndCleanDocument(content, originalName) { + // Remove excessive whitespace and normalize line endings + let cleanContent = content + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/\n{3,}/g, '\n\n') + .replace(/[ \t]{2,}/g, ' ') + .trim(); + + // Check document size + if (cleanContent.length > MAX_DOCUMENT_SIZE) { + throw new Error(`Document is too large (${cleanContent.length} characters). Maximum size is ${MAX_DOCUMENT_SIZE} characters.`); + } + + // Check if document has meaningful content + if (cleanContent.length < 50) { + throw new Error('Document content is too short. Please upload a document with at least 50 characters.'); + } + + // Remove or replace problematic characters that might cause issues + cleanContent = cleanContent + .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control characters + .replace(/[^\x20-\x7E\n\t]/g, (char) => { // Replace non-printable characters + const code = char.charCodeAt(0); + return code > 127 ? char : ''; // Keep Unicode characters, remove other non-printable + }); + + return cleanContent; +} + +// Helper function to generate document hash for deduplication +function generateDocumentHash(content, originalName) { + return crypto.createHash('sha256') + .update(content + originalName) + .digest('hex') + .substring(0, 16); +} + +// Helper function to get document loaders from document store +async function getDocumentStoreLoaders(documentStoreId) { + try { + console.log(`Getting document store loaders for: ${documentStoreId}`); + + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + if (FLOWISE_API_KEY) { + headers['Authorization'] = `Bearer ${FLOWISE_API_KEY}`; + } + + // Get document store details which includes loaders + const storeUrl = `${FLOWISE_BASE_URL}/api/v1/document-store/store/${documentStoreId}`; + const response = await axios.get(storeUrl, { + headers, + timeout: 10000 + }); + + console.log('Document store details:', { + status: response.status, + id: response.data.id, + name: response.data.name, + loaders: response.data.loaders + }); + + // Parse loaders - they may come as array directly or as JSON string + let loaders = []; + if (response.data.loaders) { + if (Array.isArray(response.data.loaders)) { + // Loaders are already an array + loaders = response.data.loaders; + console.log('Loaders received as array:', loaders.length, 'loaders found'); + } else if (typeof response.data.loaders === 'string') { + // Loaders are a JSON string that needs parsing + try { + loaders = JSON.parse(response.data.loaders); + console.log('Loaders parsed from JSON string:', loaders.length, 'loaders found'); + } catch (parseError) { + console.log('Could not parse loaders JSON string, will create new one'); + loaders = []; + } + } else { + console.log('Loaders in unexpected format, will create new one'); + loaders = []; + } + } + + return { + store: response.data, + loaders: loaders + }; + } catch (error) { + console.error('Error getting document store loaders:', { + error: error.message, + response: error.response ? { + status: error.response.status, + data: error.response.data + } : 'No response' + }); + return null; + } +} + +// Helper function to upsert document to Flowise using FormData (direct file upload) +async function upsertDocumentToFlowiseFormData(fileInfo, documentMetadata) { + console.log('Starting Flowise Document Store upsert with FormData...'); + console.log(`Document: ${documentMetadata.originalName}`); + console.log(`Original file path: ${fileInfo.path}`); + console.log(`Document Store ID: ${FLOWISE_DOCUMENT_STORE_ID}`); + + try { + // Create FormData for file upload + const FormData = require('form-data'); + const formData = new FormData(); + + // Read the file and append to FormData + const filePath = path.isAbsolute(fileInfo.path) ? fileInfo.path : path.join(__dirname, fileInfo.path); + const fileStream = fs.createReadStream(filePath); + + // Try to get existing document store info to find existing loaders + const storeInfo = await getDocumentStoreLoaders(FLOWISE_DOCUMENT_STORE_ID); + let docId = null; + let useExistingLoader = false; + + if (storeInfo && storeInfo.loaders && storeInfo.loaders.length > 0) { + // Use the first existing loader ID, but only if replaceExisting is true + docId = storeInfo.loaders[0].id || storeInfo.loaders[0].loaderId; + useExistingLoader = true; + console.log(`Using existing document loader ID: ${docId}`); + console.log(`Existing loader details:`, { + id: docId, + loaderName: storeInfo.loaders[0].loaderName, + splitterName: storeInfo.loaders[0].splitterName, + totalChunks: storeInfo.loaders[0].totalChunks, + status: storeInfo.loaders[0].status + }); + } else { + // Create a new loader by letting Flowise auto-generate or create new store + console.log('No existing loaders found, will create new document entry'); + } + + // Append form fields following the Flowise API structure + formData.append('files', fileStream, { + filename: fileInfo.originalName, + contentType: fileInfo.mimetype + }); + + // Only append docId if we have an existing loader, otherwise let Flowise create new + if (useExistingLoader && docId) { + formData.append('docId', docId); + formData.append('replaceExisting', 'true'); + } else { + // For new documents, don't specify docId and create new store entry + formData.append('replaceExisting', 'false'); + formData.append('createNewDocStore', 'true'); + } + + formData.append('splitter', JSON.stringify({ + "config": { + "chunkSize": CHUNK_SIZE, + "chunkOverlap": CHUNK_OVERLAP + } + })); + + // Add metadata + const metadata = { + documentId: documentMetadata.documentId, + originalName: documentMetadata.originalName, + uploadDate: documentMetadata.uploadDate, + userId: documentMetadata.userId, + source: 'EduCat', + fileSize: fileInfo.size, + mimetype: fileInfo.mimetype + }; + formData.append('metadata', JSON.stringify(metadata)); + + // Don't duplicate the replaceExisting and createNewDocStore - they're set above + + // Prepare the upsert URL + const upsertUrl = `${FLOWISE_BASE_URL}/api/v1/document-store/upsert/${FLOWISE_DOCUMENT_STORE_ID}`; + console.log(`Upserting to: ${upsertUrl}`); + + // Prepare headers + const headers = { + ...formData.getHeaders() + }; + + if (FLOWISE_API_KEY) { + headers['Authorization'] = `Bearer ${FLOWISE_API_KEY}`; + } + + console.log('Sending FormData upsert request...'); + console.log('FormData fields:', { + docId: docId || 'auto-generated', + fileName: fileInfo.originalName, + fileSize: fileInfo.size, + replaceExisting: useExistingLoader, + createNewDocStore: !useExistingLoader, + chunkSize: CHUNK_SIZE, + useExistingLoader: useExistingLoader + }); + + // Make the request using axios with FormData + const response = await axios.post(upsertUrl, formData, { + headers, + timeout: 120000, // 2 minute timeout for large files + maxContentLength: Infinity, + maxBodyLength: Infinity + }); + + console.log('FormData upsert successful:', { + status: response.status, + statusText: response.statusText, + data: response.data + }); + + return { + success: true, + totalChunks: 1, // FormData uploads are treated as single operations + successfulChunks: 1, + failedChunks: 0, + results: [{ + chunkIndex: 0, + success: true, + response: response.data + }], + errors: [] + }; + + } catch (error) { + console.error('Flowise FormData upsert failed:', { + error: error.message, + response: error.response ? { + status: error.response.status, + statusText: error.response.statusText, + data: error.response.data + } : 'No response', + config: error.config ? { + url: error.config.url, + method: error.config.method + } : 'No config' + }); + + // Fall back to local storage if Flowise fails + console.log('Falling back to local storage...'); + + try { + const documentData = { + id: documentMetadata.documentId, + fileInfo: fileInfo, + metadata: documentMetadata, + storedAt: new Date().toISOString(), + fallbackReason: 'flowise_formdata_upsert_failed' + }; + + const documentsDir = path.join(__dirname, 'data', 'documents'); + await fs.ensureDir(documentsDir); + const documentPath = path.join(documentsDir, `${documentMetadata.documentId}.json`); + await fs.writeJSON(documentPath, documentData, { spaces: 2 }); + + console.log(`Document stored locally as fallback: ${documentPath}`); + + return { + success: true, + totalChunks: 1, + successfulChunks: 1, + failedChunks: 0, + results: [{ + chunkIndex: 0, + success: true, + response: { status: 'stored_locally', message: 'Fallback to local storage' } + }], + errors: [] + }; + + } catch (fallbackError) { + console.error('Local storage fallback also failed:', fallbackError); + return { + success: false, + totalChunks: 1, + successfulChunks: 0, + failedChunks: 1, + results: [], + errors: [{ + chunkIndex: 0, + error: `Flowise FormData upsert failed: ${error.message}. Local fallback failed: ${fallbackError.message}`, + chunk: 'Complete document' + }] + }; + } + } +} + +// Routes +app.get('/', (req, res) => { + res.render('index', { + title: 'EduCat - AI-Powered Note Revision', + messages: req.flash() + }); +}); + +// Authentication routes +app.get('/login', (req, res) => { + if (req.session.userId) { + return res.redirect('/dashboard'); + } + res.render('login', { + title: 'Login - EduCat', + messages: req.flash() + }); +}); + +app.post('/login', async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + req.flash('error', 'Please provide both username and password'); + return res.redirect('/login'); + } + + const user = users.find(u => u.username === username || u.email === username); + + if (!user) { + req.flash('error', 'Invalid username or password'); + return res.redirect('/login'); + } + + const isValidPassword = await bcrypt.compare(password, user.password); + + if (!isValidPassword) { + req.flash('error', 'Invalid username or password'); + return res.redirect('/login'); + } + + req.session.userId = user.id; + req.flash('success', `Welcome back, ${user.name}!`); + res.redirect('/dashboard'); + } catch (error) { + console.error('Login error:', error); + req.flash('error', 'An error occurred during login'); + res.redirect('/login'); + } +}); + +app.get('/register', (req, res) => { + if (req.session.userId) { + return res.redirect('/dashboard'); + } + res.render('register', { + title: 'Register - EduCat', + messages: req.flash() + }); +}); + +app.post('/register', async (req, res) => { + try { + const { username, email, password, confirmPassword, name } = req.body; + + if (!username || !email || !password || !confirmPassword || !name) { + req.flash('error', 'Please fill in all fields'); + return res.redirect('/register'); + } + + if (password !== confirmPassword) { + req.flash('error', 'Passwords do not match'); + return res.redirect('/register'); + } + + if (password.length < 6) { + req.flash('error', 'Password must be at least 6 characters long'); + return res.redirect('/register'); + } + + // Check if user already exists + const existingUser = users.find(u => u.username === username || u.email === email); + if (existingUser) { + req.flash('error', 'Username or email already exists'); + return res.redirect('/register'); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create new user + const newUser = { + id: users.length + 1, + username, + email, + password: hashedPassword, + name + }; + + users.push(newUser); + + req.session.userId = newUser.id; + req.flash('success', `Welcome to EduCat, ${newUser.name}!`); + res.redirect('/dashboard'); + } catch (error) { + console.error('Registration error:', error); + req.flash('error', 'An error occurred during registration'); + res.redirect('/register'); + } +}); + +app.get('/logout', (req, res) => { + req.session.destroy((err) => { + if (err) { + console.error('Logout error:', err); + } + res.redirect('/'); + }); +}); + +app.get('/upload', requireAuth, (req, res) => { + res.render('upload', { + title: 'Upload Your Notes - EduCat' + }); +}); + +app.post('/upload', requireAuth, upload.single('noteFile'), async (req, res) => { + try { + console.log('=== UPLOAD REQUEST START ==='); + console.log('Request file:', req.file); + console.log('Request body:', req.body); + console.log('Session userId:', req.session.userId); + + if (!req.file) { + console.log('ERROR: No file uploaded'); + return res.status(400).json({ + success: false, + error: 'No file uploaded' + }); + } + + const fileInfo = { + id: uuidv4(), + originalName: req.file.originalname, + filename: req.file.filename, + path: req.file.path, + size: req.file.size, + mimetype: req.file.mimetype, + uploadDate: new Date().toISOString(), + userId: req.session.userId, + status: 'processing' + }; + + console.log('Created fileInfo:', fileInfo); + + // Store initial file info in session + if (!req.session.uploadedFiles) { + req.session.uploadedFiles = []; + } + req.session.uploadedFiles.push(fileInfo); + + console.log('Total uploaded files in session:', req.session.uploadedFiles.length); + + // Send immediate response to prevent timeout + res.json({ + success: true, + message: 'File uploaded successfully! Processing document...', + fileInfo: fileInfo, + processing: true + }); + + console.log('Response sent, starting async processing...'); + + // Process document asynchronously + processDocumentAsync(fileInfo, req.session.userId); + + } catch (error) { + console.error('Upload error:', error); + res.status(500).json({ + success: false, + error: 'Upload failed', + details: error.message + }); + } +}); + +// Async function to process document and upsert to Flowise using FormData +async function processDocumentAsync(fileInfo, userId) { + try { + console.log('=== DOCUMENT PROCESSING START (FormData) ==='); + console.log(`Starting document processing for: ${fileInfo.originalName}`); + console.log(`File path: ${fileInfo.path}`); + console.log(`File size: ${fileInfo.size} bytes`); + console.log(`User ID: ${userId}`); + + // Create document metadata + const documentMetadata = { + documentId: fileInfo.id, + originalName: fileInfo.originalName, + uploadDate: fileInfo.uploadDate, + userId: userId, + fileSize: fileInfo.size, + fileInfo: fileInfo // Pass file info for progress tracking + }; + + console.log('Document metadata:', documentMetadata); + + // Initialize processing result + fileInfo.processingResult = { + totalChunks: 1, // FormData uploads are single operations + successfulChunks: 0, + failedChunks: 0, + startedAt: new Date().toISOString(), + status: 'uploading_to_flowise' + }; + + console.log('About to start FormData upsert to Flowise...'); + + // Upsert document to Flowise using FormData + const upsertResult = await upsertDocumentToFlowiseFormData(fileInfo, documentMetadata); + + console.log('FormData upsert result:', upsertResult); + + // Update file info with processing results + fileInfo.status = upsertResult.success ? 'processed' : 'failed'; + fileInfo.processingProgress = null; // Clear progress when done + fileInfo.processingResult = { + totalChunks: upsertResult.totalChunks, + successfulChunks: upsertResult.successfulChunks, + failedChunks: upsertResult.failedChunks, + processedAt: new Date().toISOString(), + startedAt: fileInfo.processingResult.startedAt, + duration: Date.now() - new Date(fileInfo.processingResult.startedAt).getTime(), + method: 'formdata' + }; + + if (upsertResult.errors.length > 0) { + fileInfo.processingErrors = upsertResult.errors; + console.log(`Processing errors for ${fileInfo.originalName}:`, upsertResult.errors); + } + + console.log(`Document processing completed for: ${fileInfo.originalName}`); + console.log(`Result: FormData upload ${upsertResult.success ? 'successful' : 'failed'}`); + + // Log final upsert verification + console.log('Final FormData upsert verification:', { + documentStore: `${FLOWISE_BASE_URL}/api/v1/document-store/${FLOWISE_DOCUMENT_STORE_ID}`, + documentId: fileInfo.id, + fileName: fileInfo.originalName, + success: upsertResult.success + }); + + } catch (error) { + console.error(`Error processing document ${fileInfo.originalName}:`, error); + fileInfo.status = 'failed'; + fileInfo.processingError = error.message; + fileInfo.processingResult = fileInfo.processingResult || {}; + fileInfo.processingResult.failedAt = new Date().toISOString(); + } +} + +app.get('/revise/:fileId', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const files = req.session.uploadedFiles || []; + const file = files.find(f => f.id === fileId); + + if (!file) { + return res.status(404).render('error', { + title: 'File Not Found', + error: 'File not found' + }); + } + + // Read file content + const fileContent = await fs.readFile(file.path, 'utf-8'); + + res.render('revise', { + title: 'Revise Notes - EduCat', + file: file, + content: fileContent + }); + } catch (error) { + console.error('Error loading file:', error); + res.status(500).render('error', { + title: 'Error', + error: 'Failed to load file' + }); + } +}); + +app.post('/api/revise', requireAuth, async (req, res) => { + try { + const { content, revisionType, fileId } = req.body; + + let prompt = ''; + let contextContent = content; + + // If fileId is provided, try to get additional context from stored document + if (fileId) { + try { + const documentPath = path.join(__dirname, 'data', 'documents', `${fileId}.json`); + if (await fs.pathExists(documentPath)) { + const documentData = await fs.readJSON(documentPath); + contextContent = documentData.content; + console.log(`Using stored document content for revision: ${documentData.metadata.originalName}`); + } + } catch (error) { + console.log('Could not load stored document, using provided content'); + } + } + + switch (revisionType) { + case 'summarize': + prompt = `Please summarize the following notes in a clear and concise manner:\n\n${contextContent}`; + break; + case 'improve': + prompt = `Please improve and enhance the following notes, making them more comprehensive and well-structured:\n\n${contextContent}`; + break; + case 'questions': + prompt = `Based on the following notes, generate study questions that would help test understanding:\n\n${contextContent}`; + break; + default: + prompt = `Please help improve these notes:\n\n${contextContent}`; + } + + // Call Flowise API + const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { + question: prompt, + history: [] + }); + + res.json({ + success: true, + revisedContent: response.data.text || response.data.answer || 'No response received' + }); + } catch (error) { + console.error('Revision error:', error); + res.status(500).json({ + error: 'Failed to revise notes', + details: error.message + }); + } +}); + +app.get('/chat', requireAuth, (req, res) => { + res.render('chat', { + title: 'Chat with EduCat AI' + }); +}); + +app.post('/api/chat', requireAuth, async (req, res) => { + try { + const { message, history } = req.body; + + // Call Flowise API for chat + const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { + question: message, + history: history || [] + }); + + res.json({ + success: true, + response: response.data.text || response.data.answer || 'No response received' + }); + } catch (error) { + console.error('Chat error:', error); + res.status(500).json({ + error: 'Failed to get chat response', + details: error.message + }); + } +}); + +app.get('/dashboard', requireAuth, (req, res) => { + const files = req.session.uploadedFiles || []; + res.render('dashboard', { + title: 'Dashboard - EduCat', + files: files + }); +}); + +// File management endpoints +app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const files = req.session.uploadedFiles || []; + const file = files.find(f => f.id === fileId); + + if (!file) { + 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'); + + res.json({ + success: true, + file: { + id: file.id, + originalName: file.originalName, + size: file.size, + uploadDate: file.uploadDate, + content: fileContent + } + }); + } catch (error) { + console.error('Error previewing file:', error); + res.status(500).json({ + success: false, + error: 'Failed to preview file', + details: error.message + }); + } +}); + +app.delete('/api/files/:fileId', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const files = req.session.uploadedFiles || []; + const fileIndex = files.findIndex(f => f.id === fileId); + + if (fileIndex === -1) { + return res.status(404).json({ success: false, error: 'File not found' }); + } + + const file = files[fileIndex]; + + // Delete the physical file + const filePath = path.join(__dirname, file.path); + if (await fs.pathExists(filePath)) { + await fs.unlink(filePath); + } + + // Remove from session + req.session.uploadedFiles.splice(fileIndex, 1); + + console.log(`File deleted: ${file.originalName} (ID: ${fileId})`); + + res.json({ + success: true, + message: 'File deleted successfully' + }); + } catch (error) { + console.error('Error deleting file:', error); + res.status(500).json({ + success: false, + error: 'Failed to delete file', + details: error.message + }); + } +}); + +// File processing status endpoint +app.get('/api/files/:fileId/status', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const files = req.session.uploadedFiles || []; + const file = files.find(f => f.id === fileId); + + if (!file) { + return res.status(404).json({ success: false, error: 'File not found' }); + } + + res.json({ + success: true, + file: { + id: file.id, + originalName: file.originalName, + status: file.status, + uploadDate: file.uploadDate, + processingResult: file.processingResult, + processingError: file.processingError, + processingErrors: file.processingErrors + } + }); + } catch (error) { + console.error('Error getting file status:', error); + res.status(500).json({ + success: false, + error: 'Failed to get file status', + details: error.message + }); + } +}); + +// Retry processing endpoint +app.post('/api/files/:fileId/retry', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const files = req.session.uploadedFiles || []; + const file = files.find(f => f.id === fileId); + + if (!file) { + return res.status(404).json({ success: false, error: 'File not found' }); + } + + if (file.status === 'processing') { + return res.status(400).json({ success: false, error: 'File is already being processed' }); + } + + // Reset file status for retry + file.status = 'processing'; + file.processingResult = null; + file.processingError = null; + file.processingErrors = null; + + // Start processing asynchronously + processDocumentAsync(file, req.session.userId); + + res.json({ + success: true, + message: 'Processing retry initiated' + }); + } catch (error) { + console.error('Error retrying processing:', error); + res.status(500).json({ + success: false, + error: 'Failed to retry processing', + details: error.message + }); + } +}); + +// Bulk processing status endpoint +app.get('/api/files/status/all', requireAuth, async (req, res) => { + try { + const files = req.session.uploadedFiles || []; + const statusSummary = files.map(file => ({ + id: file.id, + originalName: file.originalName, + status: file.status, + uploadDate: file.uploadDate, + processingResult: file.processingResult ? { + totalChunks: file.processingResult.totalChunks, + successfulChunks: file.processingResult.successfulChunks, + failedChunks: file.processingResult.failedChunks, + processedAt: file.processingResult.processedAt + } : null + })); + + const summary = { + totalFiles: files.length, + processing: files.filter(f => f.status === 'processing').length, + processed: files.filter(f => f.status === 'processed').length, + failed: files.filter(f => f.status === 'failed').length, + files: statusSummary + }; + + res.json({ + success: true, + summary: summary + }); + } catch (error) { + console.error('Error getting files status:', error); + res.status(500).json({ + success: false, + error: 'Failed to get files status', + details: error.message + }); + } +}); + +app.get('/quiz', requireAuth, (req, res) => { + res.render('quiz', { + title: 'AI Quiz Generator - EduCat' + }); +}); + +app.post('/api/generate-quiz', requireAuth, async (req, res) => { + console.log('=== QUIZ API REQUEST RECEIVED ==='); + console.log('Request body:', req.body); + console.log('User ID:', req.session.userId); + console.log('================================='); + + try { + const { topic, difficulty, questionCount, quizType } = req.body; + + console.log('Quiz request:', { topic, difficulty, questionCount, quizType }); + + let prompt = ''; + switch (quizType) { + case 'multiple-choice': + prompt = `Generate exactly ${questionCount} multiple choice questions about "${topic}" at ${difficulty} difficulty level. + Each question should have 4 options (A, B, C, D). + Format the response as a JSON array where each question follows this exact structure: + [ + { + "question": "What is the capital of France?", + "options": ["A) London", "B) Paris", "C) Berlin", "D) Madrid"], + "correct": "B", + "explanation": "Paris is the capital and largest city of France." + } + ] + Return ONLY the JSON array, no additional text.`; + break; + case 'true-false': + prompt = `Generate exactly ${questionCount} true/false questions about "${topic}" at ${difficulty} difficulty level. + Format the response as a JSON array where each question follows this exact structure: + [ + { + "question": "The Earth is flat.", + "correct": "False", + "explanation": "The Earth is spherical, not flat." + } + ] + Return ONLY the JSON array, no additional text.`; + break; + case 'short-answer': + prompt = `Generate exactly ${questionCount} short answer questions about "${topic}" at ${difficulty} difficulty level. + Format the response as a JSON array where each question follows this exact structure: + [ + { + "question": "What is the process by which plants make their own food?", + "answer": "Photosynthesis", + "keywords": ["photosynthesis", "sunlight", "chlorophyll"] + } + ] + Return ONLY the JSON array, no additional text.`; + break; + default: + prompt = `Generate exactly ${questionCount} multiple choice questions about "${topic}" at ${difficulty} difficulty level.`; + } + + console.log('Sending prompt to Flowise:', prompt.substring(0, 100) + '...'); + + // Call Flowise API + const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, { + question: prompt, + history: [] + }); + + console.log('Flowise response received:', { + status: response.status, + dataType: typeof response.data, + dataPreview: JSON.stringify(response.data).substring(0, 200) + '...' + }); + + let quizData; + try { + const responseText = response.data.text || response.data.answer || response.data; + console.log('Raw response text preview:', responseText.substring(0, 500) + '...'); + + // 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]; + console.log('Found JSON in code block'); + } 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); + console.log('Found JSON by bracket counting'); + } + } + } + + if (jsonString) { + console.log('Parsing JSON string:', jsonString.substring(0, 200) + '...'); + quizData = JSON.parse(jsonString); + console.log('Successfully parsed quiz data, questions:', quizData.length); + } else { + console.log('No JSON found, using fallback quiz'); + quizData = generateFallbackQuiz(topic, questionCount, quizType); + } + } catch (parseError) { + console.error('Quiz parsing error:', parseError); + console.log('Using fallback quiz due to parsing error'); + quizData = generateFallbackQuiz(topic, questionCount, quizType); + } + + console.log('Final quiz data:', { + questionsCount: quizData.length, + firstQuestion: quizData[0] + }); + + res.json({ + success: true, + quiz: quizData, + topic: topic, + difficulty: difficulty, + questionCount: questionCount, + quizType: quizType + }); + } catch (error) { + 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'); + + console.log('Returning fallback quiz due to error'); + 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' + }); + } +}); + +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; + + let score = 0; + const results = []; + + quiz.forEach((question, index) => { + const userAnswer = answers[index]; + const isCorrect = userAnswer === question.correct; + if (isCorrect) score++; + + results.push({ + question: question.question, + userAnswer: userAnswer, + correctAnswer: question.correct, + isCorrect: isCorrect, + explanation: question.explanation || '', + options: question.options || null // Include options for multiple choice + }); + }); + + const percentage = Math.round((score / quiz.length) * 100); + + // Store quiz result in session + if (!req.session.quizResults) { + req.session.quizResults = []; + } + + const quizResult = { + id: uuidv4(), + topic: topic, + score: score, + total: quiz.length, + percentage: percentage, + date: new Date().toISOString(), + results: results, + userId: req.session.userId, + difficulty: difficulty || 'beginner', + quizType: quizType || 'multiple-choice' + }; + + // Store quiz result in session (for immediate use) + req.session.quizResults.push(quizResult); + + // Store quiz result persistently + await addQuizResult(req.session.userId, quizResult); + + console.log(`Quiz result saved for user ${req.session.userId}: ${score}/${quiz.length} (${percentage}%)`); + + res.json({ + success: true, + score: score, + total: quiz.length, + percentage: percentage, + results: results, + quizId: quizResult.id + }); + } catch (error) { + console.error('Quiz submission error:', error); + res.status(500).json({ + error: 'Failed to submit quiz', + details: error.message + }); + } +}); + +// Quiz results persistence +const QUIZ_RESULTS_FILE = path.join(__dirname, 'data', 'quiz-results.json'); + +// Ensure data directory exists +async function ensureDataDirectory() { + await fs.ensureDir(path.join(__dirname, 'data')); +} + +// Load quiz results from file +async function loadQuizResults() { + try { + await ensureDataDirectory(); + if (await fs.pathExists(QUIZ_RESULTS_FILE)) { + const data = await fs.readJSON(QUIZ_RESULTS_FILE); + return data || {}; + } + 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 +async function getUserQuizResults(userId) { + const allResults = await loadQuizResults(); + return allResults[userId] || []; +} + +// Error handling middleware +app.use((error, req, res, next) => { + if (error instanceof multer.MulterError) { + if (error.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ error: 'File too large. Maximum size is 10MB.' }); + } + } + + console.error('Error:', error); + res.status(500).json({ error: 'Internal server error' }); +}); + +// Quiz history route +app.get('/quiz-history', requireAuth, async (req, res) => { + try { + const quizResults = await getUserQuizResults(req.session.userId); + res.render('quiz-history', { + title: 'Quiz History - EduCat', + quizResults: quizResults.reverse() // Show newest first + }); + } catch (error) { + console.error('Error loading quiz history:', error); + res.render('quiz-history', { + title: 'Quiz History - EduCat', + quizResults: [] + }); + } +}); + +// API route to clear quiz history +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); + + console.log(`Quiz history cleared for user ${userId}`); + + res.json({ + success: true, + message: 'Quiz history cleared successfully' + }); + } catch (error) { + console.error('Error clearing quiz history:', error); + res.status(500).json({ + success: false, + error: 'Failed to clear quiz history' + }); + } +}); + +// API route to get quiz statistics +app.get('/api/quiz-stats', requireAuth, async (req, res) => { + try { + const quizResults = await getUserQuizResults(req.session.userId); + + if (quizResults.length === 0) { + return res.json({ + success: true, + stats: { + totalQuizzes: 0, + averageScore: 0, + bestScore: 0, + recentQuizzes: [], + topicStats: {}, + progressChart: [] + } + }); + } + + // Calculate statistics + const totalQuizzes = quizResults.length; + const averageScore = Math.round(quizResults.reduce((sum, quiz) => sum + quiz.percentage, 0) / totalQuizzes); + const bestScore = Math.max(...quizResults.map(quiz => quiz.percentage)); + const recentQuizzes = quizResults.slice(-5).reverse(); // Last 5 quizzes + + // Topic statistics + const topicStats = {}; + quizResults.forEach(quiz => { + if (!topicStats[quiz.topic]) { + topicStats[quiz.topic] = { + count: 0, + totalScore: 0, + bestScore: 0 + }; + } + topicStats[quiz.topic].count++; + topicStats[quiz.topic].totalScore += quiz.percentage; + topicStats[quiz.topic].bestScore = Math.max(topicStats[quiz.topic].bestScore, quiz.percentage); + }); + + // Calculate average for each topic + Object.keys(topicStats).forEach(topic => { + topicStats[topic].averageScore = Math.round(topicStats[topic].totalScore / topicStats[topic].count); + }); + + // Progress chart data (last 10 quizzes) + const progressChart = quizResults.slice(-10).map((quiz, index) => ({ + quiz: index + 1, + score: quiz.percentage, + topic: quiz.topic, + date: quiz.date + })); + + res.json({ + success: true, + stats: { + totalQuizzes, + averageScore, + bestScore, + recentQuizzes, + topicStats, + progressChart + } + }); + } catch (error) { + console.error('Error getting quiz stats:', error); + res.status(500).json({ success: false, error: 'Failed to get quiz statistics' }); + } +}); + +// API route to get detailed quiz results by ID +app.get('/api/quiz-details/:quizId', requireAuth, async (req, res) => { + try { + const quizId = req.params.quizId; + const quizResults = await getUserQuizResults(req.session.userId); + + const quiz = quizResults.find(q => q.id === quizId); + + if (!quiz) { + return res.status(404).json({ success: false, error: 'Quiz not found' }); + } + + res.json({ + success: true, + quiz: quiz + }); + } catch (error) { + console.error('Error getting quiz details:', error); + res.status(500).json({ success: false, error: 'Failed to get quiz details' }); + } +}); + +// File progress tracking endpoint +app.get('/api/files/:fileId/progress', requireAuth, async (req, res) => { + try { + const fileId = req.params.fileId; + const files = req.session.uploadedFiles || []; + const file = files.find(f => f.id === fileId); + + if (!file) { + return res.status(404).json({ success: false, error: 'File not found' }); + } + + res.json({ + success: true, + progress: { + status: file.status, + processingProgress: file.processingProgress || null, + processingResult: file.processingResult || null, + processingError: file.processingError || null + } + }); + } catch (error) { + console.error('Error getting file progress:', error); + res.status(500).json({ + success: false, + error: 'Failed to get file progress', + details: error.message + }); + } +}); + +// 404 handler +app.use((req, res) => { + res.status(404).render('error', { + title: 'Page Not Found', + error: 'The page you are looking for does not exist.' + }); +}); + +app.listen(PORT, () => { + console.log(`EduCat server running on http://localhost:${PORT}`); + console.log(`Flowise API URL: ${FLOWISE_API_URL}`); + console.log(`Flowise Chatflow ID: ${FLOWISE_CHATFLOW_ID}`); + console.log(`Flowise Document Store ID: ${FLOWISE_DOCUMENT_STORE_ID}`); +}); + + + + diff --git a/views/chat.ejs b/views/chat.ejs new file mode 100644 index 0000000..dfbc85c --- /dev/null +++ b/views/chat.ejs @@ -0,0 +1,196 @@ +<%- include('partials/header') %> + +
+
+
+
+
+

Chat with EduCat AI

+
+
+
+
+
+
+ +
+
+
+

Hello! I'm EduCat AI, your study assistant. I can help you with questions about your notes, study techniques, and academic topics. How can I assist you today?

+
+ Just now +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ +
Ask Questions
+

Get answers about your study materials

+
+
+
+
+
+
+ +
Study Help
+

Get study tips and techniques

+
+
+
+
+
+
+
+ + + +<%- include('partials/footer') %> diff --git a/views/dashboard.ejs b/views/dashboard.ejs new file mode 100644 index 0000000..fe8eea1 --- /dev/null +++ b/views/dashboard.ejs @@ -0,0 +1,671 @@ +<%- include('partials/header') %> + +
+
+
+
+

Your Dashboard

+ + Upload New Notes + +
+ + <% if (files.length === 0) { %> +
+ +

No files uploaded yet

+

Upload your first set of notes to get started with AI-powered revision.

+ + Upload Notes + +
+ <% } else { %> +
+ <% files.forEach(function(file, index) { %> +
+
+
+
+
+ +
+
+
<%= file.originalName %>
+ <%= Math.round(file.size / 1024) %> KB +
+
+ <% if (file.status === 'processing') { %> + + Processing + + <% } else if (file.status === 'processed') { %> + + Processed + + <% } else if (file.status === 'failed') { %> + + Failed + + <% } else { %> + + Uploaded + + <% } %> +
+
+ +
+ + + Uploaded: <%= new Date(file.uploadDate).toLocaleDateString() %> + + <% if (file.processingResult) { %> +
+ + Chunks: <%= file.processingResult.successfulChunks %>/<%= file.processingResult.totalChunks %> + + <% } %> +
+ +
+ <% if (file.status === 'processed') { %> + + Revise with AI + + <% } else if (file.status === 'processing') { %> + + <% } else if (file.status === 'failed') { %> + + <% } else { %> + + <% } %> + +
+ + + +
+
+
+
+
+ <% }); %> +
+ <% } %> +
+
+
+ + + + + + + + + +<%- include('partials/footer') %> diff --git a/views/error.ejs b/views/error.ejs new file mode 100644 index 0000000..c279fc6 --- /dev/null +++ b/views/error.ejs @@ -0,0 +1,20 @@ +<%- include('partials/header') %> + +
+
+
+
+
+ +

Oops! Something went wrong

+

<%= error %>

+ + Go Home + +
+
+
+
+
+ +<%- include('partials/footer') %> diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..e09a396 --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,224 @@ +<%- include('partials/header') %> + + +<% if (messages.error) { %> + +<% } %> + +<% if (messages.success) { %> + +<% } %> + +
+
+
+
+ <% if (user) { %> +

Welcome back, <%= user.name %>!

+

Ready to enhance your study experience? Upload notes, generate AI quizzes, get personalized revision, and chat with our intelligent assistant.

+ + <% } else { %> +

Welcome to EduCat

+

Transform your study experience with AI-powered note revision, personalized quizzes, and intelligent tutoring. Upload your notes and let our smart system create the perfect study environment for you.

+ + <% } %> +
+
+ EduCat +
+
+
+
+ +
+
+
+
+
+
+ +
+
Upload Your Notes
+

Simply upload your study notes in various formats (PDF, DOC, TXT) and let our AI analyze them.

+ <% if (user) { %> + Get Started + <% } else { %> + Get Started + <% } %> +
+
+
+
+
+
+
+ +
+
AI-Powered Revision
+

Our AI will summarize, improve, and generate study questions from your notes automatically.

+ <% if (user) { %> + View Dashboard + <% } else { %> + View Dashboard + <% } %> +
+
+
+
+
+
+
+ +
+
AI Quiz Generator
+

Generate personalized quizzes on any topic with multiple question types and difficulty levels.

+ <% if (user) { %> + Take Quiz + <% } else { %> + Take Quiz + <% } %> +
+
+
+
+
+
+
+ +
+
Interactive Chat
+

Ask questions about your notes and get instant, intelligent responses from our AI assistant.

+ <% if (user) { %> + Start Chatting + <% } else { %> + Start Chatting + <% } %> +
+
+
+
+
+ + +
+
+
+
+

Test Your Knowledge

+

Generate personalized quizzes on any topic with our AI-powered quiz generator. Choose from multiple question types and difficulty levels to challenge yourself.

+
+
+ + Multiple choice questions +
+
+ + True/False questions +
+
+ + Instant feedback and scoring +
+
+ + Customizable difficulty levels +
+
+ <% if (user) { %> + + Start Quiz + + <% } else { %> + + Login to Start + + <% } %> +
+
+
+
Sample Quiz Question
+
+

What is the capital of France?

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ Correct! Great job. +
+
+
+
+
+
+ +
+
+
+
+

How EduCat Works

+
+
+
1
+
Upload
+

Upload your study notes in any supported format

+
+
+
2
+
AI Analysis
+

Our AI analyzes and processes your content

+
+
+
3
+
Take Quizzes
+

Generate personalized quizzes to test your knowledge

+
+
+
4
+
Get Results
+

Receive improved notes, summaries, and instant feedback

+
+
+
+
+
+
+ +<%- include('partials/footer') %> diff --git a/views/layout.ejs b/views/layout.ejs new file mode 100644 index 0000000..eb1c198 --- /dev/null +++ b/views/layout.ejs @@ -0,0 +1,61 @@ + + + + + + <%= title %> + + + + + + + +
+ <%- body %> +
+ +
+
+
+
+
EduCat
+

AI-powered note revision platform for students

+
+
+

© 2025 EduCat. All rights reserved.

+
+
+
+
+ + + + + diff --git a/views/login.ejs b/views/login.ejs new file mode 100644 index 0000000..593f1bc --- /dev/null +++ b/views/login.ejs @@ -0,0 +1,151 @@ + + + + + + <%= title %> + + + + + + + +
+
+
+
+
+
+ EduCat Logo +

Welcome Back!

+

Sign in to your EduCat account

+
+ + + <% if (messages.error) { %> + + <% } %> + + <% if (messages.success) { %> + + <% } %> + +
+
+ +
+ + + + +
+
+ +
+ +
+ + + + + +
+
+ +
+ +
+
+ +
+

Don't have an account? + Sign up here +

+
+
+
+ +
+
+
Demo Accounts
+
+
+ + Admin:
+ Username: admin
+ Password: password +
+
+
+ + Student:
+ Username: student
+ Password: password +
+
+
+
+
+
+
+
+ + + + + + diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs new file mode 100644 index 0000000..57ed1cd --- /dev/null +++ b/views/partials/footer.ejs @@ -0,0 +1,18 @@ +
+
+
+
+
EduCat
+

AI-powered note revision platform for students

+
+
+

© 2025 EduCat. All rights reserved.

+
+
+
+
+ + + + + diff --git a/views/partials/header.ejs b/views/partials/header.ejs new file mode 100644 index 0000000..8563293 --- /dev/null +++ b/views/partials/header.ejs @@ -0,0 +1,64 @@ + + + + + + <%= title %> + + + + + + diff --git a/views/quiz-history.ejs b/views/quiz-history.ejs new file mode 100644 index 0000000..d2247cf --- /dev/null +++ b/views/quiz-history.ejs @@ -0,0 +1,549 @@ +<%- include('partials/header') %> + +
+
+
+

Quiz History

+
+
+ + +
+
+
+
+ +

-

+

Total Quizzes

+
+
+
+
+
+
+ +

-%

+

Average Score

+
+
+
+
+
+
+ +

-%

+

Best Score

+
+
+
+
+
+
+ +

-

+

Favorite Topic

+
+
+
+
+ + +
+
+
+
+
Progress Over Time
+
+
+ +
+
+
+
+ + +
+
+
+
+
Performance by Topic
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
All Quiz Results
+ <% if (quizResults.length > 0) { %> + + <% } %> +
+
+ <% if (quizResults.length === 0) { %> +
+ +

No Quiz History Yet

+

Take your first quiz to see your results here!

+ + Take Your First Quiz + +
+ <% } else { %> +
+ + + + + + + + + + + + + + + <% quizResults.forEach(function(quiz) { %> + <% + let grade = 'F'; + let badgeClass = 'bg-danger'; + if (quiz.percentage >= 90) { grade = 'A'; badgeClass = 'bg-success'; } + else if (quiz.percentage >= 80) { grade = 'B'; badgeClass = 'bg-info'; } + else if (quiz.percentage >= 70) { grade = 'C'; badgeClass = 'bg-warning'; } + else if (quiz.percentage >= 60) { grade = 'D'; badgeClass = 'bg-warning'; } + %> + + + + + + + + + + + <% }); %> + +
DateTopicDifficultyTypeScorePercentageGradeActions
<%= new Date(quiz.date).toLocaleDateString() %> + <%= quiz.topic %> + + <%= quiz.difficulty || 'unknown' %> + <%= quiz.quizType || 'multiple-choice' %><%= quiz.score %>/<%= quiz.total %> +
+
+ <%= quiz.percentage %>% +
+
+
+ <%= grade %> + + +
+
+ <% } %> +
+
+
+
+
+ + + + + + + +<%- include('partials/footer') %> diff --git a/views/quiz.ejs b/views/quiz.ejs new file mode 100644 index 0000000..4ee6392 --- /dev/null +++ b/views/quiz.ejs @@ -0,0 +1,642 @@ +<%- include('partials/header') %> + +
+
+
+

AI Quiz Generator

+
+
+ + +
+
+
+
+

Generate New Quiz

+
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+ +
+
+
+
How It Works
+
    +
  • Choose your topic and difficulty
  • +
  • Select question count and type
  • +
  • AI generates personalized quiz
  • +
  • Take the quiz and get instant feedback
  • +
+
+
+ +
+
+
Tips
+
    +
  • Be specific with your topic for better questions
  • +
  • Start with beginner if you're new to the subject
  • +
  • Multiple choice is great for concept testing
  • +
  • True/False is perfect for fact checking
  • +
+
+
+
+
+ + +
+
+ Loading... +
+

Generating Your Quiz...

+

Our AI is creating personalized questions for you

+
+ + +
+
+
+
+
+

Quiz

+
+ Question 1 of 10 +
+
+
+
+ +
+ + + +
+
+
+
+ +
+
+
+
Quiz Overview
+
+
+
+
+
+
+ + +
+
+
+
+
+

Quiz Results

+
+
+
+
+ +
+ + + View History + + +
+
+
+
+ +
+
+
+
Performance
+
+
+
+
+
+
+
+ + + +<%- include('partials/footer') %> diff --git a/views/register.ejs b/views/register.ejs new file mode 100644 index 0000000..b56d7d3 --- /dev/null +++ b/views/register.ejs @@ -0,0 +1,194 @@ + + + + + + <%= title %> + + + + + + + +
+
+
+
+
+
+ EduCat Logo +

Join EduCat!

+

Create your account to get started

+
+ + + <% if (messages.error) { %> + + <% } %> + + <% if (messages.success) { %> + + <% } %> + +
+
+ +
+ + + + +
+
+ +
+ +
+ + + + +
+
+ +
+ +
+ + + + +
+
+ +
+ +
+ + + + + +
+ Password must be at least 6 characters long +
+ +
+ +
+ + + + + +
+
+ +
+ +
+
+ +
+

Already have an account? + Sign in here +

+
+
+
+
+
+
+ + + + + + diff --git a/views/revise.ejs b/views/revise.ejs new file mode 100644 index 0000000..b0d16d7 --- /dev/null +++ b/views/revise.ejs @@ -0,0 +1,149 @@ +<%- include('partials/header') %> + +
+
+
+
+
+

Revise: <%= file.originalName %>

+
+
+
+
+
Original Notes
+
+
<%= content %>
+
+
+
+
AI-Revised Notes
+
+

Select a revision type and click "Revise" to see AI-enhanced notes here.

+
+
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+
+
+
+

AI is processing your notes...

+
+
+
+
+
+
+
+
File Information
+
    +
  • Name: <%= file.originalName %>
  • +
  • Size: <%= Math.round(file.size / 1024) %> KB
  • +
  • Uploaded: <%= new Date(file.uploadDate).toLocaleDateString() %>
  • +
+
+
+ +
+
+
Revision Tips
+
    +
  • Improve & Enhance: Makes your notes more comprehensive and well-structured
  • +
  • Summarize: Creates concise summaries of your key points
  • +
  • Generate Questions: Creates study questions to test your understanding
  • +
+
+
+ +
+
+ + +
+
+
+
+
+ + + +<%- include('partials/footer') %> diff --git a/views/upload.ejs b/views/upload.ejs new file mode 100644 index 0000000..14047d5 --- /dev/null +++ b/views/upload.ejs @@ -0,0 +1,75 @@ +<%- include('partials/header') %> + +
+
+
+
+
+

Upload Your Notes

+
+
+

Upload your study notes and let our AI help you create better, more comprehensive study materials.

+ +
+ +
Drag & Drop your files here
+

or click to browse

+ + +
+ +
+
+ + + +
+
+ +
+ +
+ +
+
+
+
+

Processing your notes...

+
+ +
+
+
+ +
+
+
+ +
PDF Files
+

Upload PDF documents

+
+
+
+
+ +
Word Documents
+

DOC & DOCX files

+
+
+
+
+ +
Text Files
+

Plain text documents

+
+
+
+
+
+
+ +<%- include('partials/footer') %>