├── client ├── README.md ├── src │ ├── assets │ │ └── .gitkeep │ ├── app │ │ ├── app.component.css │ │ ├── models │ │ │ ├── navLinks.ts │ │ │ ├── auth.spec.ts │ │ │ ├── research.spec.ts │ │ │ ├── auth.ts │ │ │ └── research.ts │ │ ├── pages │ │ │ ├── welcome │ │ │ │ ├── welcome.component.css │ │ │ │ ├── welcome.component.html │ │ │ │ ├── welcome.routes.ts │ │ │ │ └── welcome.component.ts │ │ │ ├── dashboard │ │ │ │ ├── profile │ │ │ │ │ ├── profile.component.css │ │ │ │ │ ├── profile.component.html │ │ │ │ │ ├── profile.component.ts │ │ │ │ │ └── profile.component.spec.ts │ │ │ │ ├── research │ │ │ │ │ ├── home │ │ │ │ │ │ ├── home.component.css │ │ │ │ │ │ ├── home.component.html │ │ │ │ │ │ ├── home.component.spec.ts │ │ │ │ │ │ └── home.component.ts │ │ │ │ │ ├── research.component.css │ │ │ │ │ ├── research.component.html │ │ │ │ │ ├── research.component.ts │ │ │ │ │ ├── details │ │ │ │ │ │ ├── details.component.spec.ts │ │ │ │ │ │ ├── details.component.css │ │ │ │ │ │ ├── details.component.html │ │ │ │ │ │ └── details.component.ts │ │ │ │ │ └── research.component.spec.ts │ │ │ │ ├── settings │ │ │ │ │ ├── settings.component.css │ │ │ │ │ ├── settings.component.html │ │ │ │ │ ├── settings.component.ts │ │ │ │ │ └── settings.component.spec.ts │ │ │ │ ├── documents │ │ │ │ │ ├── documents.component.css │ │ │ │ │ ├── documents.component.html │ │ │ │ │ ├── documents.component.ts │ │ │ │ │ └── documents.component.spec.ts │ │ │ │ ├── home │ │ │ │ │ ├── home.component.spec.ts │ │ │ │ │ ├── home.component.css │ │ │ │ │ ├── home.component.html │ │ │ │ │ └── home.component.ts │ │ │ │ ├── dashboard.component.spec.ts │ │ │ │ ├── dashboard.component.html │ │ │ │ ├── dashboard.component.ts │ │ │ │ └── dashboard.component.css │ │ │ ├── signin │ │ │ │ ├── signin.component.css │ │ │ │ ├── signin.component.spec.ts │ │ │ │ ├── signin.component.html │ │ │ │ └── signin.component.ts │ │ │ └── signup │ │ │ │ ├── signup.component.css │ │ │ │ ├── signup.component.spec.ts │ │ │ │ ├── signup.component.html │ │ │ │ └── signup.component.ts │ │ ├── components │ │ │ ├── chatbox │ │ │ │ ├── chatbox.component.css │ │ │ │ ├── chatbox.component.spec.ts │ │ │ │ ├── chatbox.component.html │ │ │ │ └── chatbox.component.ts │ │ │ ├── empty │ │ │ │ ├── empty.component.css │ │ │ │ ├── empty.component.html │ │ │ │ ├── empty.component.ts │ │ │ │ └── empty.component.spec.ts │ │ │ └── forms │ │ │ │ ├── review-form │ │ │ │ ├── review-form.component.css │ │ │ │ ├── review-form.component.spec.ts │ │ │ │ ├── review-form.component.html │ │ │ │ └── review-form.component.ts │ │ │ │ └── create-query │ │ │ │ ├── create-query.component.css │ │ │ │ ├── create-query.component.spec.ts │ │ │ │ ├── create-query.component.ts │ │ │ │ └── create-query.component.html │ │ ├── app.component.html │ │ ├── utils │ │ │ └── customFetch.ts │ │ ├── modals │ │ │ ├── edit │ │ │ │ ├── edit.component.css │ │ │ │ ├── edit.component.spec.ts │ │ │ │ ├── edit.component.html │ │ │ │ └── edit.component.ts │ │ │ ├── create-query │ │ │ │ ├── create-query.component.css │ │ │ │ ├── create-query.component.spec.ts │ │ │ │ ├── create-query.component.ts │ │ │ │ └── create-query.component.html │ │ │ └── create-review │ │ │ │ ├── create-review.component.css │ │ │ │ ├── create-review.component.spec.ts │ │ │ │ ├── create-review.component.html │ │ │ │ └── create-review.component.ts │ │ ├── app.component.ts │ │ ├── services │ │ │ ├── api.service.spec.ts │ │ │ ├── chat.service.spec.ts │ │ │ ├── user.service.spec.ts │ │ │ ├── general.service.spec.ts │ │ │ ├── research.service.spec.ts │ │ │ ├── chat.service.ts │ │ │ ├── general.service.ts │ │ │ ├── user.service.ts │ │ │ ├── api.service.ts │ │ │ └── research.service.ts │ │ ├── auth │ │ │ ├── guard.guard.ts │ │ │ └── guard.guard.spec.ts │ │ ├── interceptors │ │ │ ├── auth.interceptor.spec.ts │ │ │ └── auth.interceptor.ts │ │ ├── app.config.ts │ │ ├── app.component.spec.ts │ │ └── app.routes.ts │ ├── favicon.ico │ ├── main.ts │ ├── index.html │ ├── theme.less │ └── styles.css ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── package.json └── angular.json ├── middleware ├── userMiddleware.js ├── not-found.js └── authMiddleware.js ├── .gitignore ├── .gitattributes ├── .env-example ├── db └── connect.js ├── routes ├── userRoutes.js ├── chatRoutes.js ├── authRoutes.js └── researchRoutes.js ├── errors ├── notFound.js └── unauthenticated.js ├── utils ├── filterScholarResponse.js ├── attachCookie.js ├── tokenUtils.js ├── openAiRequest.js └── webScrapper.js ├── controllers ├── userController.js ├── authController.js ├── chatController.js └── researchController.js ├── index.html ├── models ├── SystematicReview.js ├── FilterQuery.js ├── Chat.js ├── PrimaryStudies.js ├── ResearchPapers.js └── User.js ├── 404.html ├── LICENSE ├── package.json ├── server.js └── README.md /client/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /middleware/userMiddleware.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/models/navLinks.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /client/src/app/pages/welcome/welcome.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/components/chatbox/chatbox.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/profile/profile.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/home/home.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/research.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/settings/settings.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/documents/documents.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/home/home.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/pages/welcome/welcome.component.html: -------------------------------------------------------------------------------- 1 |

welcome works!

2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |

profile works!

2 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/settings/settings.component.html: -------------------------------------------------------------------------------- 1 |

settings works!

2 | -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftoucch/weblit/HEAD/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/research.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/documents/documents.component.html: -------------------------------------------------------------------------------- 1 |

2 | Document works. 3 |

4 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | JWT_EXPIRES_IN 2 | JWT_SECRET 3 | MONGO_URL 4 | NODE_ENV 5 | OPEN_API_SECRET_KEY 6 | PORT 7 | -------------------------------------------------------------------------------- /client/src/app/components/empty/empty.component.css: -------------------------------------------------------------------------------- 1 | span { 2 | text-align: center; 3 | margin-inline: auto; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/app/utils/customFetch.ts: -------------------------------------------------------------------------------- 1 | export const customFetch = ({ 2 | baseURL: 'http://localhost:5100/api/v1/', 3 | }); 4 | -------------------------------------------------------------------------------- /middleware/not-found.js: -------------------------------------------------------------------------------- 1 | const notFoundMiddleware = (req, res) => 2 | res.status(404).send('Route does not exist') 3 | 4 | export default notFoundMiddleware -------------------------------------------------------------------------------- /db/connect.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const connectDB = (url) => { 4 | return mongoose.connect(url) 5 | } 6 | 7 | export default connectDB -------------------------------------------------------------------------------- /client/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /client/src/app/components/forms/review-form/review-form.component.css: -------------------------------------------------------------------------------- 1 | .form-actions { 2 | display: flex; 3 | gap: 1rem; 4 | } 5 | 6 | .btn { 7 | min-width: 60px ; 8 | } -------------------------------------------------------------------------------- /client/src/app/modals/edit/edit.component.css: -------------------------------------------------------------------------------- 1 | .form-input.large { 2 | min-height: 300px; 3 | } 4 | .form-input, 5 | .form-textarea, 6 | .form-select { 7 | background: transparent; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/app/models/auth.spec.ts: -------------------------------------------------------------------------------- 1 | import { Auth } from './auth'; 2 | 3 | describe('Auth', () => { 4 | it('should create an instance', () => { 5 | expect(new Auth()).toBeTruthy(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /client/src/app/modals/create-query/create-query.component.css: -------------------------------------------------------------------------------- 1 | .form-input.large { 2 | min-height: 120px; 3 | } 4 | .form-input, 5 | .form-textarea, 6 | .form-select { 7 | background: transparent; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/app/modals/create-review/create-review.component.css: -------------------------------------------------------------------------------- 1 | .form-input.large { 2 | min-height: 300px; 3 | } 4 | .form-input, 5 | .form-textarea, 6 | .form-select { 7 | background: transparent; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/app/models/research.spec.ts: -------------------------------------------------------------------------------- 1 | import { Research } from './research'; 2 | 3 | describe('Research', () => { 4 | it('should create an instance', () => { 5 | expect(new Research()).toBeTruthy(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { getCurrentUser } from '../controllers/userController.js'; 3 | 4 | const router = express.Router(); 5 | 6 | router.route('/me').get(getCurrentUser) 7 | 8 | export default router -------------------------------------------------------------------------------- /client/src/app/pages/welcome/welcome.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { WelcomeComponent } from './welcome.component'; 3 | 4 | export const WELCOME_ROUTES: Routes = [ 5 | { path: '', component: WelcomeComponent }, 6 | ]; 7 | -------------------------------------------------------------------------------- /errors/notFound.js: -------------------------------------------------------------------------------- 1 | class NotFoundError extends Error { 2 | constructor(message) { 3 | super(message); 4 | this.name = 'NotFoundError'; 5 | this.statusCode = 404; 6 | } 7 | } 8 | 9 | export default NotFoundError; 10 | -------------------------------------------------------------------------------- /routes/chatRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Startchat, getChatHistory } from '../controllers/chatController.js'; 3 | const router = express.Router(); 4 | 5 | router.route('/:id').post(Startchat).get(getChatHistory) 6 | 7 | export default router -------------------------------------------------------------------------------- /client/src/app/models/auth.ts: -------------------------------------------------------------------------------- 1 | export class Auth { 2 | } 3 | 4 | export class Signin { 5 | email: string = ''; 6 | password: string = ''; 7 | } 8 | 9 | 10 | export class Signup { 11 | name: string ='' ; 12 | email: string = ''; 13 | password: string = ''; 14 | } -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /utils/filterScholarResponse.js: -------------------------------------------------------------------------------- 1 | const filterScholarresponse = (data) => { 2 | const filteredPapers = data.data.filter( 3 | (paper) => paper.abstract !== null && paper.abstract.trim() !== '' 4 | ); 5 | return filteredPapers; 6 | }; 7 | 8 | export default filterScholarresponse; 9 | -------------------------------------------------------------------------------- /errors/unauthenticated.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes' 2 | 3 | class UnAuthenticatedError extends Error { 4 | constructor(message) { 5 | super(message) 6 | this.statusCode = StatusCodes.UNAUTHORIZED 7 | } 8 | } 9 | 10 | export default UnAuthenticatedError -------------------------------------------------------------------------------- /routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { register, login, logout} from '../controllers/authController.js'; 3 | 4 | const router = express.Router(); 5 | 6 | router.route('/register').post(register) 7 | router.route('/login').post(login) 8 | router.route('/logout').get(logout) 9 | 10 | 11 | export default router -------------------------------------------------------------------------------- /utils/attachCookie.js: -------------------------------------------------------------------------------- 1 | const attachCookie = ({ res, token }) => { 2 | const oneDay = 1000 * 60 * 60 * 24; 3 | 4 | res.cookie('token', token, { 5 | httpOnly: true, 6 | expires: new Date(Date.now() + oneDay), 7 | secure: process.env.NODE_ENV === 'production', 8 | }); 9 | }; 10 | 11 | export default attachCookie; 12 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/src/app/components/forms/create-query/create-query.component.css: -------------------------------------------------------------------------------- 1 | .form-numbers-group { 2 | display: grid; 3 | grid-template-columns: repeat(3, 1fr); 4 | gap: 1rem; 5 | } 6 | 7 | textarea { 8 | min-height: 100px; 9 | } 10 | 11 | .form-actions { 12 | display: flex; 13 | gap: 1rem; 14 | } 15 | 16 | .btn { 17 | min-width: 60px ; 18 | } -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Weblit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | standalone: true, 7 | imports: [RouterOutlet], 8 | templateUrl: './app.component.html', 9 | styleUrl: './app.component.css' 10 | }) 11 | export class AppComponent { 12 | title = 'client'; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/app/pages/welcome/welcome.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-welcome', 5 | standalone: true, 6 | templateUrl: './welcome.component.html', 7 | styleUrls: ['./welcome.component.css'] 8 | }) 9 | export class WelcomeComponent implements OnInit { 10 | 11 | constructor() { } 12 | 13 | ngOnInit() { } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /utils/tokenUtils.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | export const createJWT = (payload) => { 4 | const token = jwt.sign(payload, process.env.JWT_SECRET, { 5 | expiresIn: process.env.JWT_EXPIRES_IN, 6 | }); 7 | 8 | return token; 9 | }; 10 | 11 | export const verifyJWT = (token) => { 12 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 13 | return decoded; 14 | } 15 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/research.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-research', 6 | standalone: true, 7 | imports: [RouterOutlet], 8 | templateUrl: './research.component.html', 9 | styleUrl: './research.component.css' 10 | }) 11 | export class ResearchComponent { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /controllers/userController.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User.js'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | 4 | const getCurrentUser = async (req, res) => { 5 | const user = await User.findOne({ _id: req.user.userId }); 6 | res 7 | .status(StatusCodes.OK) 8 | .json({id: user.id, email: user.email, name: user.name, token: req.user.token }); 9 | }; 10 | 11 | export { getCurrentUser }; 12 | -------------------------------------------------------------------------------- /client/src/app/models/research.ts: -------------------------------------------------------------------------------- 1 | export class systematicReview { 2 | title: any = ''; 3 | description: any = ''; 4 | user: any = ''; 5 | } 6 | 7 | export class filterQuery { 8 | researchQuestion: string = ''; 9 | inclusionCriteria: string = ''; 10 | exclusionCriteria: string = ''; 11 | searchString: string = ''; 12 | systematicReviewId: any = ''; 13 | startYear: any = ''; 14 | endYear: any= ''; 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/services/api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiService } from './api.service'; 4 | 5 | describe('ApiService', () => { 6 | let service: ApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/chat.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatService } from './chat.service'; 4 | 5 | describe('ChatService', () => { 6 | let service: ChatService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ChatService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserService } from './user.service'; 4 | 5 | describe('UserService', () => { 6 | let service: UserService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/components/empty/empty.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | You currently have no primary study please create a new query to begin. 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/app/services/general.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { GeneralService } from './general.service'; 4 | 5 | describe('GeneralService', () => { 6 | let service: GeneralService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(GeneralService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/research.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ResearchService } from './research.service'; 4 | 5 | describe('ResearchService', () => { 6 | let service: ResearchService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ResearchService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/auth/guard.guard.ts: -------------------------------------------------------------------------------- 1 | import { PLATFORM_ID, inject } from '@angular/core'; 2 | import { CanActivateFn } from '@angular/router'; 3 | import { GeneralService } from '../services/general.service'; 4 | 5 | export const guardGuard: CanActivateFn = (route, state) => { 6 | const generalService = inject(GeneralService); 7 | let jwtToken = generalService.getToken(); 8 | if (!jwtToken) { 9 | generalService.logOutUser(); 10 | return false; 11 | } 12 | return true; 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/app/services/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ApiService } from './api.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class ChatService { 8 | 9 | constructor(private apiService: ApiService) { 10 | } 11 | 12 | startChat(data:any, id: String) { 13 | return this.apiService.post(`chat/${id}`, data); 14 | } 15 | getChataHistory(id:String) { 16 | return this.apiService.get(`chat/${id}`) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ResearchService } from '../../../services/research.service'; 3 | 4 | @Component({ 5 | selector: 'app-profile', 6 | standalone: true, 7 | imports: [], 8 | templateUrl: './profile.component.html', 9 | styleUrl: './profile.component.css', 10 | }) 11 | export class ProfileComponent { 12 | constructor(private researchService: ResearchService) { 13 | this.researchService.clearSelectedResearch(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ResearchService } from '../../../services/research.service'; 3 | 4 | @Component({ 5 | selector: 'app-settings', 6 | standalone: true, 7 | imports: [], 8 | templateUrl: './settings.component.html', 9 | styleUrl: './settings.component.css', 10 | }) 11 | export class SettingsComponent { 12 | constructor(private researchService: ResearchService) { 13 | this.researchService.clearSelectedResearch(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/documents/documents.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ResearchService } from '../../../services/research.service'; 3 | 4 | @Component({ 5 | selector: 'app-documents', 6 | standalone: true, 7 | imports: [], 8 | templateUrl: './documents.component.html', 9 | styleUrl: './documents.component.css', 10 | }) 11 | export class DocumentsComponent { 12 | constructor(private researchService: ResearchService) { 13 | this.researchService.clearSelectedResearch(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/auth/guard.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { CanActivateFn } from '@angular/router'; 3 | 4 | import { guardGuard } from './guard.guard'; 5 | 6 | describe('guardGuard', () => { 7 | const executeGuard: CanActivateFn = (...guardParameters) => 8 | TestBed.runInInjectionContext(() => guardGuard(...guardParameters)); 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({}); 12 | }); 13 | 14 | it('should be created', () => { 15 | expect(executeGuard).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /client/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /client/src/app/interceptors/auth.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { HttpInterceptorFn } from '@angular/common/http'; 3 | 4 | import { authInterceptor } from './auth.interceptor'; 5 | 6 | describe('authInterceptor', () => { 7 | const interceptor: HttpInterceptorFn = (req, next) => 8 | TestBed.runInInjectionContext(() => authInterceptor(req, next)); 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({}); 12 | }); 13 | 14 | it('should be created', () => { 15 | expect(interceptor).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /client/src/app/components/empty/empty.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { NzEmptyModule } from 'ng-zorro-antd/empty'; 3 | 4 | @Component({ 5 | selector: 'app-empty', 6 | standalone: true, 7 | templateUrl: './empty.component.html', 8 | imports: [NzEmptyModule], 9 | }) 10 | export class EmptyComponent { 11 | @Input() createQuery!: Function; 12 | @Input() createReview!: Function; 13 | handleClick(): void { 14 | if (this.createQuery) { 15 | this.createQuery(); 16 | } 17 | if (this.createReview) { 18 | this.createReview(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/app/pages/signin/signin.component.css: -------------------------------------------------------------------------------- 1 | .full-page { 2 | display: grid; 3 | align-items: center; 4 | } 5 | 6 | .image-container { 7 | display: none; 8 | } 9 | .btn { 10 | margin-top: 1rem; 11 | } 12 | 13 | .member-btn { 14 | background: transparent; 15 | border: transparent; 16 | color: var(--primary-500); 17 | cursor: pointer; 18 | letter-spacing: var(--letterSpacing); 19 | } 20 | 21 | @media(min-width:768px) { 22 | .full-page { 23 | grid-template-columns: 1fr 2fr; 24 | } 25 | 26 | .image-container { 27 | display: block; 28 | height: 100dvh; 29 | background-color: var(--primary-500); 30 | } 31 | } -------------------------------------------------------------------------------- /client/src/app/pages/signup/signup.component.css: -------------------------------------------------------------------------------- 1 | .full-page { 2 | display: grid; 3 | align-items: center; 4 | } 5 | 6 | .image-container { 7 | display: none; 8 | } 9 | 10 | .btn { 11 | margin-top: 1rem; 12 | } 13 | 14 | .member-btn { 15 | background: transparent; 16 | border: transparent; 17 | color: var(--primary-500); 18 | cursor: pointer; 19 | letter-spacing: var(--letterSpacing); 20 | } 21 | 22 | input.input-error { 23 | border: 1px solid var(--red-dark) 24 | } 25 | 26 | @media(min-width:768px) { 27 | .full-page { 28 | grid-template-columns: 1fr 2fr; 29 | } 30 | 31 | .image-container { 32 | display: block; 33 | height: 100dvh; 34 | background-color: var(--primary-500); 35 | } 36 | } -------------------------------------------------------------------------------- /client/src/app/services/general.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class GeneralService { 8 | constructor() {} 9 | 10 | saveUser(user: any) { 11 | sessionStorage.setItem('user', JSON.stringify(user)); 12 | } 13 | getToken() { 14 | let res: any = sessionStorage.getItem('user') ?? undefined; 15 | 16 | if (!res || res == '') { 17 | return ''; 18 | } 19 | 20 | return JSON.parse(res).token; 21 | } 22 | 23 | logOutUser() { 24 | sessionStorage.clear(); 25 | window.location.replace('/signin'); 26 | console.log('out'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/src/app/modals/edit/edit.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EditComponent } from './edit.component'; 4 | 5 | describe('EditComponent', () => { 6 | let component: EditComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [EditComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(EditComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HomeComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(HomeComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/components/empty/empty.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { fakeAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { EmptyComponent } from './empty.component'; 3 | 4 | describe('EmptyComponent', () => { 5 | let component: EmptyComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(fakeAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [ EmptyComponent ] 11 | }) 12 | .compileComponents(); 13 | 14 | fixture = TestBed.createComponent(EmptyComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | })); 18 | 19 | it('should compile', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HomeComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(HomeComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/pages/signin/signin.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SigninComponent } from './signin.component'; 4 | 5 | describe('SigninComponent', () => { 6 | let component: SigninComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SigninComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SigninComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/pages/signup/signup.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SignupComponent } from './signup.component'; 4 | 5 | describe('SignupComponent', () => { 6 | let component: SignupComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SignupComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SignupComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProfileComponent } from './profile.component'; 4 | 5 | describe('ProfileComponent', () => { 6 | let component: ProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ProfileComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ProfileComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import UnAuthenticatedError from '../errors/unauthenticated.js'; 3 | import { verifyJWT } from '../utils/tokenUtils.js'; 4 | 5 | const authMiddleware = async (req, res, next) => { 6 | const authHeader = req.headers.authorization; 7 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 8 | throw new UnAuthenticatedError('Authentication Invalid'); 9 | } 10 | const token = authHeader.split(' ')[1]; 11 | 12 | try { 13 | const { userId, email } = verifyJWT(token); 14 | req.user = { userId, email, token }; 15 | next(); 16 | } catch (error) { 17 | throw new UnAuthenticatedError('Authentication Invalid'); 18 | } 19 | }; 20 | 21 | export default authMiddleware; 22 | -------------------------------------------------------------------------------- /client/src/app/components/chatbox/chatbox.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { fakeAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { ChatboxComponent } from './chatbox.component'; 3 | 4 | describe('ChatboxComponent', () => { 5 | let component: ChatboxComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(fakeAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [ ChatboxComponent ] 11 | }) 12 | .compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatboxComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | })); 18 | 19 | it('should compile', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DashboardComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DashboardComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/details/details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DetailsComponent } from './details.component'; 4 | 5 | describe('DetailsComponent', () => { 6 | let component: DetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DetailsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DetailsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/research.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ResearchComponent } from './research.component'; 4 | 5 | describe('ResearchComponent', () => { 6 | let component: ResearchComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ResearchComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ResearchComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SettingsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SettingsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/documents/documents.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DocumentsComponent } from './documents.component'; 4 | 5 | describe('DocumentsComponent', () => { 6 | let component: DocumentsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DocumentsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DocumentsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/modals/create-query/create-query.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateQueryComponent } from './create-query.component'; 4 | 5 | describe('CreateQueryComponent', () => { 6 | let component: CreateQueryComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CreateQueryComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CreateQueryComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/components/forms/review-form/review-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ReviewFormComponent } from './review-form.component'; 4 | 5 | describe('ReviewFormComponent', () => { 6 | let component: ReviewFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ReviewFormComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ReviewFormComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/components/forms/create-query/create-query.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateQueryComponent } from './create-query.component'; 4 | 5 | describe('CreateQueryComponent', () => { 6 | let component: CreateQueryComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CreateQueryComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CreateQueryComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/modals/create-review/create-review.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateReviewComponent } from './create-review.component'; 4 | 5 | describe('CreateReviewComponent', () => { 6 | let component: CreateReviewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CreateReviewComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CreateReviewComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Signin, Signup } from '../models/auth'; 2 | import { ApiService } from './api.service'; 3 | import { Injectable } from '@angular/core'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class UserService { 9 | constructor(private apiService: ApiService) {} 10 | signIn(data: Signin) { 11 | return this.apiService.post('auth/login', data); 12 | } 13 | signUp(data: Signup) { 14 | return this.apiService.post('auth/register', data); 15 | } 16 | 17 | refreshToken() { 18 | return this.apiService.get('users/me'); 19 | } 20 | getUser() { 21 | let res: any = sessionStorage.getItem('user') ?? undefined; 22 | 23 | if (!res || res == '') { 24 | return ''; 25 | } 26 | 27 | return JSON.parse(res); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | l 2 | 3 | 4 | 5 | 6 | Weblit Backend API 7 | 8 | 28 | 29 |
30 |

WELCOME TO WEBLIT !!!

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /models/SystematicReview.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const SystematicReviewSchema = new mongoose.Schema({ 4 | 5 | title: { 6 | type: String, 7 | required: [true, 'Please provide a title for the Systematic Literature Review'], 8 | }, 9 | description: { 10 | type: String, 11 | }, 12 | user: { 13 | type: mongoose.Types.ObjectId, 14 | ref: 'User', 15 | required: [true, 'Please provide user'], 16 | }, 17 | researchAssistantId: { 18 | type: String, 19 | required: [true, 'please provide assistant ID'] 20 | }, 21 | 22 | chatAssistantId: { 23 | type: String, 24 | required: [true, 'please provide assistant ID'] 25 | } 26 | 27 | }, 28 | { timestamps: true } 29 | ) 30 | 31 | export default mongoose.model('SystematicReviewScholar', SystematicReviewSchema) -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Weblit Backend API 7 | 8 | 26 | 27 |
28 |

NOT FOUND

29 |

The requested route does not exist

30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /routes/researchRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { 3 | createResearch, 4 | allResearch, 5 | getResearch, 6 | createQuery, 7 | deleteResearch, 8 | updateResearch, 9 | allQuery, 10 | getAllPrimaryStudy, 11 | deleteQuery, 12 | getAllResearchPaper, 13 | } from '../controllers/researchController.js'; 14 | 15 | const router = express.Router(); 16 | 17 | router.route('/create').post(createResearch); 18 | router.route('/all').get(allResearch); 19 | router 20 | .route('/:id') 21 | .get(getResearch) 22 | .delete(deleteResearch) 23 | .patch(updateResearch); 24 | router.route('/query/create').post(createQuery); 25 | router.route('/query/:id').delete(deleteQuery); 26 | router.route('/query/all/:id').get(allQuery); 27 | router.route('/primarystudies/:id').get(getAllPrimaryStudy); 28 | router.route('/un/papers/:id').get(getAllResearchPaper); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /models/FilterQuery.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const FilterQuerySchema = new mongoose.Schema( 4 | { 5 | researchQuestion: { 6 | type: String, 7 | required: [true, 'please provide research question'], 8 | }, 9 | inclusionCriteria: { 10 | type: String, 11 | required: [true, 'please provide inclusion criteria'], 12 | }, 13 | exclusionCriteria: { 14 | type: String, 15 | required: [true, 'please provide exclusion criteria'], 16 | }, 17 | searchString: { 18 | type: String, 19 | required: [true, 'please provide a search string'], 20 | }, 21 | systematicReviewId: { 22 | type: mongoose.Types.ObjectId, 23 | ref: 'SystematicReview', 24 | required: [true, 'please provide the systematic review ID'], 25 | }, 26 | totalFound: { 27 | type: Number, 28 | }, 29 | }, 30 | { timestamps: true } 31 | ); 32 | 33 | export default mongoose.model('FilterQuery', FilterQuerySchema); 34 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "useDefineForClassFields": false, 21 | "lib": [ 22 | "ES2022", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { en_US, provideNzI18n } from 'ng-zorro-antd/i18n'; 6 | import { registerLocaleData } from '@angular/common'; 7 | import en from '@angular/common/locales/en'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 10 | import { 11 | provideHttpClient, 12 | withFetch, 13 | withInterceptors, 14 | } from '@angular/common/http'; 15 | import { authInterceptor } from './interceptors/auth.interceptor'; 16 | 17 | registerLocaleData(en); 18 | 19 | export const appConfig: ApplicationConfig = { 20 | providers: [ 21 | provideRouter(routes), 22 | provideNzI18n(en_US), 23 | importProvidersFrom(FormsModule), 24 | provideAnimationsAsync(), 25 | provideHttpClient(withInterceptors([authInterceptor]), withFetch()), 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /models/Chat.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const MessageSchema = new mongoose.Schema({ 4 | role: { type: String, required: true }, // 'user' or 'assistant' 5 | message: { type: String }, 6 | timestamp: { type: Date, default: Date.now } 7 | }); 8 | 9 | const ChatSchema = new mongoose.Schema({ 10 | chatAssistantId: { 11 | type: String, 12 | required: [true, 'please provide assistant ID'] 13 | }, 14 | threadId: { 15 | type: String, 16 | required: [true, 'please provide thread ID'] 17 | }, 18 | user: { 19 | type: mongoose.Types.ObjectId, 20 | ref: 'User', 21 | required: [true, 'Please provide user'] 22 | }, 23 | systematicReviewId: { 24 | type: mongoose.Types.ObjectId, 25 | ref: 'SystematicReview', 26 | required: [true, 'please provide the systematic review ID'] 27 | }, 28 | messages: [MessageSchema] 29 | }, { 30 | timestamps: true 31 | }); 32 | 33 | export default mongoose.model('Chat', ChatSchema); 34 | -------------------------------------------------------------------------------- /client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'client' title`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('client'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, client'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /client/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Fatai Faith Alimi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weblit", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "nodemon server.js", 9 | "setup-project": "npm i && cd client && npm i", 10 | "postinstall": "npx playwright install chromium" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "axios": "^1.7.9", 17 | "bcryptjs": "^2.4.3", 18 | "cheerio": "^1.0.0", 19 | "cloudinary": "^2.5.1", 20 | "concurrently": "^9.1.2", 21 | "cookie-parser": "^1.4.7", 22 | "cors": "^2.8.5", 23 | "datauri": "^4.1.0", 24 | "dayjs": "^1.11.13", 25 | "dotenv": "^16.4.7", 26 | "express": "^4.21.2", 27 | "express-async-errors": "^3.1.1", 28 | "express-mongo-sanitize": "^2.2.0", 29 | "express-rate-limit": "^7.5.0", 30 | "express-validator": "^7.2.1", 31 | "helmet": "^8.0.0", 32 | "http-status-codes": "^2.3.0", 33 | "jsonwebtoken": "^9.0.2", 34 | "mongoose": "^8.9.7", 35 | "morgan": "^1.10.0", 36 | "multer": "^1.4.5-lts.1", 37 | "nanoid": "^5.0.9", 38 | "nodemon": "^3.1.9", 39 | "openai": "^4.82.0", 40 | "playwright": "^1.50.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/src/app/modals/edit/edit.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 | 11 | 22 |
23 |
24 | 25 | 35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/home/home.component.css: -------------------------------------------------------------------------------- 1 | .header-section { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | } 6 | .reviews { 7 | display: flex; 8 | flex-direction: column; 9 | gap: 2rems; 10 | } 11 | 12 | .reviews .review { 13 | display: flex; 14 | justify-content: space-between; 15 | cursor: pointer; 16 | transition: all 0.3s ease-in-out; 17 | padding: 1rem; 18 | border-radius: var(--borderRadius); 19 | } 20 | .reviews .review:hover { 21 | background-color: var(--grey-50); 22 | } 23 | p { 24 | margin-bottom: 0; 25 | } 26 | .research-title { 27 | text-transform: capitalize; 28 | font-size: 1.1rem; 29 | } 30 | .date-nav { 31 | display: flex; 32 | gap: 1rem; 33 | align-items: center; 34 | } 35 | .date-nav span { 36 | cursor: pointer; 37 | } 38 | 39 | .create-review { 40 | display: flex; 41 | gap: 1rem; 42 | margin-top: 1rem; 43 | padding: .5rem 1rem; 44 | background-color: var(--grey-50); 45 | border-radius: var(--borderRadius); 46 | cursor: pointer; 47 | align-items: center; 48 | } 49 | 50 | .create-review-wrapper{ 51 | padding: 1rem; 52 | } 53 | .review-edit-form{ 54 | width: 100%; 55 | } -------------------------------------------------------------------------------- /client/src/app/modals/create-review/create-review.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 | 11 | 22 |
23 |
24 | 25 | 35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /client/src/app/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { customFetch } from '../utils/customFetch.js'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class ApiService { 9 | constructor(public http: HttpClient) {} 10 | get url() { 11 | return customFetch.baseURL; 12 | } 13 | 14 | get(endpoint: string, params?: any, reqOpts?: any) { 15 | if (!reqOpts) { 16 | reqOpts = { 17 | params: new HttpParams(), 18 | }; 19 | } 20 | 21 | //support easy query params for GET request 22 | if (params) { 23 | reqOpts.params = new HttpParams(); 24 | for (const k in params) { 25 | reqOpts.params = reqOpts.params.set(k, params[k]); 26 | } 27 | } 28 | 29 | return this.http.get(this.url + endpoint, reqOpts); 30 | } 31 | post(endpoint: string, body: any, reqOpts?: any) { 32 | return this.http.post(this.url + endpoint, body, reqOpts); 33 | } 34 | 35 | delete(endpoint: string, reqOpts?: any) { 36 | return this.http.delete(this.url + endpoint, reqOpts); 37 | } 38 | update(endpoint: string, reqOpts?: any) { 39 | return this.http.patch(this.url + endpoint, reqOpts); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /models/PrimaryStudies.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const AuthorSchema = new mongoose.Schema( 4 | { 5 | authorId: { 6 | type: String, 7 | }, 8 | name: { 9 | type: String, 10 | }, 11 | }, 12 | { _id: false } 13 | ); 14 | 15 | const PrimaryStudySchema = new mongoose.Schema({ 16 | title: { 17 | type: String, 18 | required: [true, 'please provide title'], 19 | }, 20 | abstract: { 21 | type: String, 22 | }, 23 | url: { 24 | type: String 25 | }, 26 | authors: [AuthorSchema], 27 | referenceCount: { 28 | type: Number, 29 | }, 30 | referenceCount: { 31 | type: Number, 32 | }, 33 | citationCount: { 34 | type: Number, 35 | }, 36 | year: { 37 | type: Number, 38 | }, 39 | openAccessPdf: { 40 | type: Object, 41 | }, 42 | filterQuery: [ 43 | { 44 | type: mongoose.Types.ObjectId, 45 | ref: 'FilterQuery', 46 | }, 47 | ], 48 | systematicReviewId: { 49 | type: mongoose.Types.ObjectId, 50 | ref: 'SystematicReview', 51 | required: [true, 'please provide the systematic review ID'], 52 | }, 53 | user: { 54 | type: mongoose.Types.ObjectId, 55 | ref: 'User', 56 | required: [true, 'please provide the user ID'], 57 | }, 58 | }); 59 | 60 | export default mongoose.model('PrimaryStudy', PrimaryStudySchema); 61 | -------------------------------------------------------------------------------- /client/src/app/pages/signin/signin.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
Login

Login to get started

5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 18 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /models/ResearchPapers.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const AuthorSchema = new mongoose.Schema( 4 | { 5 | authorId: { 6 | type: String, 7 | }, 8 | name: { 9 | type: String, 10 | }, 11 | }, 12 | { _id: false } 13 | ); 14 | 15 | const ResearchPaperSchema = new mongoose.Schema({ 16 | title: { 17 | type: String, 18 | required: [true, 'please provide title'], 19 | }, 20 | abstract: { 21 | type: String, 22 | }, 23 | url: { 24 | type: String 25 | }, 26 | authors: [AuthorSchema], 27 | referenceCount: { 28 | type: Number, 29 | }, 30 | referenceCount: { 31 | type: Number, 32 | }, 33 | citationCount: { 34 | type: Number, 35 | }, 36 | year: { 37 | type: Number, 38 | }, 39 | openAccessPdf: { 40 | type: Object, 41 | }, 42 | filterQuery: [ 43 | { 44 | type: mongoose.Types.ObjectId, 45 | ref: 'FilterQuery', 46 | }, 47 | ], 48 | systematicReviewId: { 49 | type: mongoose.Types.ObjectId, 50 | ref: 'SystematicReview', 51 | required: [true, 'please provide the systematic review ID'], 52 | }, 53 | user: { 54 | type: mongoose.Types.ObjectId, 55 | ref: 'User', 56 | required: [true, 'please provide the user ID'], 57 | }, 58 | }); 59 | 60 | export default mongoose.model('ResearchPaper', ResearchPaperSchema); 61 | -------------------------------------------------------------------------------- /client/src/app/interceptors/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http'; 2 | import { GeneralService } from '../services/general.service'; 3 | import { inject } from '@angular/core'; 4 | import { catchError, throwError } from 'rxjs'; 5 | 6 | export const authInterceptor: HttpInterceptorFn = (req, next) => { 7 | const generalService = inject(GeneralService); 8 | const apiToken = generalService.getToken(); 9 | const authReq = req.clone({ 10 | setHeaders: { 11 | Authorization: `Bearer ${apiToken}`, 12 | }, 13 | }); 14 | return next(authReq).pipe( 15 | catchError((err: any) => { 16 | if (err instanceof HttpErrorResponse) { 17 | if (err.status === 401) { 18 | generalService.logOutUser(); 19 | return throwError( 20 | 'Your session has expired. Please login to continue.' 21 | ); 22 | } 23 | if (err.status === 403) { 24 | generalService.logOutUser(); 25 | return throwError('You are not authorised.'); 26 | } 27 | 28 | if (err.status == 500) { 29 | return throwError( 30 | 'An expected error occured. Please try again later.' 31 | ); 32 | } 33 | } else { 34 | console.error('An error occurred:', err); 35 | } 36 | return throwError(() => err); 37 | }) 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import validator from 'validator' 3 | import bcrypt from 'bcryptjs' 4 | import jwt from 'jsonwebtoken' 5 | const UserSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | required: [true, 'Please provide name'], 9 | minlength: 3, 10 | maxlength: 20, 11 | trim: true, 12 | }, 13 | email: { 14 | type: String, 15 | required: [true, 'Please provide email'], 16 | validate: { 17 | validator: validator.isEmail, 18 | message: 'Please provide a valid email', 19 | }, 20 | unique: true, 21 | }, 22 | password: { 23 | type: String, 24 | required: [true, 'Please provide password'], 25 | minlength: 6, 26 | select: false, 27 | }, 28 | }) 29 | 30 | UserSchema.pre('save', async function () { 31 | // console.log(this.modifiedPaths()) 32 | if (!this.isModified('password')) return 33 | const salt = await bcrypt.genSalt(10) 34 | this.password = await bcrypt.hash(this.password, salt) 35 | }) 36 | 37 | UserSchema.methods.createJWT = function () { 38 | return jwt.sign({ userId: this._id }, process.env.JWT_SECRET, { 39 | expiresIn: process.env.JWT_LIFETIME, 40 | }) 41 | } 42 | 43 | UserSchema.methods.comparePassword = async function (candidatePassword) { 44 | const isMatch = await bcrypt.compare(candidatePassword, this.password) 45 | return isMatch 46 | } 47 | 48 | export default mongoose.model('User', UserSchema) -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-v2", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^17.3.0", 14 | "@angular/common": "^17.3.0", 15 | "@angular/compiler": "^17.3.0", 16 | "@angular/core": "^17.3.0", 17 | "@angular/forms": "^17.3.0", 18 | "@angular/platform-browser": "^17.3.0", 19 | "@angular/platform-browser-dynamic": "^17.3.0", 20 | "@angular/router": "^17.3.0", 21 | "ng-zorro-antd": "^17.3.0", 22 | "ngx-lottie": "^13.0.0", 23 | "rxjs": "~7.8.0", 24 | "tslib": "^2.3.0", 25 | "zone.js": "~0.14.3" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "^17.3.1", 29 | "@angular/cli": "^17.3.1", 30 | "@angular/compiler-cli": "^17.3.0", 31 | "@types/jasmine": "~5.1.0", 32 | "jasmine-core": "~5.1.0", 33 | "karma": "~6.4.0", 34 | "karma-chrome-launcher": "~3.2.0", 35 | "karma-coverage": "~2.2.0", 36 | "karma-jasmine": "~5.1.0", 37 | "karma-jasmine-html-reporter": "~2.1.0", 38 | "typescript": "~5.4.2" 39 | }, 40 | "description": "This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.1.", 41 | "main": "index.js", 42 | "author": "", 43 | "license": "ISC" 44 | } 45 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/details/details.component.css: -------------------------------------------------------------------------------- 1 | .results-container { 2 | display: grid; 3 | } 4 | .primary-studies p{ 5 | color: var(--grey-500); 6 | font-size: 0.8rem; 7 | max-width: 100%; 8 | margin-bottom: 5px; 9 | } 10 | .download, span { 11 | font-size: 0.8rem; 12 | } 13 | .documents-wrapper { 14 | display: flex; 15 | flex-direction: column; 16 | gap: 2rem; 17 | } 18 | 19 | .meta-data { 20 | display: flex; 21 | gap: 1rem; 22 | } 23 | 24 | .meta-data span { 25 | color: var(--grey-700); 26 | } 27 | 28 | .document-title { 29 | color: var(--primary-500); 30 | transition: all 0.3 ease-in-out; 31 | } 32 | 33 | .document-title:hover { 34 | color: var(--primary-400); 35 | } 36 | .query-title { 37 | background-color: white; 38 | border-radius: var(--borderRadius); 39 | padding: 1rem; 40 | display: flex; 41 | gap: 1rem; 42 | align-items: center; 43 | cursor: pointer; 44 | margin-bottom: 1rem; 45 | justify-content: space-between; 46 | } 47 | 48 | .total-found{ 49 | background-color: var(--backgroundColor); 50 | padding: .3rem; 51 | border-radius: var(--borderRadius); 52 | } 53 | 54 | .details { 55 | padding-inline: 1rem; 56 | } 57 | .add-new-query { 58 | width: 100%; 59 | margin-bottom: 2rem; 60 | min-height: 3rem; 61 | background-color: transparent; 62 | border: 1px dashed var(--grey-500); 63 | color: var(--grey-500); 64 | box-shadow: none; 65 | } 66 | @media(min-width:768px) { 67 | .results-container { 68 | grid-template-columns: 1fr 3fr; 69 | gap: 3rem; 70 | } 71 | 72 | 73 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | import connectDB from './db/connect.js'; 5 | import cors from 'cors'; 6 | import authMiddleware from './middleware/authMiddleware.js'; 7 | 8 | import authRouter from './routes/authRoutes.js'; 9 | import researchRouter from './routes/researchRoutes.js'; 10 | import userRouter from './routes/userRoutes.js'; 11 | import chatRouter from './routes/chatRoutes.js'; 12 | 13 | // import for default view 14 | import { dirname } from 'path'; 15 | import { fileURLToPath } from 'url'; 16 | import path from 'path'; 17 | import cookieParser from 'cookie-parser'; 18 | 19 | const __dirname = dirname(fileURLToPath(import.meta.url)); 20 | const app = express(); 21 | app.use(cors()); 22 | app.use(express.json()); 23 | app.use(express.urlencoded({ extended: false })); 24 | app.use(cookieParser()); 25 | 26 | const port = process.env.PORT || 5100; 27 | 28 | app.use('/api/v1/auth', authRouter); 29 | app.use('/api/v1/research', authMiddleware, researchRouter); 30 | app.use('/api/v1/users', authMiddleware, userRouter); 31 | app.use('/api/v1/chat', authMiddleware, chatRouter); 32 | 33 | app.get('/healthz', (req, res) => { 34 | res.status(200).send('OK'); 35 | }); 36 | 37 | app.get('*', (req, res) => { 38 | res.sendFile(path.resolve(__dirname, 'index.html')); 39 | }); 40 | 41 | const start = async () => { 42 | try { 43 | await connectDB(process.env.MONGO_URL); 44 | app.listen(port, () => { 45 | console.log(`Server is running on ${port}....`); 46 | }); 47 | } catch (error) { 48 | console.log(error); 49 | } 50 | }; 51 | 52 | start(); 53 | -------------------------------------------------------------------------------- /client/src/app/components/chatbox/chatbox.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | How can I help you with your systematic Literature review: 6 | {{ researchTitle }} 7 |
8 | 9 |
10 |
11 |

{{ chat.role }}: {{ chat.message }}

12 |
13 | 14 |
15 | 16 | Research Assistant: Processing... 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 34 |
35 | 36 | 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /client/src/app/services/research.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ApiService } from './api.service'; 3 | import { BehaviorSubject } from 'rxjs'; 4 | import { filterQuery, systematicReview } from '../models/research'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ResearchService { 10 | private selectedReview = new BehaviorSubject(null); 11 | public selectedResearch$ = this.selectedReview.asObservable(); 12 | 13 | constructor(private apiService: ApiService) {} 14 | getAllResearch() { 15 | return this.apiService.get('research/all'); 16 | } 17 | getResearch(id: string) { 18 | return this.apiService.get(`research/${id}`); 19 | } 20 | 21 | setSelectedResearch(research: any): void { 22 | this.selectedReview.next(research); 23 | } 24 | 25 | clearSelectedResearch(): void { 26 | this.selectedReview.next(null); 27 | } 28 | createResearch(data: systematicReview) { 29 | return this.apiService.post('research/create', data); 30 | } 31 | deleteResearch(id: string) { 32 | return this.apiService.delete(`research/${id}`); 33 | } 34 | updateResearch(id: string, data: systematicReview) { 35 | return this.apiService.update(`research/${id}`, data); 36 | } 37 | 38 | createQuery(data: filterQuery) { 39 | return this.apiService.post('research/query/create', data); 40 | } 41 | getAllQuery(id: string) { 42 | return this.apiService.get(`research/query/all/${id}`); 43 | } 44 | deleteQuery(id: string) { 45 | return this.apiService.delete(`research/query/${id}`); 46 | } 47 | getAllPrimaryStudies(id: string) { 48 | return this.apiService.get(`research/primarystudies/${id}`); 49 | } 50 | getUnfilteredPapers(id: string) { 51 | return this.apiService.get(`research/un/papers/${id}`); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/src/app/pages/signup/signup.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
Create an account

Let’s get started with a free account

5 |
6 | 7 | 17 |
18 |
19 | 20 | 30 |
31 |
32 | 33 | 42 |
43 | 47 | 50 |
51 |
52 | -------------------------------------------------------------------------------- /client/src/app/components/forms/review-form/review-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 16 |
17 | Title is required. 18 |
19 |
20 |
21 | 22 | 32 |
33 | Description is required. 34 |
35 |
36 |
37 | 41 | 42 |
43 |
44 |
-------------------------------------------------------------------------------- /client/src/theme.less: -------------------------------------------------------------------------------- 1 | // Custom Theming for NG-ZORRO 2 | // For more information: https://ng.ant.design/docs/customize-theme/en 3 | @import "../node_modules/ng-zorro-antd/ng-zorro-antd.less"; 4 | 5 | // Override less variables to here 6 | // View all variables: https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/components/style/themes/default.less 7 | 8 | @primary-color: #743884; 9 | @border-radius-base: 0.5rem; 10 | @modal-content-border-radius: 0.5rem; 11 | @btn-border-radius-base: 2px; 12 | .ant-empty p { 13 | margin-inline: auto; 14 | } 15 | 16 | .ant-drawer-body .flex { 17 | display: flex; 18 | } 19 | 20 | .ant-drawer-body .form-input, 21 | .ant-drawer-body button{ 22 | border-radius: 0; 23 | } 24 | 25 | .ant-drawer-body { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: space-between; 29 | } 30 | 31 | .ant-drawer-body .form-input { 32 | background-color: transparent; 33 | } 34 | 35 | .ant-drawer-body .form-input:focus { 36 | border: 1px solid var(--grey-200); 37 | outline: 1px solid var(--grey-200); 38 | } 39 | 40 | .ant-drawer-body .initial-chat-component { 41 | height: 90%; 42 | display: flex; 43 | flex-direction: column; 44 | justify-content: center; 45 | align-items: center; 46 | gap: 1rem; 47 | text-align: center; 48 | } 49 | 50 | .ant-drawer-body .initial-icon { 51 | font-size: 4rem; 52 | } 53 | 54 | 55 | .ant-drawer-body .chat-container.flex { 56 | flex-direction: column; 57 | margin-bottom: 2rem; 58 | max-height: 90vh; 59 | overflow-y: scroll; 60 | } 61 | 62 | .chat-container.flex::-webkit-scrollbar-track 63 | { 64 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 65 | border-radius: 10px; 66 | background-color: #F5F5F5; 67 | } 68 | 69 | .chat-container.flex::-webkit-scrollbar 70 | { 71 | width: 8px; 72 | background-color: #F5F5F5; 73 | } 74 | 75 | .chat-container.flex::-webkit-scrollbar-thumb 76 | { 77 | border-radius: 10px; 78 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 79 | background-color: var(--primary-100); 80 | } 81 | .ant-drawer-body .chat-container.flex p { 82 | font-size: 1rem; 83 | } 84 | 85 | .form .ant-spin-nested-loading > div > .ant-spin .ant-spin-dot i { 86 | background-color: white; 87 | } -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User.js'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | import attachCookie from '../utils/attachCookie.js'; 4 | import { createJWT } from '../utils/tokenUtils.js'; 5 | import UnAuthenticatedError from '../errors/unauthenticated.js'; 6 | 7 | const register = async (req, res) => { 8 | const { name, email, password } = req.body; 9 | 10 | if (!name || !email || !password) { 11 | throw new UnAuthenticatedError('please provide all field'); 12 | } 13 | const userAlreadyExists = await User.findOne({ email }); 14 | if (userAlreadyExists) { 15 | throw new UnAuthenticatedError('email already in use'); 16 | } 17 | const user = await User.create({ name, email, password }); 18 | 19 | const token = createJWT({ userId: user._id, email: user.email }); 20 | attachCookie({ res, token }); 21 | 22 | res.status(StatusCodes.CREATED).json({ 23 | user: { 24 | userId: user.id, 25 | email: user.email, 26 | name: user.name, 27 | token: token, 28 | message: 'user created', 29 | }, 30 | }); 31 | }; 32 | 33 | const login = async (req, res) => { 34 | const { email, password } = req.body; 35 | 36 | if (!email || !password) { 37 | throw new UnAuthenticatedError('please provide all field'); 38 | } 39 | 40 | const user = await User.findOne({ email }).select('+password'); 41 | if (!user) { 42 | throw new UnAuthenticatedError('Invalid Credentials'); 43 | } 44 | 45 | const isPasswordCorrect = await user.comparePassword(password); 46 | if (!isPasswordCorrect) { 47 | throw new UnAuthenticatedError('Invalid Credentials'); 48 | } 49 | const token = createJWT({ userId: user._id, email: user.email }); 50 | attachCookie({ res, token }); 51 | 52 | res.status(StatusCodes.OK).json({ 53 | userId: user.id, 54 | email: user.email, 55 | name: user.name, 56 | token: token, 57 | message: 'user logged in', 58 | }); 59 | }; 60 | 61 | const logout = async (req, res) => { 62 | res.cookie('token', 'logout', { 63 | httpOnly: true, 64 | expires: new Date(Date.now() + 1000), 65 | }); 66 | res.status(StatusCodes.OK).json({ msg: 'user logged out!' }); 67 | }; 68 | 69 | export { register, login, logout }; 70 | -------------------------------------------------------------------------------- /client/src/app/pages/signin/signin.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { 4 | FormBuilder, 5 | FormControl, 6 | FormGroup, 7 | ReactiveFormsModule, 8 | Validators, 9 | } from '@angular/forms'; 10 | import { RouterLink, Router } from '@angular/router'; 11 | import { Signin } from '../../models/auth'; 12 | import { UserService } from '../../services/user.service'; 13 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 14 | import { GeneralService } from '../../services/general.service'; 15 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 16 | 17 | @Component({ 18 | selector: 'app-signin', 19 | standalone: true, 20 | imports: [CommonModule, ReactiveFormsModule, RouterLink, NzSpinModule,], 21 | templateUrl: './signin.component.html', 22 | styleUrl: './signin.component.css', 23 | }) 24 | export class SigninComponent implements OnInit { 25 | processLoading: boolean = false; 26 | form: FormGroup; 27 | constructor( 28 | private fb: FormBuilder, 29 | private userService: UserService, 30 | private router: Router, 31 | private notification: NzNotificationService, 32 | private generalService: GeneralService 33 | ) { 34 | this.form = fb.group({ 35 | password: ['', [Validators.required]], 36 | email: ['', [Validators.required, Validators.email]], 37 | }); 38 | } 39 | 40 | ngOnInit(): void {} 41 | 42 | signin() { 43 | this.form.markAllAsTouched(); 44 | this.form.markAsDirty(); 45 | if (!this.form.valid) { 46 | this.notification.create( 47 | 'error', 48 | 'error', 49 | 'please check fields and try again' 50 | ); 51 | return; 52 | } 53 | 54 | this.processLoading = true; 55 | let data = new Signin(); 56 | data = { ...data, ...this.form.value }; 57 | 58 | this.userService.signIn(data).subscribe({ 59 | next: (res: any) => { 60 | this.processLoading = false; 61 | this.generalService.saveUser(res); 62 | this.notification.create( 63 | 'success', 64 | 'Success', 65 | 'You have successfully logged in' 66 | ); 67 | this.router.navigateByUrl('/dashboard'); 68 | }, 69 | error: (error: any) => { 70 | this.processLoading = false; 71 | this.notification.create('error', 'error', 'an error occured'); 72 | }, 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebLit: Automated Primary Study Selection for Systematic Literature Reviews (SLRs). 2 | 3 | ## Overview 4 | **WebLit** is an open-source, web-based application designed to fully automate the **primary study selection process** in **Systematic Literature Reviews (SLRs)**. WebLit leverages the power of **GPT-4** to dynamically retrieve, filter, and analyze research papers across multiple disciplines. 5 | 6 | This tool aims to: 7 | - Eliminate manual efforts in SLRs. 8 | - Provide transparent, user-controlled automation. 9 | - Enhance cross-domain applicability. 10 | 11 | --- 12 | 13 | ## Key Features 14 | 15 | - **Dynamic Paper Retrieval**: Automates the search and retrieval of research papers using customizable search strings. 16 | - **Full Automation**: From data collection to study selection—no manual uploads required. 17 | - **Customizable Criteria**: Flexible inclusion/exclusion parameters tailored to specific research needs. 18 | - **Integrated Chat**: Interact with an LLM trained on your selected studies for deeper insights. 19 | - **SLR History & Traceability**: View, edit, and manage all past SLRs for continuous improvement. 20 | - **Cross-Disciplinary Support**: Tested in medical sciences, engineering, and social sciences. 21 | 22 | 23 | --- 24 | 25 | ## Performance Highlights 26 | 27 | - **Precision Rate**: 73.3% (High accuracy in identifying relevant studies) 28 | - **Retention Rate**: 1.7% (Selective and rigorous screening process) 29 | 30 | These metrics emphasize WebLit’s focus on **quality over quantity** in study selection. 31 | 32 | --- 33 | 34 | ## Tech Stack 35 | 36 | - **Frontend:** [Angular](https://angular.io/) 37 | - **Backend:** [Node.js](https://nodejs.org/) 38 | - **LLM:** GPT-4 39 | 40 | --- 41 | 42 | ## Getting Started 43 | 44 | ### Prerequisites 45 | 46 | - **Node.js** (v14 or above) 47 | - **Angular CLI** (v15 or above) 48 | - **npm** (v6 or above) 49 | 50 | --- 51 | 52 | ### Installation 53 | 54 | 1. **Clone the Repository** 55 | ```bash 56 | git clone https://github.com/ftoucch/weblit.git 57 | cd weblit 58 | 59 | 2. **Environment Setup** 60 | Rename .env-example to .env 61 | Add the required environment variables in .env 62 | (Ensure you have API keys or credentials if needed) 63 | 64 | 3. **Backend Setup** 65 | ```bash 66 | npm install && npm run dev 67 | 68 | 4. **Frontend Setup** 69 | 70 | ```bash 71 | cd client 72 | npm install 73 | ng serve --open 74 | -------------------------------------------------------------------------------- /client/src/app/pages/signup/signup.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { 4 | FormBuilder, 5 | FormControl, 6 | FormGroup, 7 | ReactiveFormsModule, 8 | Validators, 9 | } from '@angular/forms'; 10 | import { RouterLink, Router } from '@angular/router'; 11 | import { UserService } from '../../services/user.service'; 12 | import { Signup } from '../../models/auth'; 13 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 14 | import { GeneralService } from '../../services/general.service'; 15 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 16 | 17 | @Component({ 18 | selector: 'app-signup', 19 | standalone: true, 20 | imports: [CommonModule, ReactiveFormsModule, RouterLink, NzSpinModule], 21 | templateUrl: './signup.component.html', 22 | styleUrl: './signup.component.css', 23 | }) 24 | export class SignupComponent implements OnInit { 25 | processLoading: boolean = false; 26 | form: FormGroup; 27 | constructor( 28 | private fb: FormBuilder, 29 | private userService: UserService, 30 | private router: Router, 31 | private notification: NzNotificationService, 32 | private generalService: GeneralService 33 | ) { 34 | this.form = fb.group({ 35 | name: ['', [Validators.required, Validators.minLength(4)]], 36 | email: ['', [Validators.required, Validators.email]], 37 | password: ['', [Validators.required, Validators.minLength(6)]], 38 | }); 39 | } 40 | 41 | ngOnInit(): void {} 42 | 43 | signup() { 44 | this.form.markAllAsTouched(); 45 | this.form.markAsDirty(); 46 | if (!this.form.valid) { 47 | this.notification.create( 48 | 'error', 49 | 'error', 50 | 'please check fields and try again' 51 | ); 52 | return; 53 | } 54 | 55 | this.processLoading = true; 56 | let data = new Signup(); 57 | data = { ...data, ...this.form.value }; 58 | this.userService.signUp(data).subscribe({ 59 | next: (res: any) => { 60 | this.processLoading = false; 61 | this.generalService.saveUser(res); 62 | this.notification.create( 63 | 'success', 64 | 'Success', 65 | 'Registeration Successfull sign in to continue' 66 | ); 67 | this.router.navigateByUrl('/signin'); 68 | }, 69 | error: (error: any) => { 70 | this.processLoading = false; 71 | this.notification.create('error', 'error', 'an error occured'); 72 | }, 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client/src/app/modals/create-review/create-review.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { 4 | FormBuilder, 5 | FormGroup, 6 | ReactiveFormsModule, 7 | Validators, 8 | } from '@angular/forms'; 9 | import { Router, RouterLink } from '@angular/router'; 10 | import { NzModalModule } from 'ng-zorro-antd/modal'; 11 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 12 | import { systematicReview } from '../../models/research'; 13 | import { ResearchService } from '../../services/research.service'; 14 | import { UserService } from '../../services/user.service'; 15 | @Component({ 16 | selector: 'app-create-review', 17 | standalone: true, 18 | imports: [CommonModule, ReactiveFormsModule, RouterLink, NzModalModule], 19 | templateUrl: './create-review.component.html', 20 | styleUrl: './create-review.component.css', 21 | }) 22 | export class CreateReviewComponent { 23 | isVisible = false; 24 | form: FormGroup; 25 | constructor( 26 | private fb: FormBuilder, 27 | private notification: NzNotificationService, 28 | private researchService: ResearchService, 29 | private router: Router, 30 | private userService: UserService 31 | ) { 32 | this.form = fb.group({ 33 | title: ['', Validators.required], 34 | description: ['', Validators.required], 35 | }); 36 | } 37 | 38 | showModal(): void { 39 | this.isVisible = true; 40 | } 41 | 42 | handleOk(): void { 43 | this.form.markAllAsTouched(); 44 | this.form.markAsDirty(); 45 | if (!this.form.valid) { 46 | this.notification.create( 47 | 'error', 48 | 'error', 49 | 'please check fields and try again' 50 | ); 51 | return; 52 | } 53 | let data = new systematicReview(); 54 | const user = this.userService.getUser(); 55 | data = { ...data, ...this.form.value, user: user.id }; 56 | this.researchService.createResearch(data).subscribe({ 57 | next: (res: any) => { 58 | this.notification.create( 59 | 'success', 60 | 'Success', 61 | 'You have successfully created a systematic review' 62 | ); 63 | this.isVisible = false; 64 | this.researchService.setSelectedResearch(res); 65 | this.router.navigateByUrl(`dashboard/research/${res.id}`); 66 | }, 67 | error: (error: any) => { 68 | this.notification.create('error', 'error', 'an error occured'); 69 | }, 70 | }); 71 | } 72 | 73 | handleCancel(): void { 74 | this.isVisible = false; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { 3 | ActivatedRoute, 4 | RouterLink, 5 | RouterLinkActive, 6 | RouterOutlet, 7 | Router, 8 | } from '@angular/router'; 9 | import { NzIconModule } from 'ng-zorro-antd/icon'; 10 | import { UserService } from '../../services/user.service'; 11 | import { GeneralService } from '../../services/general.service'; 12 | import { Location } from '@angular/common'; 13 | import { firstValueFrom } from 'rxjs'; 14 | import { CreateReviewComponent } from '../../modals/create-review/create-review.component'; 15 | import { ResearchService } from '../../services/research.service'; 16 | import { NzAvatarModule } from 'ng-zorro-antd/avatar'; 17 | import { NzDropDownModule } from 'ng-zorro-antd/dropdown'; 18 | 19 | @Component({ 20 | selector: 'app-dashboard', 21 | standalone: true, 22 | imports: [ 23 | RouterLink, 24 | RouterLinkActive, 25 | RouterOutlet, 26 | NzIconModule, 27 | CreateReviewComponent, 28 | NzAvatarModule, 29 | NzDropDownModule 30 | ], 31 | templateUrl: './dashboard.component.html', 32 | styleUrl: './dashboard.component.css', 33 | }) 34 | export class DashboardComponent implements OnInit { 35 | @ViewChild(CreateReviewComponent, { static: false }) 36 | createModal!: CreateReviewComponent; 37 | title: string = ''; 38 | isVisible = false; 39 | researchID = ''; 40 | user: any; 41 | constructor( 42 | private userService: UserService, 43 | private generalService: GeneralService, 44 | private route: ActivatedRoute, 45 | private router: Router, 46 | private location: Location, 47 | private researchService: ResearchService 48 | ) { 49 | this.getTitle(); 50 | this.user = this.userService.getUser() 51 | this.location.onUrlChange(() => { 52 | this.researchService.selectedResearch$.subscribe((research) => { 53 | if (research) { 54 | this.title = research.title; 55 | } else { 56 | this.getTitle(); 57 | } 58 | }); 59 | }); 60 | } 61 | ngOnInit(): void { 62 | this.refreshToken(); 63 | } 64 | 65 | async getTitle() { 66 | this.title = (await firstValueFrom(this.route.children[0].data))['title']; 67 | } 68 | 69 | refreshToken() { 70 | this.userService.refreshToken().subscribe({ 71 | next: (res: any) => { 72 | this.generalService.saveUser(res); 73 | }, 74 | }); 75 | } 76 | logOut() { 77 | this.generalService.logOutUser(); 78 | } 79 | getInitials(name: string): string { 80 | let initials = name.split(' ').map((n) => n[0]).join('').toUpperCase(); 81 | return initials; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /controllers/chatController.js: -------------------------------------------------------------------------------- 1 | import PrimaryStudy from '../models/PrimaryStudies.js'; 2 | import SystematicReview from '../models/SystematicReview.js'; 3 | import ResearchPapers from '../models/ResearchPapers.js'; 4 | import Chat from '../models/Chat.js'; 5 | import { StatusCodes } from 'http-status-codes'; 6 | import dotenv from 'dotenv'; 7 | import OpenAI from 'openai'; 8 | import { assistantChat } from '../utils/openAiRequest.js'; 9 | import UnAuthenticatedError from '../errors/unauthenticated.js'; 10 | 11 | dotenv.config(); 12 | 13 | const openai = new OpenAI({ 14 | apiKey: process.env.OPEN_API_SECRET_KEY, 15 | }); 16 | 17 | const Startchat = async (req, res) => { 18 | const { userQuestion} = req.body; 19 | const systematicReviewId = req.params.id; 20 | 21 | try { 22 | const systematicReview = await SystematicReview.findOne({_id: systematicReviewId}); 23 | if (!systematicReview) { 24 | throw new UnAuthenticatedError('Systematic review not found'); 25 | } 26 | 27 | const assistantId = systematicReview.chatAssistantId; 28 | const researchPapers = await PrimaryStudy.find({systematicReviewId: systematicReviewId}); 29 | const existingChat = await Chat.findOne({systematicReviewId: systematicReviewId}); 30 | let chat; 31 | if (existingChat) { 32 | chat = existingChat; 33 | } else { 34 | const thread = await openai.beta.threads.create(); 35 | chat = await Chat.create({ 36 | chatAssistantId: assistantId, 37 | threadId: thread.id, 38 | user: req.user.userId, 39 | systematicReviewId: systematicReviewId 40 | }); 41 | } 42 | const threadId = chat.threadId 43 | const chatResponse = await assistantChat(assistantId, researchPapers, userQuestion, threadId); 44 | chat.messages.push({ role: 'user', message: userQuestion }); 45 | chat.messages.push({ role: 'assistant', message: chatResponse }); 46 | await chat.save(); 47 | res.status(StatusCodes.OK).json({'userQuestion' : userQuestion, 'assistant':chatResponse }); 48 | 49 | } catch (error) { 50 | res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: 'Error handling the chat', error: error.message }); 51 | console.log(error); 52 | } 53 | }; 54 | 55 | const getChatHistory = async(req,res) => { 56 | const systematicReviewId = req.params.id; 57 | const chatHistory = await Chat.findOne({systematicReviewId: systematicReviewId}) 58 | if(chatHistory) { 59 | res.status(StatusCodes.OK).json({'messages': chatHistory.messages }); 60 | } 61 | } 62 | export { Startchat, getChatHistory }; 63 | -------------------------------------------------------------------------------- /client/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { guardGuard } from './auth/guard.guard'; 3 | export const routes: Routes = [ 4 | { 5 | path: 'signup', 6 | loadComponent: () => 7 | import('./pages/signup/signup.component').then((c) => c.SignupComponent), 8 | }, 9 | { 10 | path: 'signin', 11 | loadComponent: () => 12 | import('./pages/signin/signin.component').then((c) => c.SigninComponent), 13 | }, 14 | 15 | { 16 | path: 'dashboard', 17 | canActivate: [guardGuard], 18 | loadComponent: () => 19 | import('./pages/dashboard/dashboard.component').then( 20 | (c) => c.DashboardComponent 21 | ), 22 | children: [ 23 | { 24 | path: '', 25 | data: { 26 | title: 'Reviews', 27 | }, 28 | loadComponent: () => 29 | import('./pages/dashboard/home/home.component').then( 30 | (c) => c.HomeComponent 31 | ), 32 | }, 33 | { 34 | path: 'research', 35 | data: { 36 | title: 'Systematic Reviews', 37 | }, 38 | loadComponent: () => 39 | import('./pages/dashboard/research/research.component').then( 40 | (c) => c.ResearchComponent 41 | ), 42 | children: [ 43 | { 44 | path: '', 45 | loadComponent: () => 46 | import('./pages/dashboard/research/home/home.component').then( 47 | (c) => c.HomeComponent 48 | ), 49 | }, 50 | { 51 | path: ':id', 52 | loadComponent: () => 53 | import( 54 | './pages/dashboard/research/details/details.component' 55 | ).then((c) => c.DetailsComponent), 56 | }, 57 | ], 58 | }, 59 | { 60 | path: 'documents', 61 | data: { 62 | title: 'Documents', 63 | }, 64 | loadComponent: () => 65 | import('./pages/dashboard/documents/documents.component').then( 66 | (c) => c.DocumentsComponent 67 | ), 68 | }, 69 | { 70 | path: 'settings', 71 | data: { 72 | title: 'Settings', 73 | }, 74 | loadComponent: () => 75 | import('./pages/dashboard/settings/settings.component').then( 76 | (c) => c.SettingsComponent 77 | ), 78 | }, 79 | { 80 | path: 'profile', 81 | data: { 82 | title: 'Profile', 83 | }, 84 | loadComponent: () => 85 | import('./pages/dashboard/profile/profile.component').then( 86 | (c) => c.ProfileComponent 87 | ), 88 | }, 89 | ], 90 | }, 91 | { 92 | path: '**', 93 | redirectTo: 'signin', 94 | pathMatch: 'full', 95 | }, 96 | ]; 97 | -------------------------------------------------------------------------------- /client/src/app/modals/edit/edit.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input, SimpleChanges } from '@angular/core'; 3 | import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; 4 | import { Router, RouterLink } from '@angular/router'; 5 | import { NzModalModule } from 'ng-zorro-antd/modal'; 6 | import { ResearchService } from '../../services/research.service'; 7 | import { systematicReview } from '../../models/research'; 8 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 9 | import { UserService } from '../../services/user.service'; 10 | 11 | @Component({ 12 | selector: 'app-edit', 13 | standalone: true, 14 | imports: [CommonModule, ReactiveFormsModule, RouterLink, NzModalModule], 15 | templateUrl: './edit.component.html', 16 | styleUrl: './edit.component.css' 17 | }) 18 | export class EditComponent { 19 | processLoading: boolean = false; 20 | form: FormGroup; 21 | isVisible = false; 22 | researchID = '' 23 | constructor( private fb: FormBuilder, private researchService : ResearchService, private notification : NzNotificationService, private userService: UserService, private router: Router){ 24 | this.form = fb.group({ 25 | title: ['', Validators.required], 26 | description: ['', Validators.required], 27 | }); 28 | } 29 | 30 | 31 | ngOnInit(): void { 32 | this.researchService.selectedResearch$.subscribe((research) => { 33 | if (research) { 34 | this.researchID = research._id 35 | this.form.patchValue({ 36 | title: research.title, 37 | description: research.description, 38 | }); 39 | } 40 | }); 41 | } 42 | 43 | showModal(): void { 44 | this.isVisible = true; 45 | } 46 | 47 | handleOk(): void { 48 | this.form.markAllAsTouched(); 49 | this.form.markAsDirty(); 50 | if (!this.form.valid) { 51 | this.notification.create( 52 | 'error', 53 | 'error', 54 | 'please check fields and try again' 55 | ); 56 | return; 57 | } 58 | let data = new systematicReview(); 59 | const user = this.userService.getUser() 60 | data = { ...data, ...this.form.value, "user":user.id }; 61 | this.researchService.updateResearch(this.researchID, data).subscribe({ 62 | next: (res:any) => { 63 | this.notification.create( 64 | 'success', 65 | 'Success', 66 | 'Systematic Review was successfully Edited' 67 | ); 68 | this.isVisible = false; 69 | this.router.navigateByUrl(`dashboard/research/${res.id}`); 70 | }, 71 | error: (error: any) => { 72 | this.notification.create('error', 'error', 'an error occured'); 73 | }, 74 | }) 75 | } 76 | 77 | handleCancel(): void { 78 | this.isVisible = false; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/app/components/chatbox/chatbox.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; 3 | import { ActivatedRoute, RouterLink } from '@angular/router'; 4 | import { NzDrawerModule } from 'ng-zorro-antd/drawer'; 5 | import { NzIconModule } from 'ng-zorro-antd/icon'; 6 | import { UserService } from '../../services/user.service'; 7 | import { ChatService } from '../../services/chat.service'; 8 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 9 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 10 | import { CommonModule } from '@angular/common'; 11 | 12 | @Component({ 13 | standalone: true, 14 | selector: 'app-chatbox', 15 | templateUrl: './chatbox.component.html', 16 | imports: [NzDrawerModule, NzIconModule, RouterLink, ReactiveFormsModule, NzSpinModule, CommonModule], 17 | }) 18 | export class ChatboxComponent { 19 | processLoading: boolean = false; 20 | form: FormGroup 21 | chatHistory: {role: string, message: string}[] = []; 22 | @Input() research: any; 23 | researchId: string = ''; 24 | researchTitle: string = ''; 25 | visible = false; 26 | constructor( 27 | private fb: FormBuilder, 28 | private userService: UserService, 29 | private chatService : ChatService, 30 | private notification : NzNotificationService, 31 | private route : ActivatedRoute 32 | ) { 33 | this.form = fb.group({ 34 | userQuestion: ['', [Validators.required]], 35 | }) 36 | } 37 | ngOnInit() { 38 | this.getChatHistory(); 39 | } 40 | open(): void { 41 | this.visible = true; 42 | this.researchTitle = this.research.title 43 | } 44 | 45 | close(): void { 46 | this.visible = false; 47 | } 48 | getChatHistory() { 49 | const id = this.route.snapshot.params['id'] 50 | this.chatService.getChataHistory(id).subscribe({ 51 | next: (res:any) => { 52 | this.chatHistory = res.messages; 53 | } 54 | }) 55 | } 56 | 57 | startChat() { 58 | this.form.markAllAsTouched(); 59 | this.form.markAsDirty(); 60 | if (this.form.invalid) { 61 | this.notification.create('error', 'Error', 'Please enter a message'); 62 | return; 63 | } 64 | this.processLoading = true; 65 | const userQuestion = this.form.value.userQuestion; 66 | const id = this.route.snapshot.params['id']; 67 | this.chatService.startChat({ userQuestion }, id).subscribe({ 68 | next: (res: any) => { 69 | this.chatHistory.push({ role: 'assistant', message: res.assistant }); 70 | this.processLoading = false; 71 | }, 72 | error: () => { 73 | this.notification.create('error', 'Error', 'Failed to send message'); 74 | this.processLoading = false; 75 | } 76 | }); 77 | this.chatHistory.push({ role: 'user', message: userQuestion }); 78 | this.form.reset(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { ResearchService } from '../../../../services/research.service'; 3 | import { CommonModule } from '@angular/common'; 4 | import { Router, RouterLink } from '@angular/router'; 5 | import { NzIconModule } from 'ng-zorro-antd/icon'; 6 | import { EditComponent } from '../../../../modals/edit/edit.component'; 7 | import { NzModalModule, NzModalService } from 'ng-zorro-antd/modal'; 8 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 9 | import { EmptyComponent } from '../../../../components/empty/empty.component'; 10 | import { CreateReviewComponent } from '../../../../modals/create-review/create-review.component'; 11 | 12 | @Component({ 13 | selector: 'app-home', 14 | standalone: true, 15 | imports: [ 16 | CommonModule, 17 | RouterLink, 18 | NzIconModule, 19 | EditComponent, 20 | NzModalModule, 21 | EmptyComponent, 22 | CreateReviewComponent, 23 | ], 24 | templateUrl: './home.component.html', 25 | styleUrl: './home.component.css', 26 | }) 27 | export class HomeComponent { 28 | @ViewChild(EditComponent, { static: false }) editModal!: EditComponent; 29 | @ViewChild(CreateReviewComponent, { static: false }) 30 | createModal!: CreateReviewComponent; 31 | isVisible = false; 32 | researchs: Array = []; 33 | researchID = ''; 34 | constructor( 35 | private researchService: ResearchService, 36 | private modal: NzModalService, 37 | private notification: NzNotificationService, 38 | private router: Router 39 | ) { 40 | this.getAllResearch(); 41 | this.researchService.clearSelectedResearch(); 42 | } 43 | 44 | selectResearchForEdit(research: any): void { 45 | this.researchService.setSelectedResearch(research); 46 | this.showEditModal(); 47 | } 48 | 49 | selectResearch(research: any): void { 50 | this.researchService.setSelectedResearch(research); 51 | } 52 | 53 | showEditModal(): void { 54 | this.editModal.showModal(); 55 | } 56 | getAllResearch() { 57 | this.researchService.getAllResearch().subscribe({ 58 | next: (res: any) => { 59 | this.researchs = res.data; 60 | }, 61 | }); 62 | } 63 | 64 | showDeleteConfirm(researchID: any): void { 65 | this.researchID = researchID; 66 | this.modal.confirm({ 67 | nzTitle: 'Are you sure you want to delete this review ?', 68 | nzOkText: 'Delete', 69 | nzOkDanger: true, 70 | nzOnOk: () => { 71 | this.researchService.deleteResearch(this.researchID).subscribe({ 72 | next: (res: any) => { 73 | this.notification.create( 74 | 'success', 75 | 'Success', 76 | 'Review was successfully deleted' 77 | ); 78 | this.getAllResearch(); 79 | }, 80 | }); 81 | }, 82 | nzCancelText: 'Cancel', 83 | }); 84 | } 85 | 86 | showCreateReviewModal() { 87 | this.createModal.showModal(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Reviews

3 |
4 | 5 | Create new review 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 | 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | {{ research.title }} 35 |
36 |
37 | {{ research.updatedAt | date: 'h:mma MMM d' }} 38 | 39 | 40 |
    41 |
  • 42 | View Review 43 |
  • 44 |
  • 45 | Delete Review 46 |
  • 47 |
  • 48 | Edit Review 49 |
  • 50 |
51 |
52 |
53 |
54 | 55 |
56 | 63 |
64 |
65 |
-------------------------------------------------------------------------------- /client/src/app/components/forms/create-query/create-query.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, EventEmitter, Output } from '@angular/core'; 3 | import { 4 | FormBuilder, 5 | FormGroup, 6 | ReactiveFormsModule, 7 | Validators, 8 | } from '@angular/forms'; 9 | import { ActivatedRoute } from '@angular/router'; 10 | import { NzModalModule } from 'ng-zorro-antd/modal'; 11 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 12 | import { filterQuery } from '../../../models/research'; 13 | import { ResearchService } from '../../../services/research.service'; 14 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 15 | 16 | @Component({ 17 | selector: 'app-create-query', 18 | standalone: true, 19 | imports: [ 20 | CommonModule, 21 | ReactiveFormsModule, 22 | NzModalModule, 23 | NzSpinModule, 24 | ], 25 | templateUrl: './create-query.component.html', 26 | styleUrl: './create-query.component.css', 27 | }) 28 | export class CreateQueryComponent { 29 | @Output() queryCreated = new EventEmitter(); 30 | @Output() cancel = new EventEmitter(); 31 | 32 | processLoading: boolean = false; 33 | form: FormGroup; 34 | isSpinning = false; 35 | researchId = ''; 36 | 37 | constructor( 38 | private fb: FormBuilder, 39 | private notification: NzNotificationService, 40 | private researchService: ResearchService, 41 | private activeRoute: ActivatedRoute 42 | ) { 43 | this.form = fb.group({ 44 | searchString: ['', Validators.required], 45 | researchQuestion: ['', Validators.required], 46 | inclusionCriteria: ['', Validators.required], 47 | exclusionCriteria: ['', Validators.required], 48 | startYear: ['', [Validators.required, Validators.pattern('^[0-9]{4}$')]], 49 | endYear: ['', [Validators.required, Validators.pattern('^[0-9]{4}$')]], 50 | maxResearch: ['', [Validators.required, Validators.min(1)]] 51 | }); 52 | this.researchId = this.activeRoute.snapshot.params['id']; 53 | } 54 | 55 | submitForm(): void { 56 | this.form.markAllAsTouched(); 57 | this.form.markAsDirty(); 58 | 59 | if (!this.form.valid) { 60 | this.notification.create( 61 | 'error', 62 | 'Error', 63 | 'Please check fields and try again' 64 | ); 65 | return; 66 | } 67 | 68 | let data = new filterQuery(); 69 | data = { ...data, ...this.form.value, systematicReviewId: this.researchId }; 70 | 71 | this.isSpinning = true; 72 | this.researchService.createQuery(data).subscribe({ 73 | next: () => { 74 | this.notification.create( 75 | 'success', 76 | 'Success', 77 | 'You have successfully created a filter query' 78 | ); 79 | this.isSpinning = false; 80 | this.queryCreated.emit(); // Notify parent component 81 | }, 82 | error: () => { 83 | this.isSpinning = false; 84 | this.notification.create('error', 'Error', 'An error occurred'); 85 | }, 86 | }); 87 | } 88 | 89 | cancelForm(): void { 90 | this.cancel.emit(); // Notify parent to close form 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /client/src/app/modals/create-query/create-query.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { 4 | FormBuilder, 5 | FormGroup, 6 | ReactiveFormsModule, 7 | Validators, 8 | } from '@angular/forms'; 9 | import { ActivatedRoute, RouterLink } from '@angular/router'; 10 | import { NzModalModule } from 'ng-zorro-antd/modal'; 11 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 12 | import { filterQuery } from '../../models/research'; 13 | import { ResearchService } from '../../services/research.service'; 14 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 15 | 16 | @Component({ 17 | selector: 'app-create-query', 18 | standalone: true, 19 | imports: [ 20 | CommonModule, 21 | ReactiveFormsModule, 22 | RouterLink, 23 | NzModalModule, 24 | NzSpinModule, 25 | ], 26 | templateUrl: './create-query.component.html', 27 | styleUrl: './create-query.component.css', 28 | }) 29 | export class CreateQueryComponent { 30 | processLoading: boolean = false; 31 | form: FormGroup; 32 | isVisible = false; 33 | isSpinning = false; 34 | researchId = ''; 35 | filterQuery: Array = []; 36 | constructor( 37 | private fb: FormBuilder, 38 | private notification: NzNotificationService, 39 | private researchService: ResearchService, 40 | private activeRoute: ActivatedRoute 41 | ) { 42 | this.form = fb.group({ 43 | searchString: ['', Validators.required], 44 | researchQuestion: ['', Validators.required], 45 | inclusionCriteria: ['', Validators.required], 46 | exclusionCriteria: ['', Validators.required], 47 | startYear: ['', Validators.required], 48 | endYear: ['', Validators.required], 49 | maxResearch: ['', Validators.required] 50 | }); 51 | this.researchId = this.activeRoute.snapshot.params['id']; 52 | } 53 | showModal(): void { 54 | this.isVisible = true; 55 | } 56 | 57 | handleOk(): void { 58 | this.form.markAllAsTouched(); 59 | this.form.markAsDirty(); 60 | if (!this.form.valid) { 61 | this.notification.create( 62 | 'error', 63 | 'error', 64 | 'please check fields and try again' 65 | ); 66 | return; 67 | } 68 | let data = new filterQuery(); 69 | data = { ...data, ...this.form.value, systematicReviewId: this.researchId }; 70 | this.isSpinning = true; 71 | this.researchService.createQuery(data).subscribe({ 72 | next: (res: any) => { 73 | this.notification.create( 74 | 'success', 75 | 'Success', 76 | 'You have successfully created a filter query' 77 | ); 78 | this.isVisible = false; 79 | this.getAllQuery(); 80 | window.location.reload(); 81 | }, 82 | error: (error: any) => { 83 | this.isSpinning = false; 84 | this.notification.create('error', 'error', 'an error occured'); 85 | }, 86 | }); 87 | } 88 | 89 | handleCancel(): void { 90 | this.isVisible = false; 91 | } 92 | 93 | getAllQuery() { 94 | this.researchService.getAllQuery(this.researchId).subscribe({ 95 | next: (res: any) => { 96 | this.filterQuery = res.data; 97 | console.log(this.filterQuery); 98 | }, 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "client-v2": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:application", 15 | "options": { 16 | "outputPath": "dist/client-v2", 17 | "index": "src/index.html", 18 | "browser": "src/main.ts", 19 | "polyfills": [ 20 | "zone.js" 21 | ], 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": [ 24 | "src/favicon.ico", 25 | "src/assets", 26 | { 27 | "glob": "**/*", 28 | "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/", 29 | "output": "/assets/" 30 | } 31 | ], 32 | "styles": [ 33 | "src/theme.less", 34 | "src/styles.css" 35 | ], 36 | "scripts": [] 37 | }, 38 | "configurations": { 39 | "production": { 40 | "budgets": [ 41 | { 42 | "type": "initial", 43 | "maximumWarning": "500kb", 44 | "maximumError": "1mb" 45 | }, 46 | { 47 | "type": "anyComponentStyle", 48 | "maximumWarning": "2kb", 49 | "maximumError": "4kb" 50 | } 51 | ], 52 | "outputHashing": "all" 53 | }, 54 | "development": { 55 | "optimization": false, 56 | "extractLicenses": false, 57 | "sourceMap": true 58 | } 59 | }, 60 | "defaultConfiguration": "production" 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "configurations": { 65 | "production": { 66 | "buildTarget": "client-v2:build:production" 67 | }, 68 | "development": { 69 | "buildTarget": "client-v2:build:development" 70 | } 71 | }, 72 | "defaultConfiguration": "development" 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "buildTarget": "client-v2:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "polyfills": [ 84 | "zone.js", 85 | "zone.js/testing" 86 | ], 87 | "tsConfig": "tsconfig.spec.json", 88 | "assets": [ 89 | "src/favicon.ico", 90 | "src/assets" 91 | ], 92 | "styles": [ 93 | "src/styles.css" 94 | ], 95 | "scripts": [] 96 | } 97 | } 98 | } 99 | } 100 | }, 101 | "cli": { 102 | "analytics": "e0f091d2-e8af-40a1-acf1-7af7d2e98cfc" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { ResearchService } from '../../../services/research.service'; 3 | import { CommonModule } from '@angular/common'; 4 | import { Router, RouterLink } from '@angular/router'; 5 | import { NzIconModule } from 'ng-zorro-antd/icon'; 6 | import { NzModalModule, NzModalService } from 'ng-zorro-antd/modal'; 7 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 8 | import { EmptyComponent } from '../../../components/empty/empty.component'; 9 | import { NzDropDownModule } from 'ng-zorro-antd/dropdown'; 10 | import { NzSkeletonModule } from 'ng-zorro-antd/skeleton'; 11 | import { ReviewFormComponent } from '../../../components/forms/review-form/review-form.component'; 12 | 13 | @Component({ 14 | selector: 'app-home', 15 | standalone: true, 16 | imports: [ 17 | CommonModule, 18 | RouterLink, 19 | NzIconModule, 20 | NzModalModule, 21 | EmptyComponent, 22 | NzDropDownModule, 23 | NzSkeletonModule, 24 | ReviewFormComponent, 25 | ], 26 | templateUrl: './home.component.html', 27 | styleUrl: './home.component.css', 28 | }) 29 | export class HomeComponent { 30 | researchs: Array = []; 31 | researchID = ''; 32 | loading = true; 33 | showCreateForm = false; 34 | editingResearch: any = null; 35 | 36 | constructor( 37 | private researchService: ResearchService, 38 | private modal: NzModalService, 39 | private notification: NzNotificationService, 40 | private router: Router 41 | ) { 42 | this.getAllResearch(); 43 | this.researchService.clearSelectedResearch(); 44 | } 45 | 46 | // Fetch all research data 47 | getAllResearch() { 48 | this.loading = true; 49 | this.researchService.getAllResearch().subscribe({ 50 | next: (res: any) => { 51 | this.researchs = res.data; 52 | this.loading = false; 53 | }, 54 | error: () => { 55 | this.loading = false; 56 | }, 57 | }); 58 | } 59 | 60 | // Select a research for editing 61 | selectResearchForEdit(research: any): void { 62 | this.editingResearch = research; 63 | } 64 | 65 | // Select a research for viewing 66 | selectResearch(research: any): void { 67 | this.researchService.setSelectedResearch(research); 68 | } 69 | 70 | // Show delete confirmation modal 71 | showDeleteConfirm(researchID: any): void { 72 | this.researchID = researchID; 73 | this.modal.confirm({ 74 | nzTitle: 'Are you sure you want to delete this review?', 75 | nzOkText: 'Delete', 76 | nzOkDanger: true, 77 | nzOnOk: () => { 78 | this.researchService.deleteResearch(this.researchID).subscribe({ 79 | next: () => { 80 | this.notification.create('success', 'Success', 'Review was successfully deleted'); 81 | this.getAllResearch(); 82 | }, 83 | }); 84 | }, 85 | nzCancelText: 'Cancel', 86 | }); 87 | } 88 | 89 | toggleCreateForm(): void { 90 | this.showCreateForm = !this.showCreateForm; 91 | this.editingResearch = null; 92 | } 93 | 94 | toggleEditForm(research: any): void { 95 | this.editingResearch = this.editingResearch === research ? null : research; 96 | this.showCreateForm = false; 97 | } 98 | 99 | handleFormSubmitted(): void { 100 | this.getAllResearch(); 101 | this.showCreateForm = false; 102 | this.editingResearch = null; 103 | } 104 | } -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/dashboard.component.css: -------------------------------------------------------------------------------- 1 | .dashboard { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | } 5 | .dashboard-page { 6 | max-width: 80rem; 7 | width: 80%; 8 | margin: 0 auto; 9 | padding: 2rem 0; 10 | } 11 | nav { 12 | height: var(--nav-height); 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | box-shadow: 0 1px 0px 0px rgba(0, 0, 0, 0.1); 17 | } 18 | .logo { 19 | display: flex; 20 | align-items: center; 21 | width: 100px; 22 | } 23 | .nav-center { 24 | display: flex; 25 | width: 90vw; 26 | align-items: center; 27 | justify-content: space-between; 28 | .btn.btn-add-new { 29 | padding: 1rem; 30 | } 31 | } 32 | .toggle-btn { 33 | background: transparent; 34 | border-color: transparent; 35 | font-size: 1.75rem; 36 | color: var(--primary-500); 37 | cursor: pointer; 38 | display: flex; 39 | align-items: center; 40 | } 41 | .left-hand.flex { 42 | display: flex; 43 | gap: 1rem; 44 | align-items: center; 45 | } 46 | .btn-container { 47 | position: relative; 48 | } 49 | .btn { 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | gap: 0 0.5rem; 54 | position: relative; 55 | box-shadow: var(--shadow-2); 56 | max-width: fit-content; 57 | } 58 | .btn-signout { 59 | background-color: transparent; 60 | border: none; 61 | cursor: pointer; 62 | font-size: 1rem; 63 | } 64 | .dropdown { 65 | position: absolute; 66 | top: 40px; 67 | left: 0; 68 | width: 100%; 69 | background: var(--primary-100); 70 | box-shadow: var(--shadow-2); 71 | padding: 0.5rem; 72 | text-align: center; 73 | visibility: hidden; 74 | border-radius: var(--borderRadius); 75 | } 76 | .show-dropdown { 77 | visibility: visible; 78 | } 79 | .dropdown-btn { 80 | background: transparent; 81 | border-color: transparent; 82 | color: var(--primary-500); 83 | letter-spacing: var(--letterSpacing); 84 | text-transform: capitalize; 85 | cursor: pointer; 86 | } 87 | .logo-text { 88 | display: none; 89 | margin: 0; 90 | } 91 | .user-details .detail { 92 | display: flex; 93 | gap: 1rem; 94 | align-items: center; 95 | } 96 | @media (min-width: 992px) { 97 | .nav-center { 98 | width: 90%; 99 | } 100 | .logo { 101 | display: none; 102 | } 103 | .logo-text { 104 | display: block; 105 | } 106 | aside { 107 | display: block; 108 | background-color: var(--white); 109 | } 110 | .content { 111 | position: sticky; 112 | top: 0; 113 | } 114 | .show-sidebar { 115 | margin-left: 0; 116 | } 117 | header { 118 | height: 6rem; 119 | display: flex; 120 | align-items: center; 121 | padding-left: 2.5rem; 122 | } 123 | .nav-links { 124 | padding-top: 2rem; 125 | display: flex; 126 | flex-direction: column; 127 | gap: 1.5rem; 128 | padding-inline: 1.5rem; 129 | } 130 | .nav-link { 131 | display: flex; 132 | align-items: center; 133 | gap: .5rem; 134 | color: var(--text-secondary-color); 135 | padding: 1rem 0; 136 | text-transform: capitalize; 137 | transition: padding-left 0.3s ease-in-out; 138 | } 139 | .nav-link:hover { 140 | color: var(--primary-500); 141 | transition: var(--transition); 142 | } 143 | .icon { 144 | font-size: 1rem; 145 | display: grid; 146 | place-items: center; 147 | } 148 | .active { 149 | color: var(--primary-600); 150 | background-color: var(--backgroundColor); 151 | border-radius: var(--borderRadius); 152 | } 153 | .pending { 154 | background: var(--background-color); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /client/src/app/modals/create-query/create-query.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 | 11 | 22 |
23 |
24 | 25 | 36 |
37 |
38 | 39 | 43 |
44 |
45 | 46 | 50 |
51 |
52 | 55 | 65 |
66 |
67 | 70 | 80 |
81 |
82 | 85 | 95 |
96 |
97 | 98 |
99 |
100 | -------------------------------------------------------------------------------- /client/src/app/components/forms/review-form/review-form.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 3 | import { 4 | FormBuilder, 5 | FormGroup, 6 | ReactiveFormsModule, 7 | Validators, 8 | } from '@angular/forms'; 9 | import { Router } from '@angular/router'; 10 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 11 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 12 | import { systematicReview } from '../../../models/research'; 13 | import { ResearchService } from '../../../services/research.service'; 14 | import { UserService } from '../../../services/user.service'; 15 | 16 | @Component({ 17 | selector: 'app-review-form', 18 | standalone: true, 19 | imports: [CommonModule, ReactiveFormsModule, NzSpinModule], 20 | templateUrl: './review-form.component.html', 21 | styleUrl: './review-form.component.css', 22 | }) 23 | export class ReviewFormComponent { 24 | @Input() isEditMode: boolean = false; 25 | @Input() reviewToEdit: any = null; 26 | @Output() formSubmitted = new EventEmitter(); 27 | @Output() formCanceled = new EventEmitter(); 28 | 29 | form: FormGroup; 30 | processLoading: boolean = false; 31 | 32 | constructor( 33 | private fb: FormBuilder, 34 | private notification: NzNotificationService, 35 | private researchService: ResearchService, 36 | private userService: UserService, 37 | private router: Router 38 | ) { 39 | this.form = this.fb.group({ 40 | title: ['', Validators.required], 41 | description: ['', Validators.required], 42 | }); 43 | } 44 | 45 | ngOnInit(): void { 46 | if (this.isEditMode && this.reviewToEdit) { 47 | this.form.patchValue({ 48 | title: this.reviewToEdit.title, 49 | description: this.reviewToEdit.description, 50 | }); 51 | } 52 | } 53 | 54 | handleSubmit(): void { 55 | this.form.markAllAsTouched(); 56 | if (!this.form.valid) { 57 | this.notification.create( 58 | 'error', 59 | 'Error', 60 | 'Please check fields and try again' 61 | ); 62 | return; 63 | } 64 | 65 | this.processLoading = true; 66 | 67 | const data = new systematicReview(); 68 | const user = this.userService.getUser(); 69 | data.title = this.form.value.title; 70 | data.description = this.form.value.description; 71 | data.user = user.id; 72 | 73 | if (this.isEditMode && this.reviewToEdit) { 74 | // Update existing review 75 | this.researchService.updateResearch(this.reviewToEdit._id, data).subscribe({ 76 | next: (res: any) => { 77 | this.notification.create( 78 | 'success', 79 | 'Success', 80 | 'Review updated successfully' 81 | ); 82 | this.processLoading = false; 83 | this.formSubmitted.emit(); 84 | this.router.navigateByUrl(`dashboard/research/${res.id}`); 85 | }, 86 | error: (error: any) => { 87 | this.notification.create('error', 'Error', 'An error occurred'); 88 | this.processLoading = false; 89 | }, 90 | }); 91 | } else { 92 | // Create new review 93 | this.researchService.createResearch(data).subscribe({ 94 | next: (res: any) => { 95 | this.notification.create( 96 | 'success', 97 | 'Success', 98 | 'Review created successfully' 99 | ); 100 | this.processLoading = false; 101 | this.formSubmitted.emit(); 102 | this.router.navigateByUrl(`dashboard/research/${res.id}`); 103 | }, 104 | error: (error: any) => { 105 | this.notification.create('error', 'Error', 'An error occurred'); 106 | this.processLoading = false; 107 | }, 108 | }); 109 | } 110 | } 111 | 112 | handleCancel(): void { 113 | this.formCanceled.emit(); 114 | this.form.reset(); 115 | } 116 | } -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/details/details.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 | 45 | 46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 | 58 |

59 | {{ truncateText(primaryStudy.abstract, i) }} 60 | 61 | {{ expandedAbstracts[i] ? "Show Less" : "Read More" }} 62 | 63 |

64 |

65 | {{ author.name ?? "Author not found" }}, 66 |

67 | 75 |
76 |
77 |
78 |
79 |
80 |
81 | 84 |
85 | 86 | -------------------------------------------------------------------------------- /client/src/app/components/forms/create-query/create-query.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 16 |
17 |
18 |
19 | 20 | 31 |
32 | 33 |
34 | 35 | 46 |
47 | 48 |
49 | 50 | 61 |
62 |
63 |
64 | 65 | 74 |
75 | 76 |
77 | 78 | 87 |
88 | 89 |
90 | 91 | 100 |
101 | 102 | 103 |
104 | 108 | 109 |
110 |
111 |
112 | -------------------------------------------------------------------------------- /utils/openAiRequest.js: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | 5 | const openai = new OpenAI({ 6 | apiKey: process.env.OPEN_API_SECRET_KEY, 7 | }); 8 | 9 | const createResearchAssistant = async () => { 10 | const assistant = await openai.beta.assistants.create({ 11 | name: "Research Paper Filter", 12 | description: "An assistant to filter research papers based on specific criteria such as inclusion and exclusion criteria.", 13 | instructions: "You are an academic researcher You will be provided with a research paper, containing the fields: abstract, title, id, referenceCount, citationCount, year, openAccessPdf, and author. You will also be Given specific inclusion criteria, exclusion criteria, and a research question, identify and return Yes if the paper meets the inclusion criteria, exclusion criteria and research question and no if it does not meet the inclusion criteria, exclusion criteria and the research queustion", 14 | model: "gpt-4-turbo", 15 | tools: [{ type: "code_interpreter" }], 16 | }); 17 | return assistant.id 18 | } 19 | const createChatAssistant = async () => { 20 | const assistant = await openai.beta.assistants.create({ 21 | name: "research Chat assistant", 22 | description: "An assistant that answers question based on an array of research papers", 23 | instructions: "you are an academic researcher You will be provided with a JavaScript array of research papers, each represented as an object containing the fields: abstract, title, id, referenceCount, citationCount, year, openAccessPdf, and author. You will be required to answer questions based on the research papers. Strictly answer Question based on the research papers provided", 24 | model: "gpt-4-turbo", 25 | }) 26 | return assistant.id 27 | } 28 | const processResearchPapers = async (assistantId, filteredPaper, inclusionCriteria, exclusionCriteria, researchQuestion) => { 29 | const thread = await openai.beta.threads.create(); 30 | try { 31 | const message = await openai.beta.threads.messages.create( 32 | thread.id, 33 | { 34 | role: "user", 35 | content: `Evaluate this paper based on the following criteria: ${JSON.stringify(filteredPaper)}, Inclusion criteria: ${inclusionCriteria}, Exclusion criteria: ${exclusionCriteria}, Research question: ${researchQuestion}` 36 | } 37 | ); 38 | 39 | let run = await openai.beta.threads.runs.createAndPoll( 40 | thread.id, 41 | { 42 | assistant_id: assistantId, 43 | instructions: "Review the criteria and paper information provided and return 'Yes' if the paper meets all criteria and 'No' if it does not. Ensure no additional explanation is given" 44 | } 45 | ); 46 | if (run.status === 'completed') { 47 | const messages = await openai.beta.threads.messages.list(run.thread_id); 48 | for (const message of messages.data.reverse()) { 49 | if(message.role === 'assistant') { 50 | if (message.content && message.content.length > 0 && message.content[0].type === 'text') { 51 | const result = message.content[0].text.value; 52 | console.log(result) 53 | return result; 54 | } 55 | } 56 | } 57 | } else { 58 | console.log("Run status:", run.status); 59 | return run.status; 60 | } 61 | } catch (error) { 62 | console.error("Failed to process research papers with assistant:", error.message); 63 | return null; 64 | } 65 | } 66 | const assistantChat = async (assistantId, researchPapers, userQuestion, threadId) => { 67 | try { 68 | const formattedPapers = JSON.stringify({ researchPapers }); 69 | const userMessage = await openai.beta.threads.messages.create(threadId, { 70 | role: "user", 71 | content: `Here are some research papers: ${formattedPapers}. Based on these papers, can you answer the question: ${userQuestion}?`, 72 | }); 73 | 74 | const questionTimestamp = new Date(userMessage.created_at); 75 | 76 | let run = await openai.beta.threads.runs.createAndPoll(threadId, { 77 | assistant_id: assistantId, 78 | instructions: "Please answer the question strictly using the information from the research papers provided." 79 | }); 80 | 81 | if (run.status === 'completed') { 82 | const messages = await openai.beta.threads.messages.list(run.thread_id); 83 | const relevantMessages = messages.data.filter(m => 84 | m.role === 'assistant' && new Date(m.created_at) > questionTimestamp 85 | ).reverse(); 86 | for (const message of relevantMessages) { 87 | if (message.content && message.content.length > 0 && message.content[0].type === 'text') { 88 | const result = message.content[0].text.value; 89 | return result; 90 | } 91 | } 92 | } 93 | } catch (error) { 94 | console.error("Error in assistant chat:", error); 95 | return "An error occurred while processing your request."; 96 | } 97 | }; 98 | export {processResearchPapers, createResearchAssistant, createChatAssistant, assistantChat} 99 | -------------------------------------------------------------------------------- /client/src/app/pages/dashboard/research/details/details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { ResearchService } from '../../../../services/research.service'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { NzModalService } from 'ng-zorro-antd/modal'; 5 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 6 | import { CommonModule } from '@angular/common'; 7 | import { NzIconModule } from 'ng-zorro-antd/icon'; 8 | import { NzInputModule } from 'ng-zorro-antd/input'; 9 | import { NzSwitchModule } from 'ng-zorro-antd/switch'; 10 | import { FormsModule } from '@angular/forms'; 11 | import { EmptyComponent } from '../../../../components/empty/empty.component'; 12 | import { ChatboxComponent } from '../../../../components/chatbox/chatbox.component'; 13 | import { NzSkeletonModule } from 'ng-zorro-antd/skeleton'; 14 | import { CreateQueryComponent } from '../../../../components/forms/create-query/create-query.component'; 15 | 16 | @Component({ 17 | selector: 'app-details', 18 | standalone: true, 19 | imports: [ 20 | CommonModule, 21 | NzIconModule, 22 | NzInputModule, 23 | NzSwitchModule, 24 | NzSkeletonModule, 25 | FormsModule, 26 | EmptyComponent, 27 | ChatboxComponent, 28 | CreateQueryComponent 29 | ], 30 | templateUrl: './details.component.html', 31 | styleUrl: './details.component.css', 32 | }) 33 | export class DetailsComponent { 34 | @ViewChild(ChatboxComponent, { static: false }) 35 | openChatBox!: ChatboxComponent; 36 | 37 | research: any; 38 | researchId: string = ''; 39 | filterQueries: Array = []; 40 | primaryStudies: Array = []; 41 | showUnfilteredPapers = false; 42 | loading = false; 43 | expandedQueries: { [key: number]: boolean } = {}; 44 | expandedAbstracts: { [key: number]: boolean } = {}; 45 | showCreateQuery = false; 46 | 47 | constructor( 48 | private route: ActivatedRoute, 49 | private researchService: ResearchService, 50 | private modal: NzModalService, 51 | private notification: NzNotificationService 52 | ) { 53 | this.researchId = this.route.snapshot.params['id']; 54 | this.getResearch(); 55 | this.getAllQuery(); 56 | this.getAllPrimaryStudies(); 57 | } 58 | 59 | getResearch() { 60 | this.loading = true; 61 | this.researchService.getResearch(this.researchId).subscribe({ 62 | next: (res: any) => { 63 | this.research = res; 64 | this.loading = false; 65 | if (res.length === 0) { 66 | this.notification.create('info', 'No Research Found', 'No research matches your criteria.'); 67 | } 68 | }, 69 | error: () => (this.loading = false), 70 | }); 71 | } 72 | 73 | getAllQuery() { 74 | this.researchService.getAllQuery(this.researchId).subscribe({ 75 | next: (res: any) => { 76 | this.filterQueries = res.data || []; 77 | }, 78 | }); 79 | } 80 | 81 | deleteQuery(queryID: any) { 82 | this.modal.confirm({ 83 | nzTitle: 'Are you sure you want to delete this Query?', 84 | nzContent: 'Deleting this query will also remove all associated research papers.', 85 | nzOkText: 'Delete', 86 | nzOkDanger: true, 87 | nzOnOk: () => { 88 | this.researchService.deleteQuery(queryID).subscribe({ 89 | next: () => { 90 | this.notification.create('success', 'Success', 'Query successfully deleted.'); 91 | this.getAllQuery(); 92 | this.getAllPrimaryStudies(); 93 | }, 94 | }); 95 | }, 96 | }); 97 | } 98 | 99 | getAllPrimaryStudies() { 100 | this.loading = true; 101 | this.researchService.getAllPrimaryStudies(this.researchId).subscribe({ 102 | next: (res: any) => { 103 | this.primaryStudies = res.data || []; 104 | this.loading = false; 105 | }, 106 | error: () => (this.loading = false), 107 | }); 108 | } 109 | 110 | getUnfilteredPaper() { 111 | this.loading = true; 112 | this.researchService.getUnfilteredPapers(this.researchId).subscribe({ 113 | next: (res: any) => { 114 | this.primaryStudies = res.data || []; 115 | this.loading = false; 116 | }, 117 | error: () => (this.loading = false), 118 | }); 119 | } 120 | 121 | showChatBox() { 122 | this.openChatBox.open(); 123 | } 124 | 125 | onSwitchChange(switchState: boolean): void { 126 | switchState ? this.getUnfilteredPaper() : this.getAllPrimaryStudies(); 127 | } 128 | 129 | toggleAbstract(index: number) { 130 | this.expandedAbstracts[index] = !this.expandedAbstracts[index]; 131 | } 132 | 133 | truncateText(text: string, index: number): string { 134 | if (!text) return 'Abstract not available'; 135 | if (this.expandedAbstracts[index]) return text; 136 | const words = text.split(' '); 137 | return words.length > 20 ? words.slice(0, 20).join(' ') + '...' : text; 138 | } 139 | 140 | toggleQueryDetails(index: number): void { 141 | this.expandedQueries[index] = !this.expandedQueries[index]; 142 | } 143 | toggleCreateQuery(): void { 144 | this.showCreateQuery = true; 145 | } 146 | 147 | onQueryCreated(): void { 148 | this.showCreateQuery = false; 149 | this.getAllQuery(); 150 | this.getAllPrimaryStudies(); 151 | } 152 | 153 | onCancelCreateQuery(): void { 154 | this.showCreateQuery = false; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /utils/webScrapper.js: -------------------------------------------------------------------------------- 1 | import { chromium } from 'playwright'; 2 | 3 | const fetchSemanticScholar = async (query, maxResults = 10, startYear, endYear) => { 4 | const results = []; 5 | let currentPage = 1; 6 | 7 | while (results.length < maxResults) { 8 | let browser; 9 | let page; 10 | 11 | try { 12 | console.log(`\n=== Fetching Page ${currentPage} ===`); 13 | 14 | // Launch a new browser instance for each page 15 | browser = await chromium.launch({ headless: true }); 16 | page = await browser.newPage(); 17 | 18 | await page.setExtraHTTPHeaders({ 19 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 20 | 'Accept-Language': 'en-US,en;q=0.9', 21 | }); 22 | 23 | const url = `https://www.semanticscholar.org/search?year%5B0%5D=${startYear}&year%5B1%5D=${endYear}&q=${encodeURIComponent(query)}&sort=relevance&page=${currentPage}`; 24 | 25 | console.log("Navigating to:", url); 26 | await page.goto(url, { waitUntil: 'networkidle' }); 27 | 28 | // Wait for content to load 29 | let attempts = 0; 30 | const maxAttempts = 3; 31 | 32 | while (attempts < maxAttempts) { 33 | try { 34 | await page.waitForFunction(() => { 35 | return document.querySelectorAll('.cl-paper-row').length > 0; 36 | }, { timeout: 60000 }); 37 | break; 38 | } catch (error) { 39 | attempts++; 40 | console.error(`Attempt ${attempts} failed: ${error.message}`); 41 | if (attempts >= maxAttempts) throw new Error('Max retry attempts reached.'); 42 | await page.reload({ waitUntil: 'networkidle' }); 43 | } 44 | } 45 | 46 | const pageResults = await page.evaluate(async () => { 47 | const papers = []; 48 | const paperElements = document.querySelectorAll('.cl-paper-row'); 49 | 50 | for (const element of paperElements) { 51 | // Click "more-toggle" button if it exists to reveal full abstract 52 | const expandButton = element.querySelector('.more-toggle'); 53 | if (expandButton) { 54 | expandButton.click(); 55 | await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for content to load 56 | } 57 | 58 | const title = element.querySelector('.cl-paper-title')?.textContent.trim() || 'No Title'; 59 | const linkElement = element.querySelector('.link-button--show-visited'); 60 | const url = linkElement ? 'https://www.semanticscholar.org' + linkElement.getAttribute('href') : null; 61 | const authors = Array.from(element.querySelectorAll('.cl-paper-authors a')).map(authorElement => ({ 62 | authorId: authorElement.getAttribute('href')?.split('/').pop() || null, 63 | name: authorElement.textContent.trim() || 'Unknown Author' 64 | })); 65 | const yearTag = element.querySelector('.cl-paper-pubdates')?.textContent.trim() || 'No Year'; 66 | const year = yearTag.match(/\d{4}/) ? parseInt(yearTag.match(/\d{4}/)[0]) : null; 67 | 68 | // Extract full abstract after clicking "more-toggle" 69 | const abstractElement = element.querySelector('.tldr-abstract-replacement'); 70 | const abstract = abstractElement ? abstractElement.textContent.trim() : 'No Abstract'; 71 | 72 | const referenceCountElement = element.querySelector('.cl-paper-stats__item .cl-paper-stats__citation-pdp-link'); 73 | const referenceCount = referenceCountElement ? parseInt(referenceCountElement.textContent.trim(), 10) || 0 : null; 74 | 75 | const citationCountElement = element.querySelector('.cl-paper-stats__item .cl-paper-stats__citation-pdp-link'); 76 | const citationCount = citationCountElement ? parseInt(citationCountElement.textContent.trim(), 10) || 0 : null; 77 | 78 | const openAccessPdfElement = element.querySelector('.cl-paper-action__button-container .cl-paper-view-paper'); 79 | const openAccessPdf = openAccessPdfElement ? { url: openAccessPdfElement.getAttribute('href') } : null; 80 | 81 | papers.push({ 82 | title, 83 | abstract, 84 | url, 85 | authors, 86 | referenceCount, 87 | citationCount, 88 | year, 89 | openAccessPdf, 90 | }); 91 | } 92 | 93 | return papers; 94 | }); 95 | 96 | results.push(...pageResults); 97 | 98 | console.log(`Fetched ${pageResults.length} results from Page ${currentPage}`); 99 | 100 | // Close the browser 101 | await browser.close(); 102 | console.log("Browser closed. Waiting 5 seconds before fetching the next page...\n"); 103 | 104 | // Wait 5 seconds before opening a new browser for the next page 105 | await new Promise(resolve => setTimeout(resolve, 5000)); 106 | 107 | // Exit if max results are collected 108 | if (results.length >= maxResults || pageResults.length === 0) break; 109 | 110 | // Move to the next page 111 | currentPage++; 112 | } catch (error) { 113 | console.error('Error occurred:', error); 114 | if (browser) await browser.close(); 115 | break; // Exit loop on failure 116 | } 117 | } 118 | 119 | return results.slice(0, maxResults); 120 | }; 121 | 122 | export default fetchSemanticScholar; 123 | -------------------------------------------------------------------------------- /controllers/researchController.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | import UnAuthenticatedError from '../errors/unauthenticated.js'; 4 | import NotFoundError from '../errors/notFound.js'; 5 | import PrimaryStudy from '../models/PrimaryStudies.js'; 6 | import SystematicReview from '../models/SystematicReview.js'; 7 | import FilterQuery from '../models/FilterQuery.js'; 8 | import ResearchPapers from '../models/ResearchPapers.js'; 9 | import Chat from '../models/Chat.js'; 10 | import fetchSemanticScholar from '../utils/webScrapper.js'; 11 | import { 12 | processResearchPapers, 13 | createResearchAssistant, 14 | createChatAssistant 15 | } from '../utils/openAiRequest.js'; 16 | 17 | dotenv.config(); 18 | 19 | const createResearch = async (req, res) => { 20 | const { title, description } = req.body; 21 | if (!title || !description) throw new UnAuthenticatedError('Please enter all fields'); 22 | 23 | const [researchAssistantId, chatAssistantId] = await Promise.all([ 24 | createResearchAssistant(), 25 | createChatAssistant() 26 | ]); 27 | 28 | const systematicReview = await SystematicReview.create({ 29 | title, 30 | description, 31 | user: req.user.userId, 32 | researchAssistantId, 33 | chatAssistantId, 34 | }); 35 | 36 | res.status(StatusCodes.CREATED).json({ 37 | message: 'Systematic Literature Review created successfully', 38 | id: systematicReview.id, 39 | title, 40 | description, 41 | assistantId: systematicReview.assistantId, 42 | }); 43 | }; 44 | 45 | const allResearch = async (req, res) => { 46 | const systematicReviews = await SystematicReview.find({ user: req.user.userId }); 47 | res.status(StatusCodes.OK).json({ message: 'Successful', data: systematicReviews }); 48 | }; 49 | 50 | const getResearch = async (req, res) => { 51 | const systematicReview = await SystematicReview.findById(req.params.id); 52 | if (!systematicReview) throw new NotFoundError('Systematic review not found'); 53 | 54 | res.status(StatusCodes.OK).json({ 55 | title: systematicReview.title, 56 | description: systematicReview.description, 57 | assistantId: systematicReview.assistantId, 58 | }); 59 | }; 60 | 61 | const deleteResearch = async (req, res) => { 62 | const systematicReview = await SystematicReview.findById(req.params.id); 63 | if (!systematicReview) throw new NotFoundError('Systematic review not found'); 64 | 65 | await Promise.all([ 66 | systematicReview.deleteOne(), 67 | FilterQuery.deleteMany({ systematicReviewId: req.params.id }), 68 | PrimaryStudy.deleteMany({ systematicReviewId: req.params.id }) 69 | ]); 70 | 71 | res.status(StatusCodes.OK).json({ message: 'Systematic review removed successfully' }); 72 | }; 73 | 74 | const updateResearch = async (req, res) => { 75 | const updatedSystematicReview = await SystematicReview.findByIdAndUpdate(req.params.id, req.body); 76 | if (!updatedSystematicReview) throw new NotFoundError('Systematic review not found'); 77 | 78 | res.status(StatusCodes.OK).json({ message: 'Systematic review updated successfully', id: updatedSystematicReview.id }); 79 | }; 80 | 81 | const createQuery = async (req, res) => { 82 | try { 83 | const { researchQuestion, inclusionCriteria, exclusionCriteria, searchString, systematicReviewId, maxResearch, startYear, endYear } = req.body; 84 | if (!researchQuestion || !inclusionCriteria || !exclusionCriteria || !searchString || !systematicReviewId || !startYear || !endYear) 85 | throw new UnAuthenticatedError('Please enter all fields'); 86 | 87 | const systematicReview = await SystematicReview.findById(systematicReviewId); 88 | if (!systematicReview) return res.status(StatusCodes.NOT_FOUND).json({ message: 'Systematic review not found' }); 89 | 90 | const filteredPapers = await fetchSemanticScholar(searchString, maxResearch, startYear, endYear) || []; 91 | let totalFound = 0; 92 | const filterQuery = await FilterQuery.create({ 93 | researchQuestion, inclusionCriteria, exclusionCriteria, searchString, systematicReviewId, totalFound 94 | }); 95 | 96 | for (const paper of filteredPapers) { 97 | try { 98 | await new ResearchPapers({ 99 | ...paper, 100 | filterQuery: filterQuery.id, 101 | systematicReviewId, 102 | user: req.user.userId, 103 | }).save(); 104 | } catch (error) { 105 | console.error('Error saving Research Paper:', error); 106 | } 107 | 108 | const openAiResponse = await processResearchPapers( 109 | systematicReview.researchAssistantId, paper, inclusionCriteria, exclusionCriteria, researchQuestion 110 | ); 111 | 112 | if (openAiResponse.trim().toLowerCase() === 'yes') { 113 | totalFound++; 114 | try { 115 | await new PrimaryStudy({ 116 | ...paper, 117 | filterQuery: filterQuery.id, 118 | systematicReviewId, 119 | user: req.user.userId, 120 | }).save(); 121 | } catch (error) { 122 | console.error('Error saving Primary Study:', error); 123 | } 124 | } 125 | } 126 | 127 | await FilterQuery.updateOne({ _id: filterQuery.id }, { $set: { totalFound } }); 128 | 129 | res.status(StatusCodes.OK).json({ message: 'Primary study selection successful' }); 130 | } catch (error) { 131 | console.error('Error during createQuery:', error); 132 | res.status(StatusCodes.BAD_REQUEST).json({ message: 'Something went wrong' }); 133 | } 134 | }; 135 | 136 | const allQuery = async (req, res) => { 137 | const filterQueries = await FilterQuery.find({ systematicReviewId: req.params.id }); 138 | res.status(StatusCodes.OK).json({ message: 'Successful', data: filterQueries }); 139 | }; 140 | 141 | const deleteQuery = async (req, res) => { 142 | const query = await FilterQuery.findById(req.params.id); 143 | if (!query) throw new NotFoundError('Query not found'); 144 | 145 | await Promise.all([ 146 | query.deleteOne(), 147 | PrimaryStudy.deleteMany({ filterQuery: req.params.id }), 148 | ResearchPapers.deleteMany({ filterQuery: req.params.id }), 149 | Chat.deleteOne({ user: req.user.userId }) 150 | ]); 151 | 152 | res.status(StatusCodes.OK).json({ message: 'Query removed successfully' }); 153 | }; 154 | 155 | const getAllPrimaryStudy = async (req, res) => { 156 | const primaryStudies = await PrimaryStudy.find({ systematicReviewId: req.params.id }); 157 | res.status(StatusCodes.OK).json({ message: 'Successful', data: primaryStudies }); 158 | }; 159 | 160 | const getAllResearchPaper = async (req, res) => { 161 | const researchPapers = await ResearchPapers.find({ systematicReviewId: req.params.id }); 162 | res.status(StatusCodes.OK).json({ message: 'Successful', data: researchPapers }); 163 | }; 164 | 165 | export { 166 | createResearch, 167 | allResearch, 168 | getResearch, 169 | createQuery, 170 | deleteResearch, 171 | updateResearch, 172 | allQuery, 173 | deleteQuery, 174 | getAllPrimaryStudy, 175 | getAllResearchPaper 176 | }; 177 | -------------------------------------------------------------------------------- /client/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | /* You can add global styles to this file, and also import other style files */ 3 | /* fonts */ 4 | @import url('https://fonts.googleapis.com/css2?family=Mona+Sans:wght@400;700&display=swap'); 5 | *, 6 | ::after, 7 | ::before { 8 | box-sizing: border-box; 9 | } 10 | 11 | html { 12 | font-size: 100%; 13 | } /*16px*/ 14 | 15 | :root { 16 | /* colors */ 17 | /* colors */ 18 | --primary-50: #eaf0f5; 19 | --primary-100: #c4d0d4; 20 | --primary-200: #a5b6bd; 21 | --primary-300: #859da6; 22 | --primary-400: #65838e; 23 | --primary-500: #101E24; 24 | --primary-600: #0e191e; 25 | --primary-700: #0b1418; 26 | --primary-800: #090f11; 27 | 28 | --off-white: #F4FAFE; 29 | /* grey */ 30 | --grey-50: #e6eef0; /* Very light desaturated cyan */ 31 | --grey-100: #cfdde0; 32 | --grey-200: #b1c5c9; 33 | --grey-300: #94acb1; 34 | --grey-400: #78939a; 35 | --grey-500: #5f7b82; /* Muted teal-gray */ 36 | --grey-600: #486168; 37 | --grey-700: #344952; 38 | --grey-800: #223238; 39 | --grey-900: #101E24; 40 | 41 | --grey-default: #626262; 42 | /* rest of the colors */ 43 | --black: #222; 44 | --white: #fff; 45 | --red-light: #f8d7da; 46 | --red-dark: #842029; 47 | --green-light: #d1e7dd; 48 | --green-dark: #0f5132; 49 | 50 | /* fonts */ 51 | --headingFont:'Mona Sans', sans-serif; 52 | --bodyFont: 'Mona Sans', sans-serif; 53 | --small-text: 0.875rem; 54 | --extra-small-text: 0.7em; 55 | /* rest of the vars */ 56 | --backgroundColor: var(--off-white); 57 | --textColor: var(--primary-500); 58 | --borderRadius: 0.25rem; 59 | --letterSpacing: .8px; 60 | --transition: 0.3s ease-in-out all; 61 | --max-width: 1120px; 62 | --fixed-width: 500px; 63 | --fluid-width: 90vw; 64 | --breakpoint-lg: 992px; 65 | --nav-height: 6rem; 66 | /* box shadow*/ 67 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 68 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 69 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 70 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 71 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 72 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 73 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 74 | } 75 | 76 | body { 77 | background: var(--backgroundColor); 78 | font-family: var(--bodyFont); 79 | font-weight: 400; 80 | line-height: 1.75; 81 | color: var(--textColor); 82 | min-height: 100vh; 83 | } 84 | 85 | p { 86 | margin-bottom: 1.5rem; 87 | max-width: 40em; 88 | } 89 | 90 | h1, 91 | h2, 92 | h3, 93 | h4, 94 | h5 { 95 | margin: 0; 96 | margin-bottom: 1.38rem; 97 | font-family: var(--headingFont); 98 | font-weight: 400; 99 | line-height: 1.3; 100 | text-transform: capitalize; 101 | letter-spacing: var(--letterSpacing); 102 | } 103 | 104 | h1 { 105 | margin-top: 0; 106 | font-size: 3.052rem; 107 | } 108 | 109 | h2 { 110 | font-size: 2.441rem; 111 | } 112 | 113 | h3 { 114 | font-size: 1.953rem; 115 | } 116 | 117 | h4 { 118 | font-size: 1.563rem; 119 | } 120 | 121 | h5 { 122 | font-size: 1.25rem; 123 | } 124 | 125 | small, 126 | .text-small { 127 | font-size: var(--small-text); 128 | } 129 | 130 | a { 131 | text-decoration: none; 132 | letter-spacing: var(--letterSpacing); 133 | font-size: 1rem; 134 | } 135 | a, 136 | button { 137 | line-height: 1.15; 138 | } 139 | button:disabled { 140 | cursor: not-allowed; 141 | } 142 | ul { 143 | list-style-type: none; 144 | padding: 0; 145 | } 146 | 147 | .img { 148 | width: 100%; 149 | display: block; 150 | object-fit: cover; 151 | } 152 | /* buttons */ 153 | 154 | .btn { 155 | cursor: pointer; 156 | color: var(--white); 157 | background: var(--primary-500); 158 | border: transparent; 159 | border-radius: var(--borderRadius); 160 | letter-spacing: var(--letterSpacing); 161 | padding: 0.375rem 0.75rem; 162 | box-shadow: var(--shadow-2); 163 | transition: var(--transition); 164 | text-transform: capitalize; 165 | display: inline-block; 166 | } 167 | .btn:hover { 168 | background: var(--primary-700); 169 | box-shadow: var(--shadow-3); 170 | } 171 | .btn-hipster { 172 | color: var(--primary-500); 173 | background: var(--primary-200); 174 | } 175 | .btn-edit { 176 | color: var(--grey-default); 177 | background: transparent; 178 | border: 2px solid var(--grey-default); 179 | } 180 | 181 | .btn-edit:hover { 182 | background: var(--grey-default); 183 | color: var(--white); 184 | } 185 | .btn-hipster:hover { 186 | color: var(--primary-200); 187 | background: var(--primary-700); 188 | } 189 | .btn-block { 190 | width: 100%; 191 | } 192 | .btn-hero { 193 | font-size: 1.25rem; 194 | padding: 0.5rem 1.25rem; 195 | } 196 | .btn-danger { 197 | background: var(--red-light); 198 | color: var(--red-dark); 199 | } 200 | .btn-danger:hover { 201 | background: var(--red-dark); 202 | color: var(--white); 203 | } 204 | /* alerts */ 205 | .alert { 206 | padding: 0.375rem 0.75rem; 207 | margin-bottom: 1rem; 208 | border-color: transparent; 209 | border-radius: var(--borderRadius); 210 | text-align: center; 211 | letter-spacing: var(--letterSpacing); 212 | } 213 | 214 | .alert-danger { 215 | color: var(--red-dark); 216 | background: var(--red-light); 217 | } 218 | .alert-success { 219 | color: var(--green-dark); 220 | background: var(--green-light); 221 | } 222 | /* form */ 223 | 224 | .form { 225 | width: 90vw; 226 | max-width: var(--fixed-width); 227 | border-radius: var(--borderRadius); 228 | padding: 1rem 1.5rem; 229 | margin: 3rem auto; 230 | transition: var(--transition); 231 | } 232 | 233 | @media(min-width:768px) { 234 | .form { 235 | padding: 2rem 2.5rem; 236 | } 237 | } 238 | 239 | .form-heading { 240 | margin-bottom: 2rem; 241 | } 242 | .form-heading p { 243 | text-align: left; 244 | color: var(--grey-300); 245 | } 246 | .form-heading h5 { 247 | margin-bottom: .5rem; 248 | } 249 | .form-label { 250 | display: block; 251 | font-size: var(--smallText); 252 | margin-bottom: 0.5rem; 253 | text-transform: capitalize; 254 | letter-spacing: var(--letterSpacing); 255 | } 256 | .form-input, 257 | .form-textarea, 258 | .form-select { 259 | width: 100%; 260 | padding: 0.375rem 0.75rem; 261 | border-radius: var(--borderRadius); 262 | background: var(--backgroundColor); 263 | border: 1px solid var(--primary-500); 264 | } 265 | .form-input, 266 | .form-select, 267 | .btn-block { 268 | height: 45px; 269 | } 270 | .form-row { 271 | margin-bottom: 1rem; 272 | } 273 | 274 | .form-textarea { 275 | height: 7rem; 276 | } 277 | ::placeholder { 278 | font-family: inherit; 279 | color: var(--grey-400); 280 | } 281 | .form-alert { 282 | color: var(--red-dark); 283 | letter-spacing: var(--letterSpacing); 284 | text-transform: capitalize; 285 | } 286 | 287 | .form-footer-link { 288 | text-align: center; 289 | margin-top: 2rem; 290 | } 291 | /* alert */ 292 | 293 | @keyframes spinner { 294 | to { 295 | transform: rotate(360deg); 296 | } 297 | } 298 | 299 | .loading { 300 | width: 6rem; 301 | height: 6rem; 302 | border: 5px solid var(--grey-400); 303 | border-radius: 50%; 304 | border-top-color: var(--primary-500); 305 | animation: spinner 2s linear infinite; 306 | } 307 | .loading-center { 308 | margin: 0 auto; 309 | } 310 | /* title */ 311 | 312 | .title-underline { 313 | background: var(--primary-500); 314 | width: 7rem; 315 | height: 0.25rem; 316 | margin: 0 auto; 317 | margin-top: -1rem; 318 | } 319 | 320 | .container { 321 | width: var(--fluid-width); 322 | max-width: var(--max-width); 323 | margin: 0 auto; 324 | } 325 | .full-page { 326 | min-height: 100vh; 327 | } 328 | .title { 329 | font-weight: 700; 330 | margin-bottom: 0; 331 | } 332 | .title span { 333 | color: var(--primary-500); 334 | } 335 | .card-wrapper { 336 | display: flex; 337 | flex-wrap: wrap; 338 | gap: 2rem; 339 | } 340 | .card { 341 | display: flex; 342 | background-color: var(--white); 343 | padding: 1.5rem; 344 | box-shadow: rgba(17, 17, 26, 0.1) 0px 4px 16px, 345 | rgba(17, 17, 26, 0.05) 0px 8px 32px; 346 | border-radius: 0.5rem; 347 | } 348 | .card h4 { 349 | margin-bottom: 0.5rem; 350 | } 351 | .vertical { 352 | flex-direction: column; 353 | gap: 1rem; 354 | } 355 | section { 356 | margin-top: 1rem; 357 | margin-bottom: 1rem; 358 | } 359 | 360 | .button-group { 361 | display: flex; 362 | gap: 0.5rem; 363 | } 364 | .input-error { 365 | border: 1px solid var(--red-dark); 366 | } 367 | .empty-container { 368 | height: 60vh; 369 | display: flex; 370 | justify-content: center; 371 | align-items: center; 372 | } 373 | .sm-d-none { 374 | display: none; 375 | } 376 | @media (min-width: 768px) { 377 | .card { 378 | min-width: min-content; 379 | width: 25%; 380 | justify-content: space-between; 381 | } 382 | .sm-d-none { 383 | display: block; 384 | } 385 | } 386 | --------------------------------------------------------------------------------