From 4b84a095381700ef3df13c23f2a7d6e98a196de4 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Sun, 30 Nov 2025 21:55:28 +0800 Subject: [PATCH] fix(dev): bind frontend to 127.0.0.1:8080 and avoid EACCES\nfix(backend): bind server to 127.0.0.1:8081, add permissive CORS whitelist\nfix(auth): login form UX remove default username, clarify placeholder, add test account autofill\nchore(api): set frontend API_BASE_URL to 127.0.0.1:8081\nrefactor(assignments): lifecycle/state logic and archive endpoint\nfeat(analytics): add exam stats endpoint and client method\nchore(lint): add eslint configs --- .eslintignore | 5 + .eslintrc.json | 17 + Model.ts | 12 +- UI_DTO.ts | 56 +- backend/package-lock.json | 112 + backend/package.json | 17 +- backend/prisma/schema.prisma | 11 +- .../src/controllers/analytics.controller.ts | 343 +- .../src/controllers/assignment.controller.ts | 351 +- backend/src/controllers/common.controller.ts | 145 +- backend/src/controllers/config.controller.ts | 17 + .../src/controllers/curriculum.controller.ts | 230 +- backend/src/controllers/exam.controller.ts | 8 +- backend/src/controllers/grading.controller.ts | 297 +- backend/src/controllers/org.controller.ts | 262 +- .../src/controllers/question.controller.ts | 212 +- .../src/controllers/submission.controller.ts | 453 +- backend/src/index.ts | 22 +- backend/src/routes/analytics.routes.ts | 2 + backend/src/routes/assignment.routes.ts | 4 + backend/src/routes/common.routes.ts | 8 +- backend/src/routes/config.routes.ts | 9 + backend/src/routes/submission.routes.ts | 1 + backend/src/services/analytics.service.ts | 214 + backend/src/services/assignment.service.ts | 483 ++ backend/src/services/common.service.ts | 80 + backend/src/services/config.service.ts | 20 + backend/src/services/curriculum.service.ts | 121 + backend/src/services/exam.service.ts | 110 +- backend/src/services/grading.service.ts | 68 + backend/src/services/org.service.ts | 103 + backend/src/services/question.service.ts | 131 + backend/src/services/submission.service.ts | 476 ++ package-lock.json | 4407 +++++++++++++++++ package.json | 12 +- .../assignments/[id]/analysis/page.tsx | 21 + src/app/(dashboard)/assignments/page.tsx | 34 +- src/app/api/auth/login/route.ts | 55 +- src/app/api/auth/me/route.ts | 32 +- src/app/api/config/db/route.ts | 34 +- src/app/api/org/classes/route.ts | 29 +- src/components/Sidebar.tsx | 13 +- src/components/ui/Button.tsx | 5 +- .../components/AssignmentAnalysis.tsx | 335 ++ .../components/CreateAssignmentModal.tsx | 99 +- .../components/EditAssignmentModal.tsx | 155 + .../components/StudentAssignmentList.tsx | 211 +- .../components/TeacherAssignmentList.tsx | 334 +- src/features/auth/components/LoginForm.tsx | 21 +- .../dashboard/components/StudentDashboard.tsx | 16 +- src/features/exam/components/ExamEditor.tsx | 209 +- src/features/exam/components/ExamList.tsx | 385 +- src/lib/auth-context.tsx | 10 +- src/lib/db.ts | 35 - src/lib/server-utils.ts | 24 - src/services/api.ts | 13 - src/services/interfaces.ts | 23 +- src/services/mockApi.ts | 696 --- src/services/realApi.ts | 72 +- src/types.ts | 345 +- src/views/ExamEngine.tsx | 90 +- src/views/StudentExamRunner.tsx | 56 +- tsconfig.tsbuildinfo | 1 + 63 files changed, 8478 insertions(+), 3694 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 backend/src/controllers/config.controller.ts create mode 100644 backend/src/routes/config.routes.ts create mode 100644 backend/src/services/analytics.service.ts create mode 100644 backend/src/services/assignment.service.ts create mode 100644 backend/src/services/common.service.ts create mode 100644 backend/src/services/config.service.ts create mode 100644 backend/src/services/curriculum.service.ts create mode 100644 backend/src/services/grading.service.ts create mode 100644 backend/src/services/org.service.ts create mode 100644 backend/src/services/question.service.ts create mode 100644 backend/src/services/submission.service.ts create mode 100644 src/app/(dashboard)/assignments/[id]/analysis/page.tsx create mode 100644 src/features/assignment/components/AssignmentAnalysis.tsx create mode 100644 src/features/assignment/components/EditAssignmentModal.tsx delete mode 100644 src/lib/db.ts delete mode 100644 src/lib/server-utils.ts delete mode 100644 src/services/mockApi.ts create mode 100644 tsconfig.tsbuildinfo diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..bcf7fcf --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +node_modules +.next +dist +backend/dist +backend/node_modules diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..628f5bf --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "root": true, + "extends": ["next/core-web-vitals"], + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module", + "project": ["./tsconfig.json"], + "tsconfigRootDir": "." + }, + "ignorePatterns": [ + "node_modules/", + ".next/", + "backend/dist/", + "backend/node_modules/", + "dist/" + ] +} diff --git a/Model.ts b/Model.ts index 8f6ced8..9b81eab 100644 --- a/Model.ts +++ b/Model.ts @@ -50,14 +50,12 @@ export interface ApplicationUser extends BaseEntity { /** 头像URL */ avatarUrl: string; - /** 性别:Male | Female | Unknown */ - gender: 'Male' | 'Female' | 'Unknown'; + gender: 'Male' | 'Female'; /** 当前所属学校ID(冗余字段,用于快速确定用户主要归属) */ currentSchoolId: string; - /** 账号状态:Active | Disabled */ - accountStatus: 'Active' | 'Disabled'; + accountStatus: 'Active' | 'Suspended' | 'Graduated'; /** 邮箱(可选) */ email?: string; @@ -419,8 +417,7 @@ export interface SubmissionDetail extends BaseEntity { */ export enum Gender { Male = 'Male', - Female = 'Female', - Unknown = 'Unknown' + Female = 'Female' } /** @@ -428,7 +425,8 @@ export enum Gender { */ export enum AccountStatus { Active = 'Active', - Disabled = 'Disabled' + Suspended = 'Suspended', + Graduated = 'Graduated' } /** diff --git a/UI_DTO.ts b/UI_DTO.ts index 73d1443..fb99aff 100644 --- a/UI_DTO.ts +++ b/UI_DTO.ts @@ -246,8 +246,12 @@ export interface ExamDto { totalScore: number; duration: number; // 建议时长(分钟) questionCount: number; // 总题数 + usageCount?: number; status: 'Draft' | 'Published'; createdAt: string; + creatorName?: string; + isMyExam?: boolean; + examType?: string; } /** @@ -273,6 +277,9 @@ export interface ExamNodeDto { // === 递归子节点 === children?: ExamNodeDto[]; // 子节点(支持无限嵌套) + + // === 学生作答(用于恢复进度) === + studentAnswer?: any; } /** @@ -304,24 +311,67 @@ export interface ExamStatsDto { // 6. Assignment / 作业 // ============================================================ +export interface AssignmentAnalysisDto { + overview: { + title: string; + examTitle: string; + totalStudents: number; + submittedCount: number; + averageScore: number; + maxScore: number; + minScore: number; + }; + questions: { + id: string; + title: string; + questionId: string | null; + score: number; + totalAnswers: number; + errorCount: number; + errorRate: number; + correctAnswer: string; + knowledgePoints: string[]; + wrongSubmissions: { + studentName: string; + studentAnswer: string; + }[]; + }[]; + knowledgePoints: { + name: string; + errorRate: number; + }[]; +} + export interface AssignmentTeacherViewDto { id: string; title: string; + examTitle: string; + subjectName?: string; + examType?: string; className: string; + gradeName: string; submittedCount: number; totalCount: number; - status: 'Active' | 'Ended' | 'Scheduled'; + status: string; // 'Active' | 'Closed' | 'Ended' + hasPendingGrading?: boolean; dueDate: string; - examTitle: string; + createdAt: string; } export interface AssignmentStudentViewDto { id: string; title: string; examTitle: string; + subjectName?: string; + teacherName?: string; + duration?: number; + questionCount?: number; + totalScore?: number; + className?: string; endTime: string; - status: 'Pending' | 'Graded' | 'Submitted'; + status: 'Pending' | 'InProgress' | 'Submitted' | 'Grading' | 'Completed'; score?: number; + isSubmitted?: boolean; // Helper flag to distinguish between 'Grading' (expired but not submitted) vs (submitted) } // ============================================================ diff --git a/backend/package-lock.json b/backend/package-lock.json index 5f777ca..b3649f3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@prisma/client": "^5.22.0", + "axios": "^1.13.2", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.4.5", @@ -718,6 +719,23 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -792,6 +810,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -850,6 +880,15 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -949,6 +988,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1070,6 +1124,42 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1186,6 +1276,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1471,6 +1576,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -1497,6 +1603,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", diff --git a/backend/package.json b/backend/package.json index b1bff6e..1a16d4d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,23 +22,24 @@ "license": "MIT", "dependencies": { "@prisma/client": "^5.22.0", - "express": "^4.21.1", + "axios": "^1.13.2", + "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.4.5", - "bcryptjs": "^2.4.3", + "express": "^4.21.1", "jsonwebtoken": "^9.0.2", - "zod": "^3.23.8", - "uuid": "^11.0.3" + "uuid": "^11.0.3", + "zod": "^3.23.8" }, "devDependencies": { - "@types/express": "^5.0.0", - "@types/node": "^22.10.1", - "@types/cors": "^2.8.17", "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.10.1", "@types/uuid": "^10.0.0", "prisma": "^5.22.0", "tsx": "^4.19.2", "typescript": "^5.7.2" } -} \ No newline at end of file +} diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f13a162..5b378a9 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -328,9 +328,11 @@ model Exam { id String @id @default(uuid()) @db.VarChar(36) subjectId String @map("subject_id") @db.VarChar(36) title String @db.VarChar(200) + examType String @default("Uncategorized") @map("exam_type") @db.VarChar(50) // e.g. Midterm, Final, Unit, Weekly totalScore Decimal @map("total_score") @default(0) @db.Decimal(5, 1) suggestedDuration Int @map("suggested_duration") status ExamStatus @default(Draft) + description String? @db.Text createdAt DateTime @default(now()) @map("created_at") createdBy String @map("created_by") @db.VarChar(36) @@ -402,7 +404,8 @@ model Assignment { endTime DateTime @map("end_time") allowLateSubmission Boolean @map("allow_late_submission") @default(false) autoScoreEnabled Boolean @map("auto_score_enabled") @default(true) - + status AssignmentStatus @default(Active) + createdAt DateTime @default(now()) @map("created_at") createdBy String @map("created_by") @db.VarChar(36) updatedAt DateTime @updatedAt @map("updated_at") @@ -425,6 +428,7 @@ model StudentSubmission { assignmentId String @map("assignment_id") @db.VarChar(36) studentId String @map("student_id") @db.VarChar(36) submissionStatus SubmissionStatus @map("submission_status") @default(Pending) + startedAt DateTime? @map("started_at") submitTime DateTime? @map("submit_time") timeSpentSeconds Int? @map("time_spent_seconds") totalScore Decimal? @map("total_score") @db.Decimal(5, 1) @@ -478,6 +482,11 @@ enum SubmissionStatus { Graded } +enum AssignmentStatus { + Active + Archived +} + enum JudgementResult { Correct Incorrect diff --git a/backend/src/controllers/analytics.controller.ts b/backend/src/controllers/analytics.controller.ts index 6dd74db..a331e22 100644 --- a/backend/src/controllers/analytics.controller.ts +++ b/backend/src/controllers/analytics.controller.ts @@ -1,66 +1,14 @@ import { Response } from 'express'; -import { PrismaClient } from '@prisma/client'; +import { analyticsService } from '../services/analytics.service'; import { AuthRequest } from '../middleware/auth.middleware'; -const prisma = new PrismaClient(); // 获取班级表现(平均分趋势) export const getClassPerformance = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - // 获取教师管理的班级 - const classes = await prisma.class.findMany({ - where: { - OR: [ - { headTeacherId: userId }, - { members: { some: { userId: userId, roleInClass: 'Teacher' } } } - ], - isDeleted: false - }, - select: { id: true, name: true } - }); - - const classIds = classes.map(c => c.id); - - // 获取最近的5次作业/考试 - const assignments = await prisma.assignment.findMany({ - where: { - classId: { in: classIds }, - isDeleted: false - }, - orderBy: { endTime: 'desc' }, - take: 5, - include: { - submissions: { - where: { submissionStatus: 'Graded' }, - select: { totalScore: true } - } - } - }); - - // 按时间正序排列 - assignments.reverse(); - - const labels = assignments.map(a => a.title); - const data = assignments.map(a => { - const scores = a.submissions.map(s => Number(s.totalScore)); - const avg = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0; - return Number(avg.toFixed(1)); - }); - - res.json({ - labels, - datasets: [ - { - label: '班级平均分', - data, - borderColor: 'rgb(75, 192, 192)', - backgroundColor: 'rgba(75, 192, 192, 0.5)', - } - ] - }); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const result = await analyticsService.getClassPerformance(req.userId); + res.json(result); } catch (error) { console.error('Get class performance error:', error); res.status(500).json({ error: 'Failed to get class performance' }); @@ -70,51 +18,32 @@ export const getClassPerformance = async (req: AuthRequest, res: Response) => { // 获取学生成长(个人成绩趋势) export const getStudentGrowth = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - // 获取学生最近的5次已批改提交 - const submissions = await prisma.studentSubmission.findMany({ - where: { - studentId: userId, - submissionStatus: 'Graded', - isDeleted: false - }, - orderBy: { submitTime: 'desc' }, - take: 5, - include: { assignment: true } - }); - - submissions.reverse(); - - const labels = submissions.map(s => s.assignment.title); - const data = submissions.map(s => Number(s.totalScore)); - - res.json({ - labels, - datasets: [ - { - label: '我的成绩', - data, - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', - } - ] - }); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const result = await analyticsService.getStudentGrowth(req.userId); + res.json(result); } catch (error) { console.error('Get student growth error:', error); res.status(500).json({ error: 'Failed to get student growth' }); } }; +// 获取学生统计数据(已完成、代办、平均分、学习时长) +export const getStudentStats = async (req: AuthRequest, res: Response) => { + try { + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const stats = await analyticsService.getStudentStats(req.userId); + res.json(stats); + } catch (error) { + console.error('Get student stats error:', error); + res.status(500).json({ error: 'Failed to get student stats' }); + } +}; + // 获取班级能力雷达图 export const getRadar = async (req: AuthRequest, res: Response) => { try { - // 模拟数据,因为目前没有明确的能力维度字段 - res.json({ - indicators: ['知识掌握', '应用能力', '分析能力', '逻辑思维', '创新能力','个人表现'], - values: [85, 78, 92, 88, 75,99] - }); + const result = await analyticsService.getRadar(); + res.json(result); } catch (error) { console.error('Get radar error:', error); res.status(500).json({ error: 'Failed to get radar data' }); @@ -124,11 +53,8 @@ export const getRadar = async (req: AuthRequest, res: Response) => { // 获取学生能力雷达图 export const getStudentRadar = async (req: AuthRequest, res: Response) => { try { - // 模拟数据 - res.json({ - indicators: ['知识掌握', '应用能力', '分析能力', '逻辑思维', '创新能力'], - values: [80, 85, 90, 82, 78] - }); + const result = await analyticsService.getStudentRadar(); + res.json(result); } catch (error) { console.error('Get student radar error:', error); res.status(500).json({ error: 'Failed to get student radar data' }); @@ -138,60 +64,8 @@ export const getStudentRadar = async (req: AuthRequest, res: Response) => { // 获取成绩分布 export const getScoreDistribution = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - // 获取教师管理的班级 - const classes = await prisma.class.findMany({ - where: { - OR: [ - { headTeacherId: userId }, - { members: { some: { userId: userId, roleInClass: 'Teacher' } } } - ], - isDeleted: false - }, - select: { id: true } - }); - const classIds = classes.map(c => c.id); - - if (classIds.length === 0) { - return res.json([]); - } - - // 获取这些班级的作业 - const assignments = await prisma.assignment.findMany({ - where: { classId: { in: classIds }, isDeleted: false }, - select: { id: true } - }); - const assignmentIds = assignments.map(a => a.id); - - // 获取所有已批改作业的分数 - const submissions = await prisma.studentSubmission.findMany({ - where: { - assignmentId: { in: assignmentIds }, - submissionStatus: 'Graded', - isDeleted: false - }, - select: { totalScore: true } - }); - - const scores = submissions.map(s => Number(s.totalScore)); - const distribution = [ - { range: '0-60', count: 0 }, - { range: '60-70', count: 0 }, - { range: '70-80', count: 0 }, - { range: '80-90', count: 0 }, - { range: '90-100', count: 0 } - ]; - - scores.forEach(score => { - if (score < 60) distribution[0].count++; - else if (score < 70) distribution[1].count++; - else if (score < 80) distribution[2].count++; - else if (score < 90) distribution[3].count++; - else distribution[4].count++; - }); - + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const distribution = await analyticsService.getScoreDistribution(req.userId); res.json(distribution); } catch (error) { console.error('Get score distribution error:', error); @@ -202,89 +76,9 @@ export const getScoreDistribution = async (req: AuthRequest, res: Response) => { // 获取教师统计数据(活跃学生、平均分、待批改、及格率) export const getTeacherStats = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - // 获取教师管理的班级 - const classes = await prisma.class.findMany({ - where: { - OR: [ - { headTeacherId: userId }, - { members: { some: { userId: userId, roleInClass: 'Teacher' } } } - ], - isDeleted: false - }, - select: { id: true } - }); - - const classIds = classes.map(c => c.id); - - if (classIds.length === 0) { - return res.json({ - activeStudents: 0, - averageScore: 0, - pendingGrading: 0, - passRate: 0 - }); - } - - // 1. 活跃学生数:这些班级中的学生总数 - const activeStudents = await prisma.classMember.count({ - where: { - classId: { in: classIds }, - roleInClass: 'Student', - isDeleted: false - } - }); - - // 2. 获取这些班级的作业 - const assignments = await prisma.assignment.findMany({ - where: { - classId: { in: classIds }, - isDeleted: false - }, - select: { id: true } - }); - - const assignmentIds = assignments.map(a => a.id); - - // 3. 待批改数 - const pendingGrading = await prisma.studentSubmission.count({ - where: { - assignmentId: { in: assignmentIds }, - submissionStatus: 'Submitted', - isDeleted: false - } - }); - - // 4. 已批改的提交(用于计算平均分和及格率) - const gradedSubmissions = await prisma.studentSubmission.findMany({ - where: { - assignmentId: { in: assignmentIds }, - submissionStatus: 'Graded', - isDeleted: false - }, - select: { totalScore: true } - }); - - let averageScore = 0; - let passRate = 0; - - if (gradedSubmissions.length > 0) { - const scores = gradedSubmissions.map(s => Number(s.totalScore)); - const sum = scores.reduce((a, b) => a + b, 0); - averageScore = Number((sum / scores.length).toFixed(1)); - - const passedCount = scores.filter(score => score >= 60).length; - passRate = Number(((passedCount / scores.length) * 100).toFixed(1)); - } - - res.json({ - activeStudents, - averageScore, - pendingGrading, - passRate - }); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const stats = await analyticsService.getTeacherStats(req.userId); + res.json(stats); } catch (error) { console.error('Get teacher stats error:', error); res.status(500).json({ error: 'Failed to get teacher stats' }); @@ -293,84 +87,9 @@ export const getTeacherStats = async (req: AuthRequest, res: Response) => { export const getExamStats = async (req: AuthRequest, res: Response) => { try { - const { id: examId } = req.params as any; - const assignments = await prisma.assignment.findMany({ - where: { examId, isDeleted: false }, - select: { id: true } - }); - - const assignmentIds = assignments.map(a => a.id); - - const gradedSubmissions = await prisma.studentSubmission.findMany({ - where: { assignmentId: { in: assignmentIds }, submissionStatus: 'Graded', isDeleted: false }, - select: { id: true, totalScore: true } - }); - - const scores = gradedSubmissions.map(s => Number(s.totalScore)); - const averageScore = scores.length > 0 ? Number((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1)) : 0; - const maxScore = scores.length > 0 ? Math.max(...scores) : 0; - const minScore = scores.length > 0 ? Math.min(...scores) : 0; - const passRate = scores.length > 0 ? Number(((scores.filter(s => s >= 60).length / scores.length) * 100).toFixed(1)) : 0; - - const distribution = [ - { range: '0-60', count: 0 }, - { range: '60-70', count: 0 }, - { range: '70-80', count: 0 }, - { range: '80-90', count: 0 }, - { range: '90-100', count: 0 } - ]; - scores.forEach(score => { - if (score < 60) distribution[0].count++; - else if (score < 70) distribution[1].count++; - else if (score < 80) distribution[2].count++; - else if (score < 90) distribution[3].count++; - else distribution[4].count++; - }); - - const examNodes = await prisma.examNode.findMany({ - where: { examId, isDeleted: false }, - select: { - id: true, - questionId: true, - question: { select: { content: true, difficulty: true, questionType: true } } - } - }); - const nodeIds = examNodes.map(n => n.id); - const submissionIds = gradedSubmissions.map(s => s.id); - - const details = await prisma.submissionDetail.findMany({ - where: { examNodeId: { in: nodeIds }, submissionId: { in: submissionIds }, isDeleted: false }, - select: { examNodeId: true, judgement: true } - }); - - const statsMap = new Map(); - for (const d of details) { - const s = statsMap.get(d.examNodeId) || { total: 0, wrong: 0 }; - s.total += 1; - if (d.judgement === 'Incorrect') s.wrong += 1; - statsMap.set(d.examNodeId, s); - } - - const wrongQuestions = examNodes.map(n => { - const s = statsMap.get(n.id) || { total: 0, wrong: 0 }; - const errorRate = s.total > 0 ? Math.round((s.wrong / s.total) * 100) : 0; - return { - id: n.questionId || n.id, - content: n.question?.content || '', - errorRate, - difficulty: n.question?.difficulty || 0, - type: n.question?.questionType || 'Unknown' - }; - }).sort((a, b) => b.errorRate - a.errorRate).slice(0, 20); - - res.json({ - averageScore, - passRate, - maxScore, - minScore, - scoreDistribution: distribution, - wrongQuestions - }); + const { id } = req.params as any; + const result = await analyticsService.getExamStats(id); + res.json(result); } catch (error) { console.error('Get exam stats error:', error); res.status(500).json({ error: 'Failed to get exam stats' }); diff --git a/backend/src/controllers/assignment.controller.ts b/backend/src/controllers/assignment.controller.ts index 7b727c7..d2e8282 100644 --- a/backend/src/controllers/assignment.controller.ts +++ b/backend/src/controllers/assignment.controller.ts @@ -1,84 +1,19 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth.middleware'; -import prisma from '../utils/prisma'; -import { v4 as uuidv4 } from 'uuid'; +import { assignmentService } from '../services/assignment.service'; // GET /api/assignments/teaching -// 获取我教的班级的作业列表(教师视角) +// 获取发布的作业列表(教师视角) export const getTeachingAssignments = async (req: AuthRequest, res: Response) => { try { - // 查询我作为教师的所有班级 - const myClasses = await prisma.classMember.findMany({ - where: { - userId: req.userId!, - roleInClass: 'Teacher', - isDeleted: false - }, - select: { classId: true } - }); - - const classIds = myClasses.map(m => m.classId); - - if (classIds.length === 0) { - return res.json({ items: [], totalCount: 0, pageIndex: 1, pageSize: 10 }); - } - - // 查询这些班级的作业 - const assignments = await prisma.assignment.findMany({ - where: { - classId: { in: classIds }, - isDeleted: false - }, - include: { - exam: { - select: { - title: true, - totalScore: true - } - }, - class: { - include: { - grade: true - } - }, - _count: { - select: { submissions: true } - }, - submissions: { - where: { - submissionStatus: { in: ['Submitted', 'Graded'] } - }, - select: { id: true } - } - }, - orderBy: { createdAt: 'desc' } - }); - - // 格式化返回数据 - const items = assignments.map(assignment => { - const totalCount = assignment._count.submissions; - const submittedCount = assignment.submissions.length; - - return { - id: assignment.id, - title: assignment.title, - examTitle: assignment.exam.title, - className: assignment.class.name, - gradeName: assignment.class.grade.name, - submittedCount, - totalCount, - status: new Date() > assignment.endTime ? 'Closed' : 'Active', - dueDate: assignment.endTime.toISOString(), - createdAt: assignment.createdAt.toISOString() - }; - }); - - res.json({ - items, - totalCount: items.length, - pageIndex: 1, - pageSize: 10 + const { classId, examType, subjectId, status } = req.query; + const result = await assignmentService.getTeachingAssignments(req.userId!, { + classId: classId as string, + examType: examType as string, + subjectId: subjectId as string, + status: status as string }); + res.json(result); } catch (error) { console.error('Get teaching assignments error:', error); res.status(500).json({ error: 'Failed to get teaching assignments' }); @@ -89,51 +24,13 @@ export const getTeachingAssignments = async (req: AuthRequest, res: Response) => // 获取我的作业列表(学生视角) export const getStudentAssignments = async (req: AuthRequest, res: Response) => { try { - // 查询我作为学生的所有提交记录 - const submissions = await prisma.studentSubmission.findMany({ - where: { - studentId: req.userId!, - isDeleted: false - }, - include: { - assignment: { - include: { - exam: { - select: { - title: true, - totalScore: true - } - }, - class: { - include: { - grade: true - } - } - } - } - }, - orderBy: { createdAt: 'desc' } - }); - - // 格式化返回数据 - const items = submissions.map(submission => ({ - id: submission.assignment.id, - title: submission.assignment.title, - examTitle: submission.assignment.exam.title, - className: submission.assignment.class.name, - startTime: submission.assignment.startTime.toISOString(), - endTime: submission.assignment.endTime.toISOString(), - status: submission.submissionStatus, - score: submission.totalScore ? Number(submission.totalScore) : null, - submitTime: submission.submitTime?.toISOString() || null - })); - - res.json({ - items, - totalCount: items.length, - pageIndex: 1, - pageSize: 10 - }); + const filters = { + subjectId: req.query.subjectId as string, + examType: req.query.examType as string, + status: req.query.status as string + }; + const result = await assignmentService.getStudentAssignments(req.userId!, filters); + res.json(result); } catch (error) { console.error('Get student assignments error:', error); res.status(500).json({ error: 'Failed to get student assignments' }); @@ -145,165 +42,93 @@ export const getStudentAssignments = async (req: AuthRequest, res: Response) => export const createAssignment = async (req: AuthRequest, res: Response) => { try { const { examId, classId, title, startTime, endTime, allowLateSubmission, autoScoreEnabled } = req.body; - if (!examId || !classId || !title || !startTime || !endTime) { return res.status(400).json({ error: 'Missing required fields' }); } - - // 验证试卷存在且已发布 - const exam = await prisma.exam.findUnique({ - where: { id: examId, isDeleted: false } - }); - - if (!exam) { - return res.status(404).json({ error: 'Exam not found' }); + try { + const result = await assignmentService.createAssignment(req.userId!, { examId, classId, title, startTime, endTime, allowLateSubmission, autoScoreEnabled }); + res.json(result); + } catch (e: any) { + if (e.message === 'Exam not found') return res.status(404).json({ error: e.message }); + if (e.message === 'You are not a teacher of this class') return res.status(403).json({ error: e.message }); + if (e.message.includes('Exam must be published')) return res.status(400).json({ error: e.message }); + if (e.message === 'Invalid startTime or endTime') return res.status(400).json({ error: e.message }); + if (e.message === 'startTime must be earlier than endTime') return res.status(400).json({ error: e.message }); + throw e; } - - if (exam.status !== 'Published') { - return res.status(400).json({ error: 'Exam must be published before creating assignment' }); - } - - // 验证我是该班级的教师 - const membership = await prisma.classMember.findFirst({ - where: { - classId, - userId: req.userId!, - roleInClass: 'Teacher', - isDeleted: false - } - }); - - if (!membership) { - return res.status(403).json({ error: 'You are not a teacher of this class' }); - } - - // 获取班级所有学生 - const students = await prisma.classMember.findMany({ - where: { - classId, - roleInClass: 'Student', - isDeleted: false - }, - select: { userId: true } - }); - - // 创建作业 - const assignmentId = uuidv4(); - const assignment = await prisma.assignment.create({ - data: { - id: assignmentId, - examId, - classId, - title, - startTime: new Date(startTime), - endTime: new Date(endTime), - allowLateSubmission: allowLateSubmission ?? false, - autoScoreEnabled: autoScoreEnabled ?? true, - createdBy: req.userId!, - updatedBy: req.userId! - } - }); - - // 为所有学生创建提交记录 - const submissionPromises = students.map(student => - prisma.studentSubmission.create({ - data: { - id: uuidv4(), - assignmentId, - studentId: student.userId, - submissionStatus: 'Pending', - createdBy: req.userId!, - updatedBy: req.userId! - } - }) - ); - - await Promise.all(submissionPromises); - - res.json({ - id: assignment.id, - title: assignment.title, - message: `Assignment created successfully for ${students.length} students` - }); } catch (error) { console.error('Create assignment error:', error); res.status(500).json({ error: 'Failed to create assignment' }); } }; +// PUT /api/assignments/:id +// 更新作业信息(如截止时间) +export const updateAssignment = async (req: AuthRequest, res: Response) => { + try { + const { id } = req.params; + const result = await assignmentService.updateAssignment(req.userId!, id, req.body); + res.json(result); + } catch (error: any) { + console.error('Update assignment error:', error); + if (error.message === 'Assignment not found') return res.status(404).json({ error: error.message }); + if (error.message === 'You are not a teacher of this class') return res.status(403).json({ error: error.message }); + if (error.message === 'startTime must be earlier than endTime') return res.status(400).json({ error: error.message }); + res.status(500).json({ error: 'Failed to update assignment' }); + } +}; + +// DELETE /api/assignments/:id +export const deleteAssignment = async (req: AuthRequest, res: Response) => { + try { + const { id } = req.params; + const result = await assignmentService.deleteAssignment(req.userId!, id); + res.json(result); + } catch (error: any) { + console.error('Delete assignment error:', error); + if (error.message === 'Assignment not found') return res.status(404).json({ error: error.message }); + if (error.message === 'You are not a teacher of this class') return res.status(403).json({ error: error.message }); + res.status(500).json({ error: 'Failed to delete assignment' }); + } +}; + +// POST /api/assignments/:id/archive +export const archiveAssignment = async (req: AuthRequest, res: Response) => { + try { + const { id } = req.params; + const result = await assignmentService.archiveAssignment(req.userId!, id); + res.json(result); + } catch (error: any) { + console.error('Archive assignment error:', error); + res.status(500).json({ error: 'Failed to archive assignment' }); + } +}; + +// GET /api/assignments/:id/analysis +// 获取作业详细分析(试卷详解 + 数据总览) +export const getAssignmentAnalysis = async (req: AuthRequest, res: Response) => { + try { + const result = await assignmentService.getAssignmentAnalysis(req.userId!, req.params.id); + res.json(result); + } catch (error) { + console.error('Get assignment analysis error:', error); + res.status(500).json({ error: 'Failed to get assignment analysis' }); + } +}; + // GET /api/assignments/:id/stats // 获取作业统计信息 export const getAssignmentStats = async (req: AuthRequest, res: Response) => { try { - const { id: assignmentId } = req.params; - - // 验证作业存在 - const assignment = await prisma.assignment.findUnique({ - where: { id: assignmentId, isDeleted: false }, - include: { - class: true - } - }); - - if (!assignment) { - return res.status(404).json({ error: 'Assignment not found' }); + const { id } = req.params; + try { + const result = await assignmentService.getAssignmentStats(req.userId!, id); + res.json(result); + } catch (e: any) { + if (e.message === 'Assignment not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + throw e; } - - // 验证权限(教师) - const isMember = await prisma.classMember.findFirst({ - where: { - classId: assignment.classId, - userId: req.userId!, - roleInClass: 'Teacher', - isDeleted: false - } - }); - - if (!isMember) { - return res.status(403).json({ error: 'Permission denied' }); - } - - // 统计提交情况 - const submissions = await prisma.studentSubmission.findMany({ - where: { - assignmentId, - isDeleted: false - }, - select: { - submissionStatus: true, - totalScore: true - } - }); - - const totalCount = submissions.length; - const submittedCount = submissions.filter(s => - s.submissionStatus === 'Submitted' || s.submissionStatus === 'Graded' - ).length; - const gradedCount = submissions.filter(s => s.submissionStatus === 'Graded').length; - - // 计算平均分(只统计已批改的) - const gradedScores = submissions - .filter(s => s.submissionStatus === 'Graded' && s.totalScore !== null) - .map(s => Number(s.totalScore)); - - const averageScore = gradedScores.length > 0 - ? gradedScores.reduce((sum, score) => sum + score, 0) / gradedScores.length - : 0; - - const maxScore = gradedScores.length > 0 ? Math.max(...gradedScores) : 0; - const minScore = gradedScores.length > 0 ? Math.min(...gradedScores) : 0; - - res.json({ - totalStudents: totalCount, - submittedCount, - gradedCount, - pendingCount: totalCount - submittedCount, - averageScore: Math.round(averageScore * 10) / 10, - maxScore, - minScore, - passRate: 0, // TODO: 需要定义及格线 - scoreDistribution: [] // TODO: 可以实现分数段分布 - }); } catch (error) { console.error('Get assignment stats error:', error); res.status(500).json({ error: 'Failed to get assignment stats' }); diff --git a/backend/src/controllers/common.controller.ts b/backend/src/controllers/common.controller.ts index 6ce520f..49a28b4 100644 --- a/backend/src/controllers/common.controller.ts +++ b/backend/src/controllers/common.controller.ts @@ -1,18 +1,12 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth.middleware'; -import prisma from '../utils/prisma'; +import { commonService } from '../services/common.service'; // 获取消息列表 export const getMessages = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - const messages = await prisma.message.findMany({ - where: { userId }, - orderBy: { createdAt: 'desc' } - }); - + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const messages = await commonService.getMessages(req.userId); res.json(messages); } catch (error) { console.error('Get messages error:', error); @@ -24,18 +18,14 @@ export const getMessages = async (req: AuthRequest, res: Response) => { export const markMessageRead = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const userId = req.userId; - - const message = await prisma.message.findUnique({ where: { id } }); - if (!message) return res.status(404).json({ error: 'Message not found' }); - if (message.userId !== userId) return res.status(403).json({ error: 'Forbidden' }); - - await prisma.message.update({ - where: { id }, - data: { isRead: true } - }); - - res.json({ success: true }); + try { + const result = await commonService.markMessageRead(req.userId!, id); + res.json(result); + } catch (e: any) { + if (e.message === 'Message not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Forbidden') return res.status(403).json({ error: e.message }); + throw e; + } } catch (error) { console.error('Mark message read error:', error); res.status(500).json({ error: 'Failed to mark message read' }); @@ -45,26 +35,14 @@ export const markMessageRead = async (req: AuthRequest, res: Response) => { // 创建消息 export const createMessage = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - const { title, content, type } = req.body; - - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - if (!title || !content) { - return res.status(400).json({ error: 'Title and content are required' }); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + try { + const message = await commonService.createMessage(req.userId!, req.body); + res.json(message); + } catch (e: any) { + if (e.message === 'Title and content are required') return res.status(400).json({ error: e.message }); + throw e; } - - const message = await prisma.message.create({ - data: { - userId, - title, - content, - type: type || 'System', - senderName: 'Me', - isRead: false - } - }); - - res.json(message); } catch (error) { console.error('Create message error:', error); res.status(500).json({ error: 'Failed to create message' }); @@ -74,42 +52,14 @@ export const createMessage = async (req: AuthRequest, res: Response) => { // 获取日程 export const getSchedule = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - // 获取用户关联的班级 - const user = await prisma.applicationUser.findUnique({ - where: { id: userId }, - include: { - classMemberships: { - include: { class: true } - } - } - }); - - if (!user) return res.status(404).json({ error: 'User not found' }); - - const classIds = user.classMemberships.map(cm => cm.classId); - - // 获取这些班级的日程 - const schedules = await prisma.schedule.findMany({ - where: { classId: { in: classIds } }, - include: { class: true } - }); - - const scheduleDtos = schedules.map(s => ({ - id: s.id, - startTime: s.startTime, - endTime: s.endTime, - className: s.class.name, - subject: s.subject, - room: s.room || '', - isToday: s.dayOfWeek === new Date().getDay(), - dayOfWeek: s.dayOfWeek, - period: s.period - })); - - res.json(scheduleDtos); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + try { + const data = await commonService.getSchedule(req.userId); + res.json(data); + } catch (e: any) { + if (e.message === 'User not found') return res.status(404).json({ error: e.message }); + throw e; + } } catch (error) { console.error('Get schedule error:', error); res.status(500).json({ error: 'Failed to get schedule' }); @@ -118,45 +68,20 @@ export const getSchedule = async (req: AuthRequest, res: Response) => { // 获取周日程 export const getWeekSchedule = async (req: AuthRequest, res: Response) => { - // 复用 getSchedule 逻辑,因为我们返回了所有日程 return getSchedule(req, res); }; // 添加日程 (仅教师) export const addEvent = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - const { subject, className, classId, room, dayOfWeek, period, startTime, endTime } = req.body; - - let resolvedClassId: string | null = null; - if (classId) { - const clsById = await prisma.class.findUnique({ where: { id: classId } }); - if (!clsById) return res.status(404).json({ error: 'Class not found' }); - resolvedClassId = clsById.id; - } else if (className) { - const clsByName = await prisma.class.findFirst({ where: { name: className } }); - if (!clsByName) return res.status(404).json({ error: 'Class not found' }); - resolvedClassId = clsByName.id; - } else { - return res.status(400).json({ error: 'classId or className is required' }); + try { + const result = await commonService.addEvent(req.userId!, req.body); + res.status(201).json(result); + } catch (e: any) { + if (e.message === 'Class not found') return res.status(404).json({ error: e.message }); + if (e.message === 'classId or className is required') return res.status(400).json({ error: e.message }); + throw e; } - - // 检查权限 (简化:假设所有教师都可以添加) - // 实际应检查是否是该班级的教师 - - await prisma.schedule.create({ - data: { - classId: resolvedClassId!, - subject, - room, - dayOfWeek, - period, - startTime, - endTime - } - }); - - res.status(201).json({ success: true }); } catch (error) { console.error('Add event error:', error); res.status(500).json({ error: 'Failed to add event' }); @@ -167,8 +92,8 @@ export const addEvent = async (req: AuthRequest, res: Response) => { export const deleteEvent = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - await prisma.schedule.delete({ where: { id } }); - res.json({ success: true }); + const result = await commonService.deleteEvent(id); + res.json(result); } catch (error) { console.error('Delete event error:', error); res.status(500).json({ error: 'Failed to delete event' }); diff --git a/backend/src/controllers/config.controller.ts b/backend/src/controllers/config.controller.ts new file mode 100644 index 0000000..102cb12 --- /dev/null +++ b/backend/src/controllers/config.controller.ts @@ -0,0 +1,17 @@ +import { Request, Response } from 'express'; +import { configService } from '../services/config.service'; + +export const testDbConnection = async (req: Request, res: Response) => { + try { + const { host, port, user, password, database } = req.body || {}; + try { + const result = await configService.testDbConnection({ host, port, user, password, database }); + return res.json(result); + } catch (e: any) { + if (e.message === 'Missing required fields') return res.status(400).json({ error: e.message }); + return res.status(500).json({ error: e.message || 'Connection failed' }); + } + } catch (e: any) { + return res.status(500).json({ error: e?.message || 'Connection failed' }); + } +}; diff --git a/backend/src/controllers/curriculum.controller.ts b/backend/src/controllers/curriculum.controller.ts index e65f9fa..b34feb6 100644 --- a/backend/src/controllers/curriculum.controller.ts +++ b/backend/src/controllers/curriculum.controller.ts @@ -1,22 +1,12 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth.middleware'; -import prisma from '../utils/prisma'; +import { curriculumService } from '../services/curriculum.service'; // GET /api/curriculum/subjects // 获取学科列表 export const getSubjects = async (req: AuthRequest, res: Response) => { try { - const subjects = await prisma.subject.findMany({ - where: { isDeleted: false }, - select: { - id: true, - name: true, - code: true, - icon: true - }, - orderBy: { name: 'asc' } - }); - + const subjects = await curriculumService.getSubjects(); res.json(subjects); } catch (error) { console.error('Get subjects error:', error); @@ -30,90 +20,13 @@ export const getSubjects = async (req: AuthRequest, res: Response) => { export const getTextbookTree = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - - // 尝试作为 textbook ID 查找 - let textbook = await prisma.textbook.findUnique({ - where: { id, isDeleted: false }, - include: { - units: { - where: { isDeleted: false }, - include: { - lessons: { - where: { isDeleted: false }, - include: { - knowledgePoints: { - where: { isDeleted: false }, - orderBy: { difficulty: 'asc' } - } - }, - orderBy: { sortOrder: 'asc' } - } - }, - orderBy: { sortOrder: 'asc' } - } - } - }); - - // 如果找不到,尝试作为 subject ID 查找第一个教材 - if (!textbook) { - textbook = await prisma.textbook.findFirst({ - where: { subjectId: id, isDeleted: false }, - include: { - units: { - where: { isDeleted: false }, - include: { - lessons: { - where: { isDeleted: false }, - include: { - knowledgePoints: { - where: { isDeleted: false }, - orderBy: { difficulty: 'asc' } - } - }, - orderBy: { sortOrder: 'asc' } - } - }, - orderBy: { sortOrder: 'asc' } - } - } - }); + try { + const result = await curriculumService.getTextbookTree(id); + res.json(result); + } catch (e: any) { + if (e.message === 'Textbook not found') return res.status(404).json({ error: e.message }); + throw e; } - - if (!textbook) { - return res.status(404).json({ error: 'Textbook not found' }); - } - - // 格式化返回数据 - const units = textbook.units.map(unit => ({ - id: unit.id, - textbookId: unit.textbookId, - name: unit.name, - sortOrder: unit.sortOrder, - lessons: unit.lessons.map(lesson => ({ - id: lesson.id, - unitId: lesson.unitId, - name: lesson.name, - sortOrder: lesson.sortOrder, - knowledgePoints: lesson.knowledgePoints.map(kp => ({ - id: kp.id, - lessonId: kp.lessonId, - name: kp.name, - difficulty: kp.difficulty, - description: kp.description - })) - })) - })); - - res.json({ - textbook: { - id: textbook.id, - name: textbook.name, - publisher: textbook.publisher, - versionYear: textbook.versionYear, - coverUrl: textbook.coverUrl - }, - units - }); } catch (error) { console.error('Get textbook tree error:', error); res.status(500).json({ error: 'Failed to get textbook tree' }); @@ -126,17 +39,7 @@ export const getTextbookTree = async (req: AuthRequest, res: Response) => { export const getTextbooksBySubject = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const textbooks = await prisma.textbook.findMany({ - where: { subjectId: id, isDeleted: false }, - select: { - id: true, - name: true, - publisher: true, - versionYear: true, - coverUrl: true - }, - orderBy: { name: 'asc' } - }); + const textbooks = await curriculumService.getTextbooksBySubject(id); res.json(textbooks); } catch (error) { console.error('Get textbooks error:', error); @@ -147,21 +50,8 @@ export const getTextbooksBySubject = async (req: AuthRequest, res: Response) => // POST /api/curriculum/textbooks export const createTextbook = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - const { subjectId, name, publisher, versionYear, coverUrl } = req.body; - const textbook = await prisma.textbook.create({ - data: { - subjectId, - name, - publisher, - versionYear, - coverUrl: coverUrl || '', - createdBy: userId, - updatedBy: userId - } - }); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const textbook = await curriculumService.createTextbook(req.userId!, req.body); res.json(textbook); } catch (error) { console.error('Create textbook error:', error); @@ -173,11 +63,7 @@ export const createTextbook = async (req: AuthRequest, res: Response) => { export const updateTextbook = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const { name, publisher, versionYear, coverUrl } = req.body; - const textbook = await prisma.textbook.update({ - where: { id }, - data: { name, publisher, versionYear, coverUrl } - }); + const textbook = await curriculumService.updateTextbook(id, req.body); res.json(textbook); } catch (error) { console.error('Update textbook error:', error); @@ -189,11 +75,8 @@ export const updateTextbook = async (req: AuthRequest, res: Response) => { export const deleteTextbook = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - await prisma.textbook.update({ - where: { id }, - data: { isDeleted: true } - }); - res.json({ success: true }); + const result = await curriculumService.deleteTextbook(id); + res.json(result); } catch (error) { console.error('Delete textbook error:', error); res.status(500).json({ error: 'Failed to delete textbook' }); @@ -205,19 +88,8 @@ export const deleteTextbook = async (req: AuthRequest, res: Response) => { // POST /api/curriculum/units export const createUnit = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - const { textbookId, name, sortOrder } = req.body; - const unit = await prisma.textbookUnit.create({ - data: { - textbookId, - name, - sortOrder: sortOrder || 0, - createdBy: userId, - updatedBy: userId - } - }); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const unit = await curriculumService.createUnit(req.userId!, req.body); res.json(unit); } catch (error) { console.error('Create unit error:', error); @@ -229,11 +101,7 @@ export const createUnit = async (req: AuthRequest, res: Response) => { export const updateUnit = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const { name, sortOrder } = req.body; - const unit = await prisma.textbookUnit.update({ - where: { id }, - data: { name, sortOrder } - }); + const unit = await curriculumService.updateUnit(id, req.body); res.json(unit); } catch (error) { console.error('Update unit error:', error); @@ -245,11 +113,8 @@ export const updateUnit = async (req: AuthRequest, res: Response) => { export const deleteUnit = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - await prisma.textbookUnit.update({ - where: { id }, - data: { isDeleted: true } - }); - res.json({ success: true }); + const result = await curriculumService.deleteUnit(id); + res.json(result); } catch (error) { console.error('Delete unit error:', error); res.status(500).json({ error: 'Failed to delete unit' }); @@ -261,19 +126,8 @@ export const deleteUnit = async (req: AuthRequest, res: Response) => { // POST /api/curriculum/lessons export const createLesson = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - const { unitId, name, sortOrder } = req.body; - const lesson = await prisma.textbookLesson.create({ - data: { - unitId, - name, - sortOrder: sortOrder || 0, - createdBy: userId, - updatedBy: userId - } - }); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const lesson = await curriculumService.createLesson(req.userId!, req.body); res.json(lesson); } catch (error) { console.error('Create lesson error:', error); @@ -285,11 +139,7 @@ export const createLesson = async (req: AuthRequest, res: Response) => { export const updateLesson = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const { name, sortOrder } = req.body; - const lesson = await prisma.textbookLesson.update({ - where: { id }, - data: { name, sortOrder } - }); + const lesson = await curriculumService.updateLesson(id, req.body); res.json(lesson); } catch (error) { console.error('Update lesson error:', error); @@ -301,11 +151,8 @@ export const updateLesson = async (req: AuthRequest, res: Response) => { export const deleteLesson = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - await prisma.textbookLesson.update({ - where: { id }, - data: { isDeleted: true } - }); - res.json({ success: true }); + const result = await curriculumService.deleteLesson(id); + res.json(result); } catch (error) { console.error('Delete lesson error:', error); res.status(500).json({ error: 'Failed to delete lesson' }); @@ -317,20 +164,8 @@ export const deleteLesson = async (req: AuthRequest, res: Response) => { // POST /api/curriculum/knowledge-points export const createKnowledgePoint = async (req: AuthRequest, res: Response) => { try { - const userId = req.userId; - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - - const { lessonId, name, difficulty, description } = req.body; - const point = await prisma.knowledgePoint.create({ - data: { - lessonId, - name, - difficulty: difficulty || 1, - description: description || '', - createdBy: userId, - updatedBy: userId - } - }); + if (!req.userId) return res.status(401).json({ error: 'Unauthorized' }); + const point = await curriculumService.createKnowledgePoint(req.userId!, req.body); res.json(point); } catch (error) { console.error('Create knowledge point error:', error); @@ -342,11 +177,7 @@ export const createKnowledgePoint = async (req: AuthRequest, res: Response) => { export const updateKnowledgePoint = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const { name, difficulty, description } = req.body; - const point = await prisma.knowledgePoint.update({ - where: { id }, - data: { name, difficulty, description } - }); + const point = await curriculumService.updateKnowledgePoint(id, req.body); res.json(point); } catch (error) { console.error('Update knowledge point error:', error); @@ -358,11 +189,8 @@ export const updateKnowledgePoint = async (req: AuthRequest, res: Response) => { export const deleteKnowledgePoint = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - await prisma.knowledgePoint.update({ - where: { id }, - data: { isDeleted: true } - }); - res.json({ success: true }); + const result = await curriculumService.deleteKnowledgePoint(id); + res.json(result); } catch (error) { console.error('Delete knowledge point error:', error); res.status(500).json({ error: 'Failed to delete knowledge point' }); diff --git a/backend/src/controllers/exam.controller.ts b/backend/src/controllers/exam.controller.ts index 7eec781..3d0a6eb 100644 --- a/backend/src/controllers/exam.controller.ts +++ b/backend/src/controllers/exam.controller.ts @@ -5,10 +5,14 @@ import { examService } from '../services/exam.service'; // GET /api/exams export const getExams = async (req: AuthRequest, res: Response) => { try { - const { subjectId, status } = req.query; + const { subjectId, status, scope, page, pageSize, examType } = req.query; const result = await examService.getExams(req.userId!, { subjectId: subjectId as string, - status: status as string + status: status as string, + scope: scope as 'mine' | 'public', + page: page ? Number(page) : 1, + pageSize: pageSize ? Number(pageSize) : 20, + examType: examType as string }); res.json(result); } catch (error) { diff --git a/backend/src/controllers/grading.controller.ts b/backend/src/controllers/grading.controller.ts index 404f3d6..d3d952a 100644 --- a/backend/src/controllers/grading.controller.ts +++ b/backend/src/controllers/grading.controller.ts @@ -1,72 +1,20 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth.middleware'; -import prisma from '../utils/prisma'; -import { v4 as uuidv4 } from 'uuid'; +import { gradingService } from '../services/grading.service'; // GET /api/grading/:assignmentId/list // 获取作业的所有学生提交列表 export const getSubmissions = async (req: AuthRequest, res: Response) => { try { const { assignmentId } = req.params; - - // 验证作业存在 - const assignment = await prisma.assignment.findUnique({ - where: { id: assignmentId, isDeleted: false }, - include: { class: true } - }); - - if (!assignment) { - return res.status(404).json({ error: 'Assignment not found' }); + try { + const items = await gradingService.getSubmissions(req.userId!, assignmentId); + res.json(items); + } catch (e: any) { + if (e.message === 'Assignment not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + throw e; } - - // 验证权限(必须是班级教师) - const isMember = await prisma.classMember.findFirst({ - where: { - classId: assignment.classId, - userId: req.userId!, - roleInClass: 'Teacher', - isDeleted: false - } - }); - - if (!isMember) { - return res.status(403).json({ error: 'Permission denied' }); - } - - // 获取所有提交 - const submissions = await prisma.studentSubmission.findMany({ - where: { - assignmentId, - isDeleted: false - }, - include: { - student: { - select: { - id: true, - realName: true, - studentId: true, - avatarUrl: true - } - } - }, - orderBy: [ - { submissionStatus: 'asc' }, // 待批改的在前 - { submitTime: 'desc' } - ] - }); - - // 格式化返回数据 - const items = submissions.map(submission => ({ - id: submission.id, - studentName: submission.student.realName, - studentId: submission.student.studentId, - avatarUrl: submission.student.avatarUrl, - status: submission.submissionStatus, - score: submission.totalScore ? Number(submission.totalScore) : null, - submitTime: submission.submitTime?.toISOString() || null - })); - - res.json(items); } catch (error) { console.error('Get submissions error:', error); res.status(500).json({ error: 'Failed to get submissions' }); @@ -78,109 +26,14 @@ export const getSubmissions = async (req: AuthRequest, res: Response) => { export const getPaperForGrading = async (req: AuthRequest, res: Response) => { try { const { submissionId } = req.params; - - // 获取提交记录 - const submission = await prisma.studentSubmission.findUnique({ - where: { id: submissionId, isDeleted: false }, - include: { - student: { - select: { - realName: true, - studentId: true - } - }, - assignment: { - include: { - exam: { - include: { - nodes: { - where: { isDeleted: false }, - include: { - question: { - include: { - knowledgePoints: { - select: { - knowledgePoint: { - select: { name: true } - } - } - } - } - } - }, - orderBy: { sortOrder: 'asc' } - } - } - }, - class: true - } - }, - details: { - include: { - examNode: { - include: { - question: true - } - } - } - } - } - }); - - if (!submission) { - return res.status(404).json({ error: 'Submission not found' }); + try { + const result = await gradingService.getPaperForGrading(req.userId!, submissionId); + res.json(result); + } catch (e: any) { + if (e.message === 'Submission not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + throw e; } - - // 验证权限 - const isMember = await prisma.classMember.findFirst({ - where: { - classId: submission.assignment.classId, - userId: req.userId!, - roleInClass: 'Teacher', - isDeleted: false - } - }); - - if (!isMember) { - return res.status(403).json({ error: 'Permission denied' }); - } - - // 构建答题详情(包含学生答案和批改信息) - const nodes = submission.assignment.exam.nodes.map(node => { - const detail = submission.details.find(d => d.examNodeId === node.id); - - return { - examNodeId: node.id, - questionId: node.questionId, - questionContent: node.question?.content, - questionType: node.question?.questionType, - // 构造完整的 question 对象以供前端使用 - question: node.question ? { - id: node.question.id, - content: node.question.content, - type: node.question.questionType, - difficulty: node.question.difficulty, - answer: node.question.answer, - parse: node.question.explanation, - knowledgePoints: node.question.knowledgePoints.map((kp: any) => kp.knowledgePoint.name) - } : undefined, - score: Number(node.score), - studentAnswer: detail?.studentAnswer || null, - studentScore: detail?.score ? Number(detail.score) : null, - judgement: detail?.judgement || null, - teacherComment: detail?.teacherComment || null - }; - }); - - res.json({ - submissionId: submission.id, - studentName: submission.student.realName, - studentId: submission.student.studentId, - status: submission.submissionStatus, - totalScore: submission.totalScore ? Number(submission.totalScore) : null, - submitTime: submission.submitTime?.toISOString() || null, - nodes - }); } catch (error) { console.error('Get paper for grading error:', error); res.status(500).json({ error: 'Failed to get paper' }); @@ -192,114 +45,20 @@ export const getPaperForGrading = async (req: AuthRequest, res: Response) => { export const submitGrade = async (req: AuthRequest, res: Response) => { try { const { submissionId } = req.params; - const { grades } = req.body; // Array of { examNodeId, score, judgement, teacherComment } - - if (!grades || !Array.isArray(grades)) { - return res.status(400).json({ error: 'Invalid grades data' }); + const { grades } = req.body; + try { + const result = await gradingService.submitGrade(req.userId!, submissionId, grades); + res.json(result); + } catch (e: any) { + if (e.message === 'Invalid grades data') return res.status(400).json({ error: e.message }); + if (e.message === 'Submission not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + if (e.message === 'Submission has not been submitted') return res.status(400).json({ error: e.message }); + if (e.message === 'Assignment archived') return res.status(400).json({ error: e.message }); + if (e.message === 'Invalid exam node') return res.status(400).json({ error: e.message }); + if (e.message === 'Cannot grade before deadline') return res.status(400).json({ error: e.message }); + throw e; } - - // 获取提交记录 - const submission = await prisma.studentSubmission.findUnique({ - where: { id: submissionId, isDeleted: false }, - include: { - assignment: { - include: { - class: true, - exam: { - include: { - nodes: true - } - } - } - } - } - }); - - if (!submission) { - return res.status(404).json({ error: 'Submission not found' }); - } - - // 验证权限 - const isMember = await prisma.classMember.findFirst({ - where: { - classId: submission.assignment.classId, - userId: req.userId!, - roleInClass: 'Teacher', - isDeleted: false - } - }); - - if (!isMember) { - return res.status(403).json({ error: 'Permission denied' }); - } - - // 更新或创建批改详情 - const updatePromises = grades.map(async (grade: any) => { - const { examNodeId, score, judgement, teacherComment } = grade; - - // 查找或创建 SubmissionDetail - const existingDetail = await prisma.submissionDetail.findFirst({ - where: { - submissionId, - examNodeId, - isDeleted: false - } - }); - - if (existingDetail) { - return prisma.submissionDetail.update({ - where: { id: existingDetail.id }, - data: { - score, - judgement, - teacherComment, - updatedBy: req.userId! - } - }); - } else { - return prisma.submissionDetail.create({ - data: { - id: uuidv4(), - submissionId, - examNodeId, - score, - judgement, - teacherComment, - createdBy: req.userId!, - updatedBy: req.userId! - } - }); - } - }); - - await Promise.all(updatePromises); - - // 重新计算总分 - const allDetails = await prisma.submissionDetail.findMany({ - where: { - submissionId, - isDeleted: false - } - }); - - const totalScore = allDetails.reduce((sum, detail) => { - return sum + (detail.score ? Number(detail.score) : 0); - }, 0); - - // 更新提交状态 - await prisma.studentSubmission.update({ - where: { id: submissionId }, - data: { - submissionStatus: 'Graded', - totalScore, - updatedBy: req.userId! - } - }); - - res.json({ - message: 'Grading submitted successfully', - totalScore - }); } catch (error) { console.error('Submit grade error:', error); res.status(500).json({ error: 'Failed to submit grading' }); diff --git a/backend/src/controllers/org.controller.ts b/backend/src/controllers/org.controller.ts index e73f3c6..f286496 100644 --- a/backend/src/controllers/org.controller.ts +++ b/backend/src/controllers/org.controller.ts @@ -1,23 +1,11 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth.middleware'; -import prisma from '../utils/prisma'; -import { v4 as uuidv4 } from 'uuid'; -import { generateInviteCode } from '../utils/helpers'; +import { orgService } from '../services/org.service'; // GET /api/org/schools export const getSchools = async (req: AuthRequest, res: Response) => { try { - const schools = await prisma.school.findMany({ - where: { isDeleted: false }, - select: { - id: true, - name: true, - regionCode: true, - address: true - }, - orderBy: { name: 'asc' } - }); - + const schools = await orgService.getSchools(); res.json(schools); } catch (error) { console.error('Get schools error:', error); @@ -30,44 +18,7 @@ export const getSchools = async (req: AuthRequest, res: Response) => { export const getMyClasses = async (req: AuthRequest, res: Response) => { try { const { role } = req.query; // 可选:筛选角色 - - // 通过 ClassMember 关联查询 - const memberships = await prisma.classMember.findMany({ - where: { - userId: req.userId!, - isDeleted: false, - ...(role && { roleInClass: role as any }) - }, - include: { - class: { - include: { - grade: { - include: { - school: true - } - }, - _count: { - select: { members: true } - } - } - } - } - }); - - // 格式化返回数据 - const classes = memberships.map(membership => { - const cls = membership.class; - return { - id: cls.id, - name: cls.name, - gradeName: cls.grade.name, - schoolName: cls.grade.school.name, - inviteCode: cls.inviteCode, - studentCount: cls._count.members, - myRole: membership.roleInClass // 我在这个班级的角色 - }; - }); - + const classes = await orgService.getMyClasses(req.userId!, role as string | undefined); res.json(classes); } catch (error) { console.error('Get my classes error:', error); @@ -84,54 +35,13 @@ export const createClass = async (req: AuthRequest, res: Response) => { if (!name || !gradeId) { return res.status(400).json({ error: 'Missing required fields: name, gradeId' }); } - - // 验证年级是否存在 - const grade = await prisma.grade.findUnique({ - where: { id: gradeId, isDeleted: false }, - include: { school: true } - }); - - if (!grade) { - return res.status(404).json({ error: 'Grade not found' }); + try { + const result = await orgService.createClass(req.userId!, name, gradeId); + res.json(result); + } catch (e: any) { + if (e.message === 'Grade not found') return res.status(404).json({ error: e.message }); + throw e; } - - // 生成唯一邀请码 - const inviteCode = await generateInviteCode(); - - // 创建班级 - const classId = uuidv4(); - const newClass = await prisma.class.create({ - data: { - id: classId, - gradeId, - name, - inviteCode, - headTeacherId: req.userId, - createdBy: req.userId!, - updatedBy: req.userId! - } - }); - - // 自动将创建者添加为班级教师 - await prisma.classMember.create({ - data: { - id: uuidv4(), - classId, - userId: req.userId!, - roleInClass: 'Teacher', - createdBy: req.userId!, - updatedBy: req.userId! - } - }); - - res.json({ - id: newClass.id, - name: newClass.name, - gradeName: grade.name, - schoolName: grade.school.name, - inviteCode: newClass.inviteCode, - studentCount: 1 // 当前只有创建者一个成员 - }); } catch (error) { console.error('Create class error:', error); res.status(500).json({ error: 'Failed to create class' }); @@ -147,55 +57,14 @@ export const joinClass = async (req: AuthRequest, res: Response) => { if (!inviteCode) { return res.status(400).json({ error: 'Missing invite code' }); } - - // 查找班级 - const targetClass = await prisma.class.findUnique({ - where: { inviteCode, isDeleted: false }, - include: { - grade: { - include: { school: true } - } - } - }); - - if (!targetClass) { - return res.status(404).json({ error: 'Invalid invite code' }); + try { + const result = await orgService.joinClass(req.userId!, inviteCode); + res.json({ message: 'Successfully joined the class', class: result }); + } catch (e: any) { + if (e.message === 'Invalid invite code') return res.status(404).json({ error: e.message }); + if (e.message === 'You are already a member of this class') return res.status(400).json({ error: e.message }); + throw e; } - - // 检查是否已经是班级成员 - const existingMember = await prisma.classMember.findFirst({ - where: { - classId: targetClass.id, - userId: req.userId!, - isDeleted: false - } - }); - - if (existingMember) { - return res.status(400).json({ error: 'You are already a member of this class' }); - } - - // 添加为班级学生 - await prisma.classMember.create({ - data: { - id: uuidv4(), - classId: targetClass.id, - userId: req.userId!, - roleInClass: 'Student', - createdBy: req.userId!, - updatedBy: req.userId! - } - }); - - res.json({ - message: 'Successfully joined the class', - class: { - id: targetClass.id, - name: targetClass.name, - gradeName: targetClass.grade.name, - schoolName: targetClass.grade.school.name - } - }); } catch (error) { console.error('Join class error:', error); res.status(500).json({ error: 'Failed to join class' }); @@ -207,99 +76,14 @@ export const joinClass = async (req: AuthRequest, res: Response) => { export const getClassMembers = async (req: AuthRequest, res: Response) => { try { const { id: classId } = req.params; - - // 验证班级存在 - const targetClass = await prisma.class.findUnique({ - where: { id: classId, isDeleted: false } - }); - - if (!targetClass) { - return res.status(404).json({ error: 'Class not found' }); + try { + const formattedMembers = await orgService.getClassMembers(req.userId!, classId); + res.json(formattedMembers); + } catch (e: any) { + if (e.message === 'Class not found') return res.status(404).json({ error: e.message }); + if (e.message === 'You are not a member of this class') return res.status(403).json({ error: e.message }); + throw e; } - - // 验证当前用户是否是班级成员 - const isMember = await prisma.classMember.findFirst({ - where: { - classId, - userId: req.userId!, - isDeleted: false - } - }); - - if (!isMember) { - return res.status(403).json({ error: 'You are not a member of this class' }); - } - - const members = await prisma.classMember.findMany({ - where: { - classId, - isDeleted: false - }, - include: { - user: { - select: { - id: true, - realName: true, - studentId: true, - avatarUrl: true, - gender: true - } - } - }, - orderBy: [ - { roleInClass: 'asc' }, // 教师在前 - { createdAt: 'asc' } - ] - }); - - const assignmentsCount = await prisma.assignment.count({ - where: { classId } - }); - - const formattedMembers = await Promise.all(members.map(async member => { - const submissions = await prisma.studentSubmission.findMany({ - where: { - studentId: member.user.id, - assignment: { classId } - }, - select: { - totalScore: true, - submissionStatus: true, - submitTime: true - }, - orderBy: { submitTime: 'desc' }, - take: 5 - }); - - const recentTrendRaw = submissions.map(s => s.totalScore ? Number(s.totalScore) : 0); - const recentTrend = recentTrendRaw.concat(Array(Math.max(0, 5 - recentTrendRaw.length)).fill(0)).slice(0,5); - - const completedCount = await prisma.studentSubmission.count({ - where: { - studentId: member.user.id, - assignment: { classId }, - submissionStatus: { in: ['Submitted', 'Graded'] } - } - }); - const attendanceRate = assignmentsCount > 0 ? Math.round((completedCount / assignmentsCount) * 100) : 0; - - const latestScore = submissions[0]?.totalScore ? Number(submissions[0].totalScore) : null; - const status = latestScore !== null ? (latestScore >= 90 ? 'Excellent' : (latestScore < 60 ? 'AtRisk' : 'Active')) : 'Active'; - - return { - id: member.user.id, - studentId: member.user.studentId, - realName: member.user.realName, - avatarUrl: member.user.avatarUrl, - gender: member.user.gender, - role: member.roleInClass, - recentTrend, - status, - attendanceRate - }; - })); - - res.json(formattedMembers); } catch (error) { console.error('Get class members error:', error); res.status(500).json({ error: 'Failed to get class members' }); diff --git a/backend/src/controllers/question.controller.ts b/backend/src/controllers/question.controller.ts index 46a689e..b4a5d10 100644 --- a/backend/src/controllers/question.controller.ts +++ b/backend/src/controllers/question.controller.ts @@ -1,104 +1,13 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth.middleware'; -import prisma from '../utils/prisma'; -import { v4 as uuidv4 } from 'uuid'; +import { questionService } from '../services/question.service'; // POST /api/questions/search // 简单的题目搜索(按科目、难度筛选) export const searchQuestions = async (req: AuthRequest, res: Response) => { try { - const { - subjectId, - questionType, - difficulty, // exact match (legacy) - difficultyMin, - difficultyMax, - keyword, - createdBy, // 'me' or specific userId - sortBy = 'latest', // 'latest' | 'popular' - page = 1, - pageSize = 10 - } = req.body; - - const skip = (page - 1) * pageSize; - - const where: any = { - isDeleted: false, - ...(subjectId && { subjectId }), - ...(questionType && { questionType }), - ...(keyword && { content: { contains: keyword } }), - }; - - // Difficulty range - if (difficultyMin || difficultyMax) { - where.difficulty = {}; - if (difficultyMin) where.difficulty.gte = difficultyMin; - if (difficultyMax) where.difficulty.lte = difficultyMax; - } else if (difficulty) { - where.difficulty = difficulty; - } - - // CreatedBy filter - if (createdBy === 'me') { - where.createdBy = req.userId; - } else if (createdBy) { - where.createdBy = createdBy; - } - - // Sorting - let orderBy: any = { createdAt: 'desc' }; - if (sortBy === 'popular') { - orderBy = { usageCount: 'desc' }; // Assuming usageCount exists, otherwise fallback to createdAt - } - - // 查询题目 - const [questions, totalCount] = await Promise.all([ - prisma.question.findMany({ - where, - select: { - id: true, - content: true, - questionType: true, - difficulty: true, - answer: true, - explanation: true, - createdAt: true, - createdBy: true, - knowledgePoints: { - select: { - knowledgePoint: { - select: { - name: true - } - } - } - } - }, - skip, - take: pageSize, - orderBy - }), - prisma.question.count({ where }) - ]); - - // 映射到前端 DTO - const items = questions.map(q => ({ - id: q.id, - content: q.content, - type: q.questionType, - difficulty: q.difficulty, - answer: q.answer, - parse: q.explanation, - knowledgePoints: q.knowledgePoints.map(kp => kp.knowledgePoint.name), - isMyQuestion: q.createdBy === req.userId - })); - - res.json({ - items, - totalCount, - pageIndex: page, - pageSize - }); + const result = await questionService.search(req.userId!, req.body); + res.json(result); } catch (error) { console.error('Search questions error:', error); res.status(500).json({ error: 'Failed to search questions' }); @@ -109,36 +18,13 @@ export const searchQuestions = async (req: AuthRequest, res: Response) => { // 创建题目 export const createQuestion = async (req: AuthRequest, res: Response) => { try { - const { subjectId, content, questionType, difficulty = 3, answer, explanation, optionsConfig, knowledgePoints } = req.body; - - if (!subjectId || !content || !questionType || !answer) { - return res.status(400).json({ error: 'Missing required fields' }); + try { + const result = await questionService.create(req.userId!, req.body); + res.json(result); + } catch (e: any) { + if (e.message === 'Missing required fields') return res.status(400).json({ error: e.message }); + throw e; } - - const questionId = uuidv4(); - - // Handle knowledge points connection if provided - // This is a simplified version, ideally we should resolve KP IDs first - - const question = await prisma.question.create({ - data: { - id: questionId, - subjectId, - content, - questionType, - difficulty, - answer, - explanation, - optionsConfig: optionsConfig || null, - createdBy: req.userId!, - updatedBy: req.userId! - } - }); - - res.json({ - id: question.id, - message: 'Question created successfully' - }); } catch (error) { console.error('Create question error:', error); res.status(500).json({ error: 'Failed to create question' }); @@ -149,32 +35,14 @@ export const createQuestion = async (req: AuthRequest, res: Response) => { export const updateQuestion = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const { content, questionType, difficulty, answer, explanation, optionsConfig } = req.body; - - const question = await prisma.question.findUnique({ where: { id } }); - if (!question) return res.status(404).json({ error: 'Question not found' }); - - // Only creator can update (or admin) - if (question.createdBy !== req.userId) { - // For now, let's assume strict ownership. - // In real app, check role. - return res.status(403).json({ error: 'Permission denied' }); + try { + const result = await questionService.update(req.userId!, id, req.body); + res.json(result); + } catch (e: any) { + if (e.message === 'Question not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + throw e; } - - await prisma.question.update({ - where: { id }, - data: { - content, - questionType, - difficulty, - answer, - explanation, - optionsConfig: optionsConfig || null, - updatedBy: req.userId! - } - }); - - res.json({ message: 'Question updated successfully' }); } catch (error) { console.error('Update question error:', error); res.status(500).json({ error: 'Failed to update question' }); @@ -185,20 +53,14 @@ export const updateQuestion = async (req: AuthRequest, res: Response) => { export const deleteQuestion = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - - const question = await prisma.question.findUnique({ where: { id } }); - if (!question) return res.status(404).json({ error: 'Question not found' }); - - if (question.createdBy !== req.userId) { - return res.status(403).json({ error: 'Permission denied' }); + try { + const result = await questionService.softDelete(req.userId!, id); + res.json(result); + } catch (e: any) { + if (e.message === 'Question not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + throw e; } - - await prisma.question.update({ - where: { id }, - data: { isDeleted: true } - }); - - res.json({ message: 'Question deleted successfully' }); } catch (error) { console.error('Delete question error:', error); res.status(500).json({ error: 'Failed to delete question' }); @@ -208,26 +70,14 @@ export const deleteQuestion = async (req: AuthRequest, res: Response) => { // POST /api/questions/parse-text export const parseText = async (req: AuthRequest, res: Response) => { try { - const { text } = req.body; - if (!text) return res.status(400).json({ error: 'Text is required' }); - - // 简单的模拟解析逻辑 - // 假设每行是一个题目,或者用空行分隔 - const questions = text.split(/\n\s*\n/).map((block: string) => { - const lines = block.trim().split('\n'); - const content = lines[0]; - const options = lines.slice(1).filter((l: string) => /^[A-D]\./.test(l)); - - return { - content: content, - type: options.length > 0 ? 'SingleChoice' : 'Subjective', - options: options.length > 0 ? options : undefined, - answer: 'A', // 默认答案 - parse: '解析暂无' - }; - }); - - res.json(questions); + try { + const { text } = req.body; + const questions = questionService.parseText(text); + res.json(questions); + } catch (e: any) { + if (e.message === 'Text is required') return res.status(400).json({ error: e.message }); + throw e; + } } catch (error) { console.error('Parse text error:', error); res.status(500).json({ error: 'Failed to parse text' }); diff --git a/backend/src/controllers/submission.controller.ts b/backend/src/controllers/submission.controller.ts index 16bf10d..5995878 100644 --- a/backend/src/controllers/submission.controller.ts +++ b/backend/src/controllers/submission.controller.ts @@ -1,229 +1,75 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth.middleware'; -import prisma from '../utils/prisma'; -import { v4 as uuidv4 } from 'uuid'; -import { calculateRank } from '../utils/helpers'; +import { submissionService } from '../services/submission.service'; // GET /api/submissions/:assignmentId/paper // 学生获取答题卡 export const getStudentPaper = async (req: AuthRequest, res: Response) => { try { const { assignmentId } = req.params; - - // 获取作业信息 - const assignment = await prisma.assignment.findUnique({ - where: { id: assignmentId, isDeleted: false }, - include: { - exam: { - include: { - nodes: { - where: { isDeleted: false }, - include: { - question: { - include: { - knowledgePoints: { - select: { - knowledgePoint: { - select: { name: true } - } - } - } - } - } - }, - orderBy: { sortOrder: 'asc' } - } - } - } - } - }); - - if (!assignment) { - return res.status(404).json({ error: 'Assignment not found' }); + try { + const paper = await submissionService.getStudentPaper(req.userId!, assignmentId); + res.json(paper); + } catch (e: any) { + if (e.message === 'Assignment not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + if (e.message === 'Assignment archived') return res.status(400).json({ error: e.message }); + if (e.message === 'Assignment has not started yet') return res.status(400).json({ error: e.message }); + if (e.message === 'Assignment has ended') return res.status(400).json({ error: e.message }); + throw e; } - - // 验证作业时间 - const now = new Date(); - if (now < assignment.startTime) { - return res.status(400).json({ error: 'Assignment has not started yet' }); - } - - if (now > assignment.endTime && !assignment.allowLateSubmission) { - return res.status(400).json({ error: 'Assignment has ended' }); - } - - // 查找或创建学生提交记录 - let submission = await prisma.studentSubmission.findFirst({ - where: { - assignmentId, - studentId: req.userId!, - isDeleted: false - }, - include: { - details: true - } - }); - - if (!submission) { - // 创建新的提交记录 - submission = await prisma.studentSubmission.create({ - data: { - id: uuidv4(), - assignmentId, - studentId: req.userId!, - submissionStatus: 'Pending', - createdBy: req.userId!, - updatedBy: req.userId! - }, - include: { - details: true - } - }); - } - - // 构建试卷结构(树形) - const buildTree = (nodes: any[], parentId: string | null = null): any[] => { - return nodes - .filter(node => node.parentNodeId === parentId) - .map(node => { - const detail = submission!.details.find(d => d.examNodeId === node.id); - - return { - id: node.id, - nodeType: node.nodeType, - title: node.title, - description: node.description, - questionId: node.questionId, - questionContent: node.question?.content, - questionType: node.question?.questionType, - // 构造完整的 question 对象以供前端使用 - question: node.question ? { - id: node.question.id, - content: node.question.content, - type: node.question.questionType, - difficulty: node.question.difficulty, - answer: node.question.answer, - parse: node.question.explanation, - knowledgePoints: node.question.knowledgePoints.map((kp: any) => kp.knowledgePoint.name), - options: (() => { - const cfg: any = (node as any).question?.optionsConfig; - if (!cfg) return []; - try { - if (Array.isArray(cfg)) return cfg.map((v: any) => String(v)); - if (cfg.options && Array.isArray(cfg.options)) return cfg.options.map((v: any) => String(v)); - if (typeof cfg === 'object') { - return Object.keys(cfg).sort().map(k => String(cfg[k])); - } - return []; - } catch { - return []; - } - })() - } : undefined, - score: Number(node.score), - sortOrder: node.sortOrder, - studentAnswer: detail?.studentAnswer || null, - children: buildTree(nodes, node.id) - }; - }); - }; - - const rootNodes = buildTree(assignment.exam.nodes); - - res.json({ - examId: assignment.exam.id, - title: assignment.title, - duration: assignment.exam.suggestedDuration, - totalScore: Number(assignment.exam.totalScore), - startTime: assignment.startTime.toISOString(), - endTime: assignment.endTime.toISOString(), - submissionId: submission.id, - status: submission.submissionStatus, - rootNodes - }); } catch (error) { console.error('Get student paper error:', error); res.status(500).json({ error: 'Failed to get student paper' }); } }; +// POST /api/submissions/:assignmentId/save +// 学生保存进度(不提交) +export const saveProgress = async (req: AuthRequest, res: Response) => { + try { + const { assignmentId } = req.params; + const { answers } = req.body; + try { + const result = await submissionService.saveProgress(req.userId!, assignmentId, answers); + res.json(result); + } catch (e: any) { + if (e.message === 'Invalid answers data') return res.status(400).json({ error: e.message }); + if (e.message === 'Submission not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + if (e.message === 'Assignment has not started yet') return res.status(400).json({ error: e.message }); + if (e.message === 'Assignment archived') return res.status(400).json({ error: e.message }); + if (e.message === 'Cannot save progress after deadline') return res.status(400).json({ error: e.message }); + if (e.message === 'Invalid exam node') return res.status(400).json({ error: e.message }); + if (e.message === 'Cannot save progress for submitted assignment') return res.status(400).json({ error: e.message }); + throw e; + } + } catch (error) { + console.error('Save progress error:', error); + res.status(500).json({ error: 'Failed to save progress' }); + } +}; + // POST /api/submissions/:assignmentId/submit // 学生提交答案 export const submitAnswers = async (req: AuthRequest, res: Response) => { try { const { assignmentId } = req.params; const { answers, timeSpent } = req.body; // answers: Array of { examNodeId, studentAnswer } - - if (!answers || !Array.isArray(answers)) { - return res.status(400).json({ error: 'Invalid answers data' }); + try { + const result = await submissionService.submitAnswers(req.userId!, assignmentId, answers, timeSpent); + res.json(result); + } catch (e: any) { + if (e.message === 'Invalid answers data') return res.status(400).json({ error: e.message }); + if (e.message === 'Submission not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + if (e.message === 'Assignment has not started yet') return res.status(400).json({ error: e.message }); + if (e.message === 'Cannot submit after deadline') return res.status(400).json({ error: e.message }); + if (e.message === 'Assignment archived') return res.status(400).json({ error: e.message }); + if (e.message === 'Invalid exam node') return res.status(400).json({ error: e.message }); + if (e.message === 'Already submitted') return res.status(400).json({ error: e.message }); + throw e; } - - // 获取提交记录 - const submission = await prisma.studentSubmission.findFirst({ - where: { - assignmentId, - studentId: req.userId!, - isDeleted: false - } - }); - - if (!submission) { - return res.status(404).json({ error: 'Submission not found' }); - } - - // 批量创建/更新答题详情 - const updatePromises = answers.map(async (answer: any) => { - const { examNodeId, studentAnswer } = answer; - - const existingDetail = await prisma.submissionDetail.findFirst({ - where: { - submissionId: submission.id, - examNodeId, - isDeleted: false - } - }); - - if (existingDetail) { - return prisma.submissionDetail.update({ - where: { id: existingDetail.id }, - data: { - studentAnswer, - updatedBy: req.userId! - } - }); - } else { - return prisma.submissionDetail.create({ - data: { - id: uuidv4(), - submissionId: submission.id, - examNodeId, - studentAnswer, - createdBy: req.userId!, - updatedBy: req.userId! - } - }); - } - }); - - await Promise.all(updatePromises); - - // 更新提交状态 - await prisma.studentSubmission.update({ - where: { id: submission.id }, - data: { - submissionStatus: 'Submitted', - submitTime: new Date(), - timeSpentSeconds: timeSpent || null, - updatedBy: req.userId! - } - }); - - // TODO: 如果开启自动批改,这里可以实现自动评分逻辑 - - res.json({ - message: 'Answers submitted successfully', - submissionId: submission.id - }); } catch (error) { console.error('Submit answers error:', error); res.status(500).json({ error: 'Failed to submit answers' }); @@ -235,110 +81,14 @@ export const submitAnswers = async (req: AuthRequest, res: Response) => { export const getSubmissionResult = async (req: AuthRequest, res: Response) => { try { const { submissionId } = req.params; - - // 获取提交记录 - const submission = await prisma.studentSubmission.findUnique({ - where: { id: submissionId, isDeleted: false }, - include: { - assignment: { - include: { - exam: { - include: { - nodes: { - where: { isDeleted: false }, - include: { - question: { - include: { - knowledgePoints: { - select: { - knowledgePoint: { - select: { name: true } - } - } - } - } - } - }, - orderBy: { sortOrder: 'asc' } - } - } - } - } - }, - details: { - include: { - examNode: { - include: { - question: true - } - } - } - } - } - }); - - if (!submission) { - return res.status(404).json({ error: 'Submission not found' }); + try { + const result = await submissionService.getSubmissionResult(req.userId!, submissionId); + res.json(result); + } catch (e: any) { + if (e.message === 'Submission not found') return res.status(404).json({ error: e.message }); + if (e.message === 'Permission denied') return res.status(403).json({ error: e.message }); + throw e; } - - // 验证是本人的提交 - if (submission.studentId !== req.userId) { - return res.status(403).json({ error: 'Permission denied' }); - } - - // 如果还没有批改,返回未批改状态 - if (submission.submissionStatus !== 'Graded') { - return res.json({ - submissionId: submission.id, - status: submission.submissionStatus, - message: 'Your submission has not been graded yet' - }); - } - - // 计算排名 - const totalScore = Number(submission.totalScore || 0); - const { rank, totalStudents, beatRate } = await calculateRank( - submission.assignmentId, - totalScore - ); - - // 构建答题详情 - const nodes = submission.assignment.exam.nodes.map(node => { - const detail = submission.details.find(d => d.examNodeId === node.id); - - return { - examNodeId: node.id, - questionId: node.questionId, - questionContent: node.question?.content, - questionType: node.question?.questionType, - // 构造完整的 question 对象以供前端使用 - question: node.question ? { - id: node.question.id, - content: node.question.content, - type: node.question.questionType, - difficulty: node.question.difficulty, - answer: node.question.answer, - parse: node.question.explanation, - knowledgePoints: node.question.knowledgePoints.map((kp: any) => kp.knowledgePoint.name) - } : undefined, - score: Number(node.score), - studentScore: detail?.score ? Number(detail.score) : null, - studentAnswer: detail?.studentAnswer || null, - autoCheckResult: detail?.judgement === 'Correct', - teacherComment: detail?.teacherComment || null - }; - }); - - res.json({ - submissionId: submission.id, - studentName: 'Me', // 学生看自己的结果 - totalScore, - rank, - totalStudents, - beatRate, - submitTime: submission.submitTime?.toISOString() || null, - nodes - }); } catch (error) { console.error('Get submission result error:', error); res.status(500).json({ error: 'Failed to get submission result' }); @@ -348,90 +98,13 @@ export const getSubmissionResult = async (req: AuthRequest, res: Response) => { export const getSubmissionResultByAssignment = async (req: AuthRequest, res: Response) => { try { const { assignmentId } = req.params; - const submission = await prisma.studentSubmission.findFirst({ - where: { assignmentId, studentId: req.userId!, isDeleted: false }, - include: { - assignment: { - include: { - exam: { - include: { - nodes: { - where: { isDeleted: false }, - include: { - question: { - include: { - knowledgePoints: { - select: { knowledgePoint: { select: { name: true } } } - } - } - } - }, - orderBy: { sortOrder: 'asc' } - } - } - } - } - }, - details: { - include: { - examNode: { include: { question: true } } - } - } - } - }); - - if (!submission) { - return res.status(404).json({ error: 'Submission not found' }); + try { + const result = await submissionService.getSubmissionResultByAssignment(req.userId!, assignmentId); + res.json(result); + } catch (e: any) { + if (e.message === 'Submission not found') return res.status(404).json({ error: e.message }); + throw e; } - - if (submission.submissionStatus !== 'Graded') { - return res.json({ - submissionId: submission.id, - status: submission.submissionStatus, - message: 'Your submission has not been graded yet' - }); - } - - const totalScore = Number(submission.totalScore || 0); - const { rank, totalStudents, beatRate } = await calculateRank( - submission.assignmentId, - totalScore - ); - - const nodes = submission.assignment.exam.nodes.map(node => { - const detail = submission.details.find(d => d.examNodeId === node.id); - return { - examNodeId: node.id, - questionId: node.questionId, - questionContent: node.question?.content, - questionType: node.question?.questionType, - question: node.question ? { - id: node.question.id, - content: node.question.content, - type: node.question.questionType, - difficulty: node.question.difficulty, - answer: node.question.answer, - parse: node.question.explanation, - knowledgePoints: node.question.knowledgePoints.map((kp: any) => kp.knowledgePoint.name) - } : undefined, - score: Number(node.score), - studentScore: detail?.score ? Number(detail.score) : null, - studentAnswer: detail?.studentAnswer || null, - autoCheckResult: detail?.judgement === 'Correct', - teacherComment: detail?.teacherComment || null - }; - }); - - res.json({ - submissionId: submission.id, - studentName: 'Me', - totalScore, - rank, - totalStudents, - beatRate, - submitTime: submission.submitTime?.toISOString() || null, - nodes - }); } catch (error) { console.error('Get submission result by assignment error:', error); res.status(500).json({ error: 'Failed to get submission result' }); diff --git a/backend/src/index.ts b/backend/src/index.ts index ec08e94..626836e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5,6 +5,7 @@ import authRoutes from './routes/auth.routes'; import examRoutes from './routes/exam.routes'; import analyticsRoutes from './routes/analytics.routes'; import commonRoutes from './routes/common.routes'; +import configRoutes from './routes/config.routes'; import orgRouter from './routes/org.routes'; import curriculumRouter from './routes/curriculum.routes'; import questionRouter from './routes/question.routes'; @@ -16,11 +17,23 @@ import gradingRouter from './routes/grading.routes'; dotenv.config(); const app = express(); -const PORT = process.env.PORT || 3001; +const PORT = Number(process.env.PORT) || 8081; +const HOST = process.env.HOST || '127.0.0.1'; // 中间件 app.use(cors({ - origin: process.env.CORS_ORIGIN || 'http://localhost:3000', + origin: (origin, callback) => { + const allowed = [ + process.env.CORS_ORIGIN || '', + 'http://127.0.0.1:8080', + 'http://localhost:8080', + 'http://localhost:3000' + ].filter(Boolean); + if (!origin || allowed.includes(origin)) { + return callback(null, true); + } + return callback(new Error('Not allowed by CORS')); + }, credentials: true })); app.use(express.json()); @@ -34,6 +47,7 @@ app.use((req, res, next) => { // API路由 app.use('/api/auth', authRoutes); +app.use('/api/config', configRoutes); app.use('/api/org', orgRouter); app.use('/api/curriculum', curriculumRouter); app.use('/api/questions', questionRouter); @@ -64,8 +78,8 @@ app.use((err: any, req: express.Request, res: express.Response, next: express.Ne }); // 启动服务器 -app.listen(PORT, () => { - console.log(`✅ Server running on http://localhost:${PORT}`); +app.listen(PORT, HOST as any, () => { + console.log(`✅ Server running on http://${HOST}:${PORT}`); console.log(`📊 Environment: ${process.env.NODE_ENV}`); console.log(`🔗 CORS enabled for: ${process.env.CORS_ORIGIN}`); }); diff --git a/backend/src/routes/analytics.routes.ts b/backend/src/routes/analytics.routes.ts index 9d2683d..e21696c 100644 --- a/backend/src/routes/analytics.routes.ts +++ b/backend/src/routes/analytics.routes.ts @@ -3,6 +3,7 @@ import { authenticate } from '../middleware/auth.middleware'; import { getClassPerformance, getStudentGrowth, + getStudentStats, getRadar, getStudentRadar, getScoreDistribution, @@ -17,6 +18,7 @@ router.use(authenticate); router.get('/class/performance', getClassPerformance); router.get('/student/growth', getStudentGrowth); +router.get('/student/stats', getStudentStats); router.get('/radar', getRadar); router.get('/student/radar', getStudentRadar); router.get('/distribution', getScoreDistribution); diff --git a/backend/src/routes/assignment.routes.ts b/backend/src/routes/assignment.routes.ts index 402575d..c241556 100644 --- a/backend/src/routes/assignment.routes.ts +++ b/backend/src/routes/assignment.routes.ts @@ -7,6 +7,10 @@ const router = Router(); router.get('/teaching', authenticate, assignmentController.getTeachingAssignments); router.get('/learning', authenticate, assignmentController.getStudentAssignments); router.post('/', authenticate, assignmentController.createAssignment); +router.put('/:id', authenticate, assignmentController.updateAssignment); +router.delete('/:id', authenticate, assignmentController.deleteAssignment); +router.post('/:id/archive', authenticate, assignmentController.archiveAssignment); +router.get('/:id/analysis', authenticate, assignmentController.getAssignmentAnalysis); router.get('/:id/stats', authenticate, assignmentController.getAssignmentStats); export default router; diff --git a/backend/src/routes/common.routes.ts b/backend/src/routes/common.routes.ts index 960d511..5ca5f40 100644 --- a/backend/src/routes/common.routes.ts +++ b/backend/src/routes/common.routes.ts @@ -14,18 +14,12 @@ const router = Router(); router.use(authenticate); -// Messages router.get('/messages', getMessages); router.post('/messages/:id/read', markMessageRead); router.post('/messages', createMessage); -// Schedule -router.get('/schedule/week', getWeekSchedule); router.get('/common/schedule/week', getWeekSchedule); -router.get('/common/schedule', getSchedule); // For realCommonService compatibility -router.post('/schedule', addEvent); -router.delete('/schedule/:id', deleteEvent); -// Compatibility for frontend realScheduleService which posts to /common/schedule +router.get('/common/schedule', getSchedule); router.post('/common/schedule', addEvent); router.delete('/common/schedule/:id', deleteEvent); diff --git a/backend/src/routes/config.routes.ts b/backend/src/routes/config.routes.ts new file mode 100644 index 0000000..afdef58 --- /dev/null +++ b/backend/src/routes/config.routes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { testDbConnection } from '../controllers/config.controller'; + +const router = Router(); + +router.post('/db', testDbConnection); + +export default router; + diff --git a/backend/src/routes/submission.routes.ts b/backend/src/routes/submission.routes.ts index 33240eb..9528d50 100644 --- a/backend/src/routes/submission.routes.ts +++ b/backend/src/routes/submission.routes.ts @@ -6,6 +6,7 @@ const router = Router(); router.get('/:assignmentId/paper', authenticate, submissionController.getStudentPaper); router.post('/:assignmentId/submit', authenticate, submissionController.submitAnswers); +router.post('/:assignmentId/save', authenticate, submissionController.saveProgress); router.get('/:submissionId/result', authenticate, submissionController.getSubmissionResult); router.get('/by-assignment/:assignmentId/result', authenticate, submissionController.getSubmissionResultByAssignment); diff --git a/backend/src/services/analytics.service.ts b/backend/src/services/analytics.service.ts new file mode 100644 index 0000000..b9ecc82 --- /dev/null +++ b/backend/src/services/analytics.service.ts @@ -0,0 +1,214 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export class AnalyticsService { + async getClassPerformance(userId: string) { + const classes = await prisma.class.findMany({ + where: { + OR: [ + { headTeacherId: userId }, + { members: { some: { userId: userId, roleInClass: 'Teacher' } } } + ], + isDeleted: false + }, + select: { id: true, name: true } + }); + const classIds = classes.map(c => c.id); + const assignments = await prisma.assignment.findMany({ + where: { classId: { in: classIds }, isDeleted: false }, + orderBy: { endTime: 'desc' }, + take: 5, + include: { submissions: { where: { submissionStatus: 'Graded' }, select: { totalScore: true } } } + }); + assignments.reverse(); + const labels = assignments.map(a => a.title); + const data = assignments.map(a => { + const scores = a.submissions.map(s => Number(s.totalScore)); + const avg = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0; + return Number(avg.toFixed(1)); + }); + return { + labels, + datasets: [{ label: '班级平均分', data, borderColor: 'rgb(75, 192, 192)', backgroundColor: 'rgba(75, 192, 192, 0.5)' }] + }; + } + + async getStudentGrowth(userId: string) { + const submissions = await prisma.studentSubmission.findMany({ + where: { studentId: userId, submissionStatus: 'Graded', isDeleted: false }, + orderBy: { submitTime: 'desc' }, + take: 5, + include: { assignment: true } + }); + submissions.reverse(); + const labels = submissions.map(s => s.assignment.title); + const myScores = submissions.map(s => Number(s.totalScore)); + + // 计算对应作业的班级平均分 + const avgScores: number[] = []; + for (const s of submissions) { + const graded = await prisma.studentSubmission.findMany({ + where: { assignmentId: s.assignmentId, submissionStatus: 'Graded', isDeleted: false }, + select: { totalScore: true } + }); + const scores = graded.map(g => Number(g.totalScore)); + const avg = scores.length > 0 ? Number((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1)) : 0; + avgScores.push(avg); + } + + return { + labels, + datasets: [ + { label: '我的成绩', data: myScores, borderColor: 'rgb(53, 162, 235)', backgroundColor: 'rgba(53, 162, 235, 0.5)' }, + { label: '班级平均', data: avgScores, borderColor: 'rgb(75, 192, 192)', backgroundColor: 'rgba(75, 192, 192, 0.3)' } + ] + }; + } + + async getStudentStats(userId: string) { + const submissions = await prisma.studentSubmission.findMany({ + where: { studentId: userId, isDeleted: false }, + select: { submissionStatus: true, totalScore: true, timeSpentSeconds: true } + }); + + const completed = submissions.filter(s => s.submissionStatus === 'Submitted' || s.submissionStatus === 'Graded').length; + const todo = submissions.filter(s => s.submissionStatus === 'Pending').length; + + const gradedSubmissions = submissions.filter(s => s.submissionStatus === 'Graded'); + let averageScore = 0; + if (gradedSubmissions.length > 0) { + const sum = gradedSubmissions.reduce((a, b) => a + Number(b.totalScore || 0), 0); + averageScore = Number((sum / gradedSubmissions.length).toFixed(1)); + } + + const totalSeconds = submissions.reduce((a, b) => a + (b.timeSpentSeconds || 0), 0); + const studyDuration = Math.round(totalSeconds / 3600); // Hours + + return { completed, todo, average: averageScore, studyDuration }; + } + + async getRadar() { + return { indicators: ['知识掌握', '应用能力', '分析能力', '逻辑思维', '创新能力','个人表现'], values: [85, 78, 92, 88, 75, 99] }; + } + + async getStudentRadar() { + return { indicators: ['知识掌握', '应用能力', '分析能力', '逻辑思维', '创新能力'], values: [80, 85, 90, 82, 78] }; + } + + async getScoreDistribution(userId: string) { + const classes = await prisma.class.findMany({ + where: { + OR: [ + { headTeacherId: userId }, + { members: { some: { userId: userId, roleInClass: 'Teacher' } } } + ], + isDeleted: false + }, + select: { id: true } + }); + const classIds = classes.map(c => c.id); + if (classIds.length === 0) return []; + const assignments = await prisma.assignment.findMany({ where: { classId: { in: classIds }, isDeleted: false }, select: { id: true } }); + const assignmentIds = assignments.map(a => a.id); + const submissions = await prisma.studentSubmission.findMany({ + where: { assignmentId: { in: assignmentIds }, submissionStatus: 'Graded', isDeleted: false }, + select: { totalScore: true } + }); + const scores = submissions.map(s => Number(s.totalScore)); + const distribution = [ + { range: '0-60', count: 0 }, + { range: '60-70', count: 0 }, + { range: '70-80', count: 0 }, + { range: '80-90', count: 0 }, + { range: '90-100', count: 0 } + ]; + scores.forEach(score => { + if (score < 60) distribution[0].count++; + else if (score < 70) distribution[1].count++; + else if (score < 80) distribution[2].count++; + else if (score < 90) distribution[3].count++; + else distribution[4].count++; + }); + return distribution; + } + + async getTeacherStats(userId: string) { + const classes = await prisma.class.findMany({ + where: { + OR: [ + { headTeacherId: userId }, + { members: { some: { userId: userId, roleInClass: 'Teacher' } } } + ], + isDeleted: false + }, + select: { id: true } + }); + const classIds = classes.map(c => c.id); + if (classIds.length === 0) { + return { activeStudents: 0, averageScore: 0, pendingGrading: 0, passRate: 0 }; + } + const activeStudents = await prisma.classMember.count({ where: { classId: { in: classIds }, roleInClass: 'Student', isDeleted: false } }); + const assignments = await prisma.assignment.findMany({ where: { classId: { in: classIds }, isDeleted: false }, select: { id: true } }); + const assignmentIds = assignments.map(a => a.id); + const pendingGrading = await prisma.studentSubmission.count({ where: { assignmentId: { in: assignmentIds }, submissionStatus: 'Submitted', isDeleted: false } }); + const gradedSubmissions = await prisma.studentSubmission.findMany({ where: { assignmentId: { in: assignmentIds }, submissionStatus: 'Graded', isDeleted: false }, select: { totalScore: true } }); + let averageScore = 0; + let passRate = 0; + if (gradedSubmissions.length > 0) { + const scores = gradedSubmissions.map(s => Number(s.totalScore)); + const sum = scores.reduce((a, b) => a + b, 0); + averageScore = Number((sum / scores.length).toFixed(1)); + const passedCount = scores.filter(score => score >= 60).length; + passRate = Number(((passedCount / scores.length) * 100).toFixed(1)); + } + return { activeStudents, averageScore, pendingGrading, passRate }; + } + + async getExamStats(examId: string) { + const assignments = await prisma.assignment.findMany({ where: { examId, isDeleted: false }, select: { id: true } }); + const assignmentIds = assignments.map(a => a.id); + const gradedSubmissions = await prisma.studentSubmission.findMany({ where: { assignmentId: { in: assignmentIds }, submissionStatus: 'Graded', isDeleted: false }, select: { id: true, totalScore: true } }); + const scores = gradedSubmissions.map(s => Number(s.totalScore)); + const averageScore = scores.length > 0 ? Number((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1)) : 0; + const maxScore = scores.length > 0 ? Math.max(...scores) : 0; + const minScore = scores.length > 0 ? Math.min(...scores) : 0; + const passRate = scores.length > 0 ? Number(((scores.filter(s => s >= 60).length / scores.length) * 100).toFixed(1)) : 0; + const distribution = [ + { range: '0-60', count: 0 }, + { range: '60-70', count: 0 }, + { range: '70-80', count: 0 }, + { range: '80-90', count: 0 }, + { range: '90-100', count: 0 } + ]; + scores.forEach(score => { + if (score < 60) distribution[0].count++; + else if (score < 70) distribution[1].count++; + else if (score < 80) distribution[2].count++; + else if (score < 90) distribution[3].count++; + else distribution[4].count++; + }); + const examNodes = await prisma.examNode.findMany({ + where: { examId, isDeleted: false }, + select: { id: true, questionId: true, question: { select: { content: true, difficulty: true, questionType: true } } } + }); + const nodeIds = examNodes.map(n => n.id); + const submissionIds = gradedSubmissions.map(s => s.id); + const details = await prisma.submissionDetail.findMany({ where: { examNodeId: { in: nodeIds }, submissionId: { in: submissionIds }, isDeleted: false }, select: { examNodeId: true, judgement: true } }); + const statsMap = new Map(); + for (const d of details) { + const s = statsMap.get(d.examNodeId) || { total: 0, wrong: 0 }; + s.total += 1; + if (d.judgement === 'Incorrect') s.wrong += 1; + statsMap.set(d.examNodeId, s); + } + const wrongQuestions = examNodes.map(n => { + const s = statsMap.get(n.id) || { total: 0, wrong: 0 }; + const errorRate = s.total > 0 ? Math.round((s.wrong / s.total) * 100) : 0; + return { id: n.questionId || n.id, content: n.question?.content || '', errorRate, difficulty: n.question?.difficulty || 0, type: n.question?.questionType || 'Unknown' }; + }).sort((a, b) => b.errorRate - a.errorRate).slice(0, 20); + return { averageScore, passRate, maxScore, minScore, scoreDistribution: distribution, wrongQuestions }; + } +} + +export const analyticsService = new AnalyticsService(); diff --git a/backend/src/services/assignment.service.ts b/backend/src/services/assignment.service.ts new file mode 100644 index 0000000..cc4d5da --- /dev/null +++ b/backend/src/services/assignment.service.ts @@ -0,0 +1,483 @@ +import prisma from '../utils/prisma'; +import { v4 as uuidv4 } from 'uuid'; + +export class AssignmentService { + async getTeachingAssignments(userId: string, filters?: { classId?: string; examType?: string; subjectId?: string; status?: string }) { + const myClasses = await prisma.classMember.findMany({ + where: { userId, roleInClass: 'Teacher', isDeleted: false }, + select: { classId: true } + }); + const myClassIds = myClasses.map(m => m.classId); + if (myClassIds.length === 0) { + return { items: [], totalCount: 0, pageIndex: 1, pageSize: 10 }; + } + + const where: any = { + classId: { in: myClassIds }, + isDeleted: false + }; + + if (filters?.classId && filters.classId !== 'all') { + where.classId = filters.classId; + } + + // For nested exam filters (examType, subjectId), we might need to filter after fetch or use nested where if supported (Prisma supports nested where in findMany for relations, but let's check). + // Prisma supports filtering on relations. + if ((filters?.examType && filters.examType !== 'all') || (filters?.subjectId && filters.subjectId !== 'all')) { + where.exam = { + ...(filters.examType && filters.examType !== 'all' ? { examType: filters.examType } : {}), + ...(filters.subjectId && filters.subjectId !== 'all' ? { subjectId: filters.subjectId } : {}) + }; + } + + // Status filter logic + // 'Active' -> endTime > now + // 'ToGrade' -> has submissions with status 'Submitted' + // 'Graded' -> endTime < now (Closed) OR maybe manually marked? Let's use endTime for 'Ended'/'Graded' context or just 'Closed'. + // The user asked for "已批改" (Graded), "未批改" (Ungraded), "进行中" (In Progress). + // Let's map: + // - In Progress: endTime > now + // - Ungraded: Has submissions where status = 'Submitted' + // - Graded: All submissions are 'Graded' AND endTime < now? Or just "Completed" tab. + + // For simplicity in SQL/Prisma: + // It is hard to filter "has submissions with status Submitted" efficiently in a single where without complex queries. + // Let's fetch and filter in memory for status, or use basic time-based status for DB and refine in memory. + + const assignments = await prisma.assignment.findMany({ + where, + include: { + exam: { select: { title: true, totalScore: true, examType: true, subject: { select: { name: true } } } }, + class: { include: { grade: true } }, + _count: { select: { submissions: true } }, + submissions: { + select: { id: true, submissionStatus: true } + } + }, + orderBy: { createdAt: 'desc' } + }); + + let items = assignments.map(a => { + const now = new Date(); + const isExpired = now > a.endTime; + const isArchived = a.status === 'Archived'; + + // Determine UI status for Teacher + // Active: Not expired and not archived + // Grading: Expired and not archived + // Ended: Archived + + let uiStatus = 'Active'; + if (isArchived) { + uiStatus = 'Ended'; + } else if (isExpired) { + uiStatus = 'Grading'; + } + + // Has pending grading: check if any submission is Submitted, regardless of assignment status + // But strictly, teacher only cares about this in Grading phase, but showing it always is fine. + const hasPendingGrading = a.submissions.some(s => s.submissionStatus === 'Submitted'); + + return { + id: a.id, + title: a.title, + examTitle: a.exam.title, + subjectName: a.exam.subject.name, + examType: a.exam.examType, + className: a.class.name, + gradeName: a.class.grade.name, + submittedCount: a.submissions.filter(s => s.submissionStatus !== 'Pending').length, + totalCount: a._count.submissions, + status: uiStatus, + hasPendingGrading, + dueDate: a.endTime.toISOString(), + createdAt: a.createdAt.toISOString() + }; + }); + + // Apply status filter in memory + if (filters?.status && filters.status !== 'all') { + if (filters.status === 'Active') { + items = items.filter(i => i.status === 'Active'); + } else if (filters.status === 'ToGrade') { + items = items.filter(i => i.status === 'Grading' && i.hasPendingGrading); + } else if (filters.status === 'Graded') { + items = items.filter(i => i.status === 'Ended'); + } + } + + return { items, totalCount: items.length, pageIndex: 1, pageSize: 10 }; + } + + async getStudentAssignments(userId: string, filters?: { subjectId?: string; examType?: string; status?: string }) { + const where: any = { studentId: userId, isDeleted: false }; + + // Status filter + if (filters?.status) { + if (filters.status === 'Pending') { + where.submissionStatus = 'Pending'; + } else if (filters.status === 'Completed') { + where.submissionStatus = { in: ['Submitted', 'Graded'] }; + } + } + + const submissions = await prisma.studentSubmission.findMany({ + where, + include: { + assignment: { + include: { + exam: { + select: { + title: true, + totalScore: true, + suggestedDuration: true, + examType: true, + subjectId: true, + subject: { select: { name: true } }, + _count: { select: { nodes: true } } + } + }, + class: { + include: { + grade: true, + members: { + where: { roleInClass: 'Teacher', isDeleted: false }, + include: { user: { select: { realName: true } } }, + take: 1 + } + } + } + } + } + }, + orderBy: { createdAt: 'desc' } + }); + + // Client-side filtering for nested properties if not efficiently filterable in query + let filteredSubmissions = submissions; + if (filters?.subjectId && filters.subjectId !== 'all') { + filteredSubmissions = filteredSubmissions.filter(s => s.assignment.exam.subjectId === filters.subjectId); + } + if (filters?.examType && filters.examType !== 'all') { + filteredSubmissions = filteredSubmissions.filter(s => s.assignment.exam.examType === filters.examType); + } + + const items = filteredSubmissions.map(s => { + const now = new Date(); + const isExpired = now > s.assignment.endTime; + const isArchived = s.assignment.status === 'Archived'; + + let status = s.submissionStatus; + + // Determine UI Status + let uiStatus: string = status as unknown as string; + const hasSubmitted = status === 'Submitted' || status === 'Graded'; + + if (isArchived) { + uiStatus = 'Completed'; + } else if (isExpired) { + if (s.assignment.allowLateSubmission && !hasSubmitted) { + // If late submission allowed and NOT submitted, keep it Pending/InProgress + // But if user already submitted, then it is Grading (waiting for grade) + if (status === 'Pending' && (s as any).startedAt) { + uiStatus = 'InProgress'; + } else { + uiStatus = 'Pending'; // Or 'Late'? But UI handles Pending well. + } + } else { + // Normal flow: Expired -> Grading + uiStatus = 'Grading'; + } + } else { + // Not expired + if (status === 'Submitted') { + uiStatus = 'Submitted'; + } else if (status === 'Pending') { + if ((s as any).startedAt) { + uiStatus = 'InProgress'; + } else { + uiStatus = 'Pending'; + } + } else if (status === 'Graded') { + // Auto-graded but not expired -> Submitted (waiting for deadline) + uiStatus = 'Submitted'; + } + } + + const teacherName = s.assignment.class.members[0]?.user.realName || ''; + return { + id: s.assignment.id, + title: s.assignment.title, + examTitle: s.assignment.exam.title, + subjectName: s.assignment.exam.subject.name, + examType: s.assignment.exam.examType, + teacherName: teacherName, + duration: s.assignment.exam.suggestedDuration, + questionCount: s.assignment.exam._count.nodes, + totalScore: s.assignment.exam.totalScore, + className: s.assignment.class.name, + startTime: s.assignment.startTime.toISOString(), + endTime: s.assignment.endTime.toISOString(), + status: uiStatus, + score: s.totalScore ? Number(s.totalScore) : null, + submitTime: s.submitTime?.toISOString() || null, + isSubmitted: hasSubmitted + }; + }); + return { items, totalCount: items.length, pageIndex: 1, pageSize: 1000 }; // Increased pageSize + } + + async createAssignment(userId: string, data: { + examId: string; + classId: string; + title: string; + startTime: string; + endTime: string; + allowLateSubmission?: boolean; + autoScoreEnabled?: boolean; + }) { + const { examId, classId, title, startTime, endTime, allowLateSubmission, autoScoreEnabled } = data; + const start = new Date(startTime); + const end = new Date(endTime); + if (isNaN(start.getTime()) || isNaN(end.getTime())) throw new Error('Invalid startTime or endTime'); + if (start >= end) throw new Error('startTime must be earlier than endTime'); + const exam = await prisma.exam.findUnique({ where: { id: examId, isDeleted: false } }); + if (!exam) throw new Error('Exam not found'); + if (exam.status !== 'Published') throw new Error('Exam must be published before creating assignment'); + const membership = await prisma.classMember.findFirst({ + where: { classId, userId, roleInClass: 'Teacher', isDeleted: false } + }); + if (!membership) throw new Error('You are not a teacher of this class'); + const students = await prisma.classMember.findMany({ + where: { classId, roleInClass: 'Student', isDeleted: false }, + select: { userId: true } + }); + const assignmentId = uuidv4(); + const assignment = await prisma.assignment.create({ + data: { + id: assignmentId, + examId, + classId, + title, + startTime: new Date(startTime), + endTime: new Date(endTime), + allowLateSubmission: allowLateSubmission ?? false, + autoScoreEnabled: autoScoreEnabled ?? true, + createdBy: userId, + updatedBy: userId + } + }); + await Promise.all(students.map(s => prisma.studentSubmission.create({ + data: { + id: uuidv4(), + assignmentId, + studentId: s.userId, + submissionStatus: 'Pending', + createdBy: userId, + updatedBy: userId + } + }))); + return { id: assignment.id, title: assignment.title, message: `Assignment created successfully for ${students.length} students` }; + } + + async updateAssignment(userId: string, assignmentId: string, data: { + title?: string; + startTime?: string; + endTime?: string; + allowLateSubmission?: boolean; + autoScoreEnabled?: boolean; + }) { + const assignment = await prisma.assignment.findUnique({ + where: { id: assignmentId, isDeleted: false } + }); + if (!assignment) throw new Error('Assignment not found'); + + const membership = await prisma.classMember.findFirst({ + where: { classId: assignment.classId, userId, roleInClass: 'Teacher', isDeleted: false } + }); + if (!membership) throw new Error('You are not a teacher of this class'); + + const start = data.startTime ? new Date(data.startTime) : undefined; + const end = data.endTime ? new Date(data.endTime) : undefined; + if (start && end && start >= end) throw new Error('startTime must be earlier than endTime'); + const updated = await prisma.assignment.update({ + where: { id: assignmentId }, + data: { + title: data.title, + startTime: start, + endTime: end, + allowLateSubmission: data.allowLateSubmission, + autoScoreEnabled: data.autoScoreEnabled, + updatedBy: userId + } + }); + return updated; + } + + async archiveAssignment(userId: string, assignmentId: string) { + const assignment = await prisma.assignment.findUnique({ + where: { id: assignmentId, isDeleted: false } + }); + if (!assignment) throw new Error('Assignment not found'); + + const membership = await prisma.classMember.findFirst({ + where: { classId: assignment.classId, userId, roleInClass: 'Teacher', isDeleted: false } + }); + if (!membership) throw new Error('You are not a teacher of this class'); + + const updated = await prisma.assignment.update({ + where: { id: assignmentId }, + data: { + status: 'Archived', + updatedBy: userId + } + }); + return updated; + } + + async deleteAssignment(userId: string, assignmentId: string) { + const assignment = await prisma.assignment.findUnique({ + where: { id: assignmentId, isDeleted: false } + }); + if (!assignment) throw new Error('Assignment not found'); + + const membership = await prisma.classMember.findFirst({ + where: { classId: assignment.classId, userId, roleInClass: 'Teacher', isDeleted: false } + }); + if (!membership) throw new Error('You are not a teacher of this class'); + + await prisma.assignment.update({ + where: { id: assignmentId }, + data: { isDeleted: true, updatedBy: userId } + }); + return { message: 'Assignment deleted successfully' }; + } + + async getAssignmentAnalysis(userId: string, assignmentId: string) { + const assignment = await prisma.assignment.findUnique({ + where: { id: assignmentId, isDeleted: false }, + include: { class: true, exam: { select: { title: true, totalScore: true } } } + }); + if (!assignment) throw new Error('Assignment not found'); + + const isMember = await prisma.classMember.findFirst({ + where: { classId: assignment.classId, userId, roleInClass: 'Teacher', isDeleted: false } + }); + if (!isMember) throw new Error('Permission denied'); + + const submissions = await prisma.studentSubmission.findMany({ + where: { assignmentId, isDeleted: false }, + include: { + student: { select: { realName: true } }, + details: { + where: { isDeleted: false } + } + } + }); + + const examNodes = await prisma.examNode.findMany({ + where: { examId: assignment.examId, nodeType: 'Question', isDeleted: false }, + include: { + question: { + include: { knowledgePoints: { include: { knowledgePoint: true } } } + } + }, + orderBy: { sortOrder: 'asc' } + }); + + const questionStats = examNodes.map(node => { + const details = submissions.flatMap(s => s.details.filter(d => d.examNodeId === node.id)); + const totalAnswers = details.length; + const errorDetails = details.filter(d => d.judgement === 'Incorrect'); + const errorCount = errorDetails.length; + + return { + id: node.id, + title: node.title || node.question?.content || 'Question', + questionId: node.questionId, + score: Number(node.score), + totalAnswers, + errorCount, + errorRate: totalAnswers > 0 ? Math.round((errorCount / totalAnswers) * 100) : 0, + correctAnswer: node.question?.answer || '', + knowledgePoints: node.question?.knowledgePoints.map(kp => kp.knowledgePoint.name) || [], + wrongSubmissions: errorDetails.map(d => { + const sub = submissions.find(s => s.id === d.submissionId); + return { + studentName: sub?.student.realName || 'Unknown', + studentAnswer: d.studentAnswer || '' + }; + }) + }; + }); + + // Overall Stats + const totalStudents = submissions.length; + const submittedCount = submissions.filter(s => s.submissionStatus === 'Submitted' || s.submissionStatus === 'Graded').length; + const gradedScores = submissions.filter(s => s.submissionStatus === 'Graded' && s.totalScore !== null).map(s => Number(s.totalScore)); + const averageScore = gradedScores.length > 0 ? gradedScores.reduce((sum, score) => sum + score, 0) / gradedScores.length : 0; + + // Knowledge Point Analysis + const kpStats: Record = {}; + questionStats.forEach(q => { + q.knowledgePoints.forEach(kp => { + if (!kpStats[kp]) kpStats[kp] = { total: 0, wrong: 0 }; + kpStats[kp].total += q.totalAnswers; + kpStats[kp].wrong += q.errorCount; + }); + }); + const knowledgePointAnalysis = Object.entries(kpStats).map(([name, stats]) => ({ + name, + errorRate: stats.total > 0 ? Math.round((stats.wrong / stats.total) * 100) : 0 + })); + + return { + overview: { + title: assignment.title, + examTitle: assignment.exam.title, + totalStudents, + submittedCount, + averageScore: Math.round(averageScore * 10) / 10, + maxScore: gradedScores.length > 0 ? Math.max(...gradedScores) : 0, + minScore: gradedScores.length > 0 ? Math.min(...gradedScores) : 0, + }, + questions: questionStats, + knowledgePoints: knowledgePointAnalysis + }; + } + + async getAssignmentStats(userId: string, assignmentId: string) { + const assignment = await prisma.assignment.findUnique({ + where: { id: assignmentId, isDeleted: false }, + include: { class: true } + }); + if (!assignment) throw new Error('Assignment not found'); + const isMember = await prisma.classMember.findFirst({ + where: { classId: assignment.classId, userId, roleInClass: 'Teacher', isDeleted: false } + }); + if (!isMember) throw new Error('Permission denied'); + const submissions = await prisma.studentSubmission.findMany({ + where: { assignmentId, isDeleted: false }, + select: { submissionStatus: true, totalScore: true } + }); + const totalCount = submissions.length; + const submittedCount = submissions.filter(s => s.submissionStatus === 'Submitted' || s.submissionStatus === 'Graded').length; + const gradedScores = submissions.filter(s => s.submissionStatus === 'Graded' && s.totalScore !== null).map(s => Number(s.totalScore)); + const averageScore = gradedScores.length > 0 ? gradedScores.reduce((sum, score) => sum + score, 0) / gradedScores.length : 0; + const maxScore = gradedScores.length > 0 ? Math.max(...gradedScores) : 0; + const minScore = gradedScores.length > 0 ? Math.min(...gradedScores) : 0; + return { + totalStudents: totalCount, + submittedCount, + gradedCount: submissions.filter(s => s.submissionStatus === 'Graded').length, + pendingCount: totalCount - submittedCount, + averageScore: Math.round(averageScore * 10) / 10, + maxScore, + minScore, + passRate: 0, + scoreDistribution: [] + }; + } +} + +export const assignmentService = new AssignmentService(); diff --git a/backend/src/services/common.service.ts b/backend/src/services/common.service.ts new file mode 100644 index 0000000..0dcec01 --- /dev/null +++ b/backend/src/services/common.service.ts @@ -0,0 +1,80 @@ +import prisma from '../utils/prisma'; + +export class CommonService { + async getMessages(userId: string) { + return prisma.message.findMany({ where: { userId }, orderBy: { createdAt: 'desc' } }); + } + + async markMessageRead(userId: string, id: string) { + const message = await prisma.message.findUnique({ where: { id } }); + if (!message) throw new Error('Message not found'); + if (message.userId !== userId) throw new Error('Forbidden'); + await prisma.message.update({ where: { id }, data: { isRead: true } }); + return { success: true }; + } + + async createMessage(userId: string, data: { title: string; content: string; type?: string }) { + const { title, content, type } = data; + if (!title || !content) throw new Error('Title and content are required'); + return prisma.message.create({ + data: { userId, title, content, type: type || 'System', senderName: 'Me', isRead: false } + }); + } + + async getSchedule(userId: string) { + const user = await prisma.applicationUser.findUnique({ + where: { id: userId }, + include: { classMemberships: { include: { class: true } } } + }); + if (!user) throw new Error('User not found'); + const classIds = user.classMemberships.map(cm => cm.classId); + const schedules = await prisma.schedule.findMany({ where: { classId: { in: classIds } }, include: { class: true } }); + return schedules.map(s => ({ + id: s.id, + startTime: s.startTime, + endTime: s.endTime, + className: s.class.name, + subject: s.subject, + room: s.room || '', + isToday: s.dayOfWeek === new Date().getDay(), + dayOfWeek: s.dayOfWeek, + period: s.period + })); + } + + async addEvent(userId: string, data: { + subject: string; + className?: string; + classId?: string; + room?: string; + dayOfWeek: number; + period: number; + startTime: string; + endTime: string; + }) { + const { subject, className, classId, room, dayOfWeek, period, startTime, endTime } = data; + let resolvedClassId: string | null = null; + if (classId) { + const clsById = await prisma.class.findUnique({ where: { id: classId } }); + if (!clsById) throw new Error('Class not found'); + resolvedClassId = clsById.id; + } else if (className) { + const clsByName = await prisma.class.findFirst({ where: { name: className } }); + if (!clsByName) throw new Error('Class not found'); + resolvedClassId = clsByName.id; + } else { + throw new Error('classId or className is required'); + } + await prisma.schedule.create({ + data: { classId: resolvedClassId!, subject, room, dayOfWeek, period, startTime, endTime } + }); + return { success: true }; + } + + async deleteEvent(id: string) { + await prisma.schedule.delete({ where: { id } }); + return { success: true }; + } +} + +export const commonService = new CommonService(); diff --git a/backend/src/services/config.service.ts b/backend/src/services/config.service.ts new file mode 100644 index 0000000..193608d --- /dev/null +++ b/backend/src/services/config.service.ts @@ -0,0 +1,20 @@ +import { PrismaClient } from '@prisma/client'; + +export class ConfigService { + async testDbConnection(params: { host: string; port?: number; user: string; password?: string; database: string }) { + const { host, port, user, password, database } = params; + if (!host || !user || !database) throw new Error('Missing required fields'); + const dsn = `mysql://${encodeURIComponent(user)}:${encodeURIComponent(password || '')}@${host}:${Number(port) || 3306}/${database}`; + const client = new PrismaClient({ datasources: { db: { url: dsn } } }); + try { + await client.$connect(); + await client.$disconnect(); + return { message: 'Connection successful' }; + } catch (err: any) { + await client.$disconnect().catch(() => {}); + throw new Error(err?.message || 'Connection failed'); + } + } +} + +export const configService = new ConfigService(); diff --git a/backend/src/services/curriculum.service.ts b/backend/src/services/curriculum.service.ts new file mode 100644 index 0000000..04bde1e --- /dev/null +++ b/backend/src/services/curriculum.service.ts @@ -0,0 +1,121 @@ +import prisma from '../utils/prisma'; + +export class CurriculumService { + async getSubjects() { + return prisma.subject.findMany({ where: { isDeleted: false }, select: { id: true, name: true, code: true, icon: true }, orderBy: { name: 'asc' } }); + } + + async getTextbookTree(id: string) { + let textbook = await prisma.textbook.findUnique({ + where: { id, isDeleted: false }, + include: { + units: { + where: { isDeleted: false }, + include: { + lessons: { + where: { isDeleted: false }, + include: { knowledgePoints: { where: { isDeleted: false }, orderBy: { difficulty: 'asc' } } }, + orderBy: { sortOrder: 'asc' } + } + }, + orderBy: { sortOrder: 'asc' } + } + } + }); + if (!textbook) { + textbook = await prisma.textbook.findFirst({ + where: { subjectId: id, isDeleted: false }, + include: { + units: { + where: { isDeleted: false }, + include: { + lessons: { + where: { isDeleted: false }, + include: { knowledgePoints: { where: { isDeleted: false }, orderBy: { difficulty: 'asc' } } }, + orderBy: { sortOrder: 'asc' } + } + }, + orderBy: { sortOrder: 'asc' } + } + } + }); + } + if (!textbook) throw new Error('Textbook not found'); + const units = textbook.units.map(unit => ({ + id: unit.id, + textbookId: unit.textbookId, + name: unit.name, + sortOrder: unit.sortOrder, + lessons: unit.lessons.map(lesson => ({ + id: lesson.id, + unitId: lesson.unitId, + name: lesson.name, + sortOrder: lesson.sortOrder, + knowledgePoints: lesson.knowledgePoints.map(kp => ({ id: kp.id, lessonId: kp.lessonId, name: kp.name, difficulty: kp.difficulty, description: kp.description })) + })) + })); + return { textbook: { id: textbook.id, name: textbook.name, publisher: textbook.publisher, versionYear: textbook.versionYear, coverUrl: textbook.coverUrl }, units }; + } + + async getTextbooksBySubject(subjectId: string) { + return prisma.textbook.findMany({ where: { subjectId, isDeleted: false }, select: { id: true, name: true, publisher: true, versionYear: true, coverUrl: true }, orderBy: { name: 'asc' } }); + } + + async createTextbook(userId: string, data: { subjectId: string; name: string; publisher: string; versionYear: string; coverUrl?: string }) { + const { subjectId, name, publisher, versionYear, coverUrl } = data; + return prisma.textbook.create({ data: { subjectId, name, publisher, versionYear, coverUrl: coverUrl || '', createdBy: userId, updatedBy: userId } }); + } + + async updateTextbook(id: string, data: { name?: string; publisher?: string; versionYear?: string; coverUrl?: string }) { + return prisma.textbook.update({ where: { id }, data }); + } + + async deleteTextbook(id: string) { + await prisma.textbook.update({ where: { id }, data: { isDeleted: true } }); + return { success: true }; + } + + async createUnit(userId: string, data: { textbookId: string; name: string; sortOrder?: number }) { + const { textbookId, name, sortOrder } = data; + return prisma.textbookUnit.create({ data: { textbookId, name, sortOrder: sortOrder || 0, createdBy: userId, updatedBy: userId } }); + } + + async updateUnit(id: string, data: { name?: string; sortOrder?: number }) { + return prisma.textbookUnit.update({ where: { id }, data }); + } + + async deleteUnit(id: string) { + await prisma.textbookUnit.update({ where: { id }, data: { isDeleted: true } }); + return { success: true }; + } + + async createLesson(userId: string, data: { unitId: string; name: string; sortOrder?: number }) { + const { unitId, name, sortOrder } = data; + return prisma.textbookLesson.create({ data: { unitId, name, sortOrder: sortOrder || 0, createdBy: userId, updatedBy: userId } }); + } + + async updateLesson(id: string, data: { name?: string; sortOrder?: number }) { + return prisma.textbookLesson.update({ where: { id }, data }); + } + + async deleteLesson(id: string) { + await prisma.textbookLesson.update({ where: { id }, data: { isDeleted: true } }); + return { success: true }; + } + + async createKnowledgePoint(userId: string, data: { lessonId: string; name: string; difficulty?: number; description?: string }) { + const { lessonId, name, difficulty, description } = data; + return prisma.knowledgePoint.create({ data: { lessonId, name, difficulty: difficulty || 1, description: description || '', createdBy: userId, updatedBy: userId } }); + } + + async updateKnowledgePoint(id: string, data: { name?: string; difficulty?: number; description?: string }) { + return prisma.knowledgePoint.update({ where: { id }, data }); + } + + async deleteKnowledgePoint(id: string) { + await prisma.knowledgePoint.update({ where: { id }, data: { isDeleted: true } }); + return { success: true }; + } +} + +export const curriculumService = new CurriculumService(); diff --git a/backend/src/services/exam.service.ts b/backend/src/services/exam.service.ts index 2abb274..48ca38c 100644 --- a/backend/src/services/exam.service.ts +++ b/backend/src/services/exam.service.ts @@ -2,30 +2,54 @@ import prisma from '../utils/prisma'; import { v4 as uuidv4 } from 'uuid'; export class ExamService { - async getExams(userId: string, query: { subjectId?: string; status?: string }) { - const { subjectId, status } = query; + async getExams(userId: string, query: { subjectId?: string; status?: string; scope?: 'mine' | 'public'; page?: number; pageSize?: number; examType?: string }) { + const { subjectId, status, scope = 'mine', page = 1, pageSize = 20, examType } = query; - const exams = await prisma.exam.findMany({ - where: { - createdBy: userId, - isDeleted: false, - ...(subjectId && { subjectId }), - ...(status && { status: status as any }) - }, - select: { - id: true, - title: true, - subjectId: true, - totalScore: true, - suggestedDuration: true, - status: true, - createdAt: true, - _count: { - select: { nodes: true } - } - }, - orderBy: { createdAt: 'desc' } - }); + const where: any = { + isDeleted: false, + ...(subjectId && { subjectId }), + ...(status && { status: status as any }), + ...(examType && { examType }) + }; + + if (scope === 'mine') { + where.createdBy = userId; + } else { + where.status = 'Published'; + // Optionally exclude own exams from public list if desired, or keep them + } + + const skip = (page - 1) * pageSize; + + const [exams, totalCount] = await Promise.all([ + prisma.exam.findMany({ + where, + select: { + id: true, + title: true, + subjectId: true, + totalScore: true, + suggestedDuration: true, + status: true, + createdAt: true, + createdBy: true, + examType: true, + creator: { + select: { realName: true } + }, + _count: { + select: { + nodes: true, + assignments: true // Count usage + } + } + }, + orderBy: { createdAt: 'desc' }, + skip, + take: Number(pageSize) + }), + prisma.exam.count({ where }) + ]); const result = exams.map(exam => ({ id: exam.id, @@ -34,15 +58,19 @@ export class ExamService { totalScore: Number(exam.totalScore), duration: exam.suggestedDuration, questionCount: exam._count.nodes, + usageCount: exam._count.assignments, status: exam.status, - createdAt: exam.createdAt.toISOString() + createdAt: exam.createdAt.toISOString(), + creatorName: exam.creator.realName, + isMyExam: exam.createdBy === userId, + examType: exam.examType })); return { items: result, - totalCount: result.length, - pageIndex: 1, - pageSize: result.length + totalCount, + pageIndex: Number(page), + pageSize: Number(pageSize) }; } @@ -277,13 +305,25 @@ export class ExamService { // Create new nodes recursively const createNodes = async (nodes: any[], parentId: string | null = null) => { for (const node of nodes) { + // Ensure node.id is a valid UUID if provided, otherwise generate one + // The frontend might send temporary IDs like "node-123456" which are not valid UUIDs + // We should always generate new UUIDs for the database to avoid format errors + // BUT we need to map the old IDs to new IDs to maintain parent-child relationships if we were doing it in parallel, + // but here we do it sequentially and pass the new parentId down. + // HOWEVER, if the frontend sends an ID, it expects that ID to persist? + // No, the frontend re-fetches or updates state. + // Actually, 'node.id' from frontend might be 'node-171...' which is not UUID. + // So we MUST generate a new UUID. + + const dbId = uuidv4(); + const newNode = await prisma.examNode.create({ data: { - id: node.id || uuidv4(), + id: dbId, examId: id, parentNodeId: parentId, nodeType: node.nodeType, - questionId: node.questionId, + questionId: node.questionId?.startsWith('temp-') ? null : node.questionId, // Handle temp IDs if any title: node.title, description: node.description, score: node.score, @@ -303,6 +343,18 @@ export class ExamService { await createNodes(rootNodes); } + // Recalculate total score + const allNodes = await prisma.examNode.findMany({ + where: { examId: id, nodeType: 'Question' }, + select: { score: true } + }); + const totalScore = allNodes.reduce((sum, n) => sum + Number(n.score), 0); + + await prisma.exam.update({ + where: { id }, + data: { totalScore } + }); + return { message: 'Exam structure updated' }; } } diff --git a/backend/src/services/grading.service.ts b/backend/src/services/grading.service.ts new file mode 100644 index 0000000..93eee69 --- /dev/null +++ b/backend/src/services/grading.service.ts @@ -0,0 +1,68 @@ +import prisma from '../utils/prisma'; +import { $Enums } from '@prisma/client'; + +export class GradingService { + async getSubmissions(userId: string, assignmentId: string) { + const assignment = await prisma.assignment.findUnique({ where: { id: assignmentId, isDeleted: false }, include: { class: true } }); + if (!assignment) throw new Error('Assignment not found'); + const isMember = await prisma.classMember.findFirst({ where: { classId: assignment.classId, userId, roleInClass: 'Teacher', isDeleted: false } }); + if (!isMember) throw new Error('Permission denied'); + const submissions = await prisma.studentSubmission.findMany({ where: { assignmentId, isDeleted: false }, include: { student: { select: { id: true, realName: true, studentId: true, avatarUrl: true } } }, orderBy: [{ submissionStatus: 'asc' }, { submitTime: 'desc' }] }); + return submissions.map(s => ({ id: s.id, studentName: s.student.realName, studentId: s.student.studentId, avatarUrl: s.student.avatarUrl, status: s.submissionStatus, score: s.totalScore ? Number(s.totalScore) : null, submitTime: s.submitTime?.toISOString() || null })); + } + + async getPaperForGrading(userId: string, submissionId: string) { + const submission = await prisma.studentSubmission.findUnique({ where: { id: submissionId, isDeleted: false }, include: { student: { select: { realName: true, studentId: true } }, assignment: { include: { exam: { include: { nodes: { where: { isDeleted: false }, include: { question: { include: { knowledgePoints: { select: { knowledgePoint: { select: { name: true } } } } } } }, orderBy: { sortOrder: 'asc' } } } }, class: true } }, details: { include: { examNode: { include: { question: true } } } } } }); + if (!submission) throw new Error('Submission not found'); + const isMember = await prisma.classMember.findFirst({ where: { classId: submission.assignment.classId, userId, roleInClass: 'Teacher', isDeleted: false } }); + if (!isMember) throw new Error('Permission denied'); + const nodes = submission.assignment.exam.nodes.map(node => { + const detail = submission.details.find(d => d.examNodeId === node.id); + return { examNodeId: node.id, questionId: node.questionId, questionContent: node.question?.content, questionType: node.question?.questionType, question: node.question ? { id: node.question.id, content: node.question.content, type: node.question.questionType, difficulty: node.question.difficulty, answer: node.question.answer, parse: node.question.explanation, knowledgePoints: node.question.knowledgePoints.map((kp: any) => kp.knowledgePoint.name) } : undefined, score: Number(node.score), studentAnswer: detail?.studentAnswer || null, studentScore: detail?.score ? Number(detail.score) : null, judgement: detail?.judgement || null, teacherComment: detail?.teacherComment || null }; + }); + return { submissionId: submission.id, studentName: submission.student.realName, studentId: submission.student.studentId, status: submission.submissionStatus, totalScore: submission.totalScore ? Number(submission.totalScore) : null, submitTime: submission.submitTime?.toISOString() || null, nodes }; + } + + async submitGrade(userId: string, submissionId: string, grades: Array<{ examNodeId: string; score?: number; judgement?: string; teacherComment?: string }>) { + if (!grades || !Array.isArray(grades)) throw new Error('Invalid grades data'); + const submission = await prisma.studentSubmission.findUnique({ where: { id: submissionId, isDeleted: false }, include: { assignment: { include: { class: true, exam: { include: { nodes: true } } } } } }); + if (!submission) throw new Error('Submission not found'); + const isMember = await prisma.classMember.findFirst({ where: { classId: submission.assignment.classId, userId, roleInClass: 'Teacher', isDeleted: false } }); + if (!isMember) throw new Error('Permission denied'); + if (submission.submissionStatus === 'Pending') throw new Error('Submission has not been submitted'); + if (submission.assignment.status === 'Archived') throw new Error('Assignment archived'); + + // Teacher can only grade after the deadline (Grading Phase) + const now = new Date(); + if (now <= submission.assignment.endTime) throw new Error('Cannot grade before deadline'); + + const allowedSet = new Set(submission.assignment.exam.nodes.filter(n => n.nodeType === 'Question').map(n => n.id)); + for (const g of grades) { + if (!allowedSet.has(g.examNodeId)) throw new Error('Invalid exam node'); + } + const updatePromises = grades.map(async g => { + const existingDetail = await prisma.submissionDetail.findFirst({ where: { submissionId, examNodeId: g.examNodeId, isDeleted: false } }); + if (existingDetail) { + return prisma.submissionDetail.update({ where: { id: existingDetail.id }, data: { score: g.score, judgement: g.judgement ? (g.judgement as $Enums.JudgementResult) : null, teacherComment: g.teacherComment, updatedBy: userId } }); + } else { + return prisma.submissionDetail.create({ data: { submissionId, examNodeId: g.examNodeId, score: g.score, judgement: g.judgement ? (g.judgement as $Enums.JudgementResult) : null, teacherComment: g.teacherComment, createdBy: userId, updatedBy: userId } }); + } + }); + await Promise.all(updatePromises); + const allDetails = await prisma.submissionDetail.findMany({ where: { submissionId, isDeleted: false } }); + const totalScore = allDetails.reduce((sum, d) => sum + (d.score ? Number(d.score) : 0), 0); + + // Ensure status is updated to 'Graded' + await prisma.studentSubmission.update({ + where: { id: submissionId }, + data: { + submissionStatus: 'Graded', + totalScore, + updatedBy: userId + } + }); + return { message: 'Grading submitted successfully', totalScore }; + } +} + +export const gradingService = new GradingService(); diff --git a/backend/src/services/org.service.ts b/backend/src/services/org.service.ts new file mode 100644 index 0000000..fadd084 --- /dev/null +++ b/backend/src/services/org.service.ts @@ -0,0 +1,103 @@ +import prisma from '../utils/prisma'; +import { v4 as uuidv4 } from 'uuid'; +import { generateInviteCode } from '../utils/helpers'; + +export class OrgService { + async getSchools() { + return prisma.school.findMany({ + where: { isDeleted: false }, + select: { id: true, name: true, regionCode: true, address: true }, + orderBy: { name: 'asc' } + }); + } + + async getMyClasses(userId: string, role?: string) { + const memberships = await prisma.classMember.findMany({ + where: { userId, isDeleted: false, ...(role && { roleInClass: role as any }) }, + include: { + class: { + include: { + grade: { include: { school: true } }, + _count: { select: { members: true } } + } + } + } + }); + return memberships.map(m => ({ + id: m.class.id, + name: m.class.name, + gradeName: m.class.grade.name, + schoolName: m.class.grade.school.name, + inviteCode: m.class.inviteCode, + studentCount: m.class._count.members, + myRole: m.roleInClass + })); + } + + async createClass(userId: string, name: string, gradeId: string) { + const grade = await prisma.grade.findUnique({ where: { id: gradeId, isDeleted: false }, include: { school: true } }); + if (!grade) throw new Error('Grade not found'); + const inviteCode = await generateInviteCode(); + const classId = uuidv4(); + const newClass = await prisma.class.create({ + data: { id: classId, gradeId, name, inviteCode, headTeacherId: userId, createdBy: userId, updatedBy: userId } + }); + await prisma.classMember.create({ + data: { id: uuidv4(), classId, userId, roleInClass: 'Teacher', createdBy: userId, updatedBy: userId } + }); + return { id: newClass.id, name: newClass.name, gradeName: grade.name, schoolName: grade.school.name, inviteCode: newClass.inviteCode, studentCount: 1 }; + } + + async joinClass(userId: string, inviteCode: string) { + const targetClass = await prisma.class.findUnique({ where: { inviteCode, isDeleted: false }, include: { grade: { include: { school: true } } } }); + if (!targetClass) throw new Error('Invalid invite code'); + const existingMember = await prisma.classMember.findFirst({ where: { classId: targetClass.id, userId, isDeleted: false } }); + if (existingMember) throw new Error('You are already a member of this class'); + await prisma.classMember.create({ data: { id: uuidv4(), classId: targetClass.id, userId, roleInClass: 'Student', createdBy: userId, updatedBy: userId } }); + + // Auto-assign existing assignments to the new student + const assignments = await prisma.assignment.findMany({ + where: { classId: targetClass.id, isDeleted: false } + }); + + if (assignments.length > 0) { + const submissionData = assignments.map(a => ({ + id: uuidv4(), + assignmentId: a.id, + studentId: userId, + submissionStatus: 'Pending' as const, + createdBy: userId, + updatedBy: userId + })); + + // Use createMany for efficiency + await prisma.studentSubmission.createMany({ + data: submissionData + }); + } + + return { id: targetClass.id, name: targetClass.name, gradeName: targetClass.grade.name, schoolName: targetClass.grade.school.name }; + } + + async getClassMembers(userId: string, classId: string) { + const targetClass = await prisma.class.findUnique({ where: { id: classId, isDeleted: false } }); + if (!targetClass) throw new Error('Class not found'); + const isMember = await prisma.classMember.findFirst({ where: { classId, userId, isDeleted: false } }); + if (!isMember) throw new Error('You are not a member of this class'); + const members = await prisma.classMember.findMany({ where: { classId, isDeleted: false }, include: { user: { select: { id: true, realName: true, studentId: true, avatarUrl: true, gender: true } } }, orderBy: [{ roleInClass: 'asc' }, { createdAt: 'asc' }] }); + const assignmentsCount = await prisma.assignment.count({ where: { classId } }); + const formattedMembers = await Promise.all(members.map(async m => { + const submissions = await prisma.studentSubmission.findMany({ where: { studentId: m.user.id, assignment: { classId } }, select: { totalScore: true, submissionStatus: true, submitTime: true }, orderBy: { submitTime: 'desc' }, take: 5 }); + const recentTrendRaw = submissions.map(s => s.totalScore ? Number(s.totalScore) : 0); + const recentTrend = recentTrendRaw.concat(Array(Math.max(0, 5 - recentTrendRaw.length)).fill(0)).slice(0, 5); + const completedCount = await prisma.studentSubmission.count({ where: { studentId: m.user.id, assignment: { classId }, submissionStatus: { in: ['Submitted', 'Graded'] } } }); + const attendanceRate = assignmentsCount > 0 ? Math.round((completedCount / assignmentsCount) * 100) : 0; + const latestScore = submissions[0]?.totalScore ? Number(submissions[0].totalScore) : null; + const status = latestScore !== null ? (latestScore >= 90 ? 'Excellent' : (latestScore < 60 ? 'AtRisk' : 'Active')) : 'Active'; + return { id: m.user.id, studentId: m.user.studentId, realName: m.user.realName, avatarUrl: m.user.avatarUrl, gender: m.user.gender, role: m.roleInClass, recentTrend, status, attendanceRate }; + })); + return formattedMembers; + } +} + +export const orgService = new OrgService(); diff --git a/backend/src/services/question.service.ts b/backend/src/services/question.service.ts new file mode 100644 index 0000000..7634a5f --- /dev/null +++ b/backend/src/services/question.service.ts @@ -0,0 +1,131 @@ +import prisma from '../utils/prisma'; +import { $Enums } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; + +export class QuestionService { + async search(userId: string, params: { + subjectId?: string; + questionType?: string; + difficulty?: number; + difficultyMin?: number; + difficultyMax?: number; + keyword?: string; + createdBy?: string; // 'me' or specific userId + sortBy?: 'latest' | 'popular'; + page?: number; + pageSize?: number; + }) { + const { + subjectId, + questionType, + difficulty, + difficultyMin, + difficultyMax, + keyword, + createdBy, + sortBy = 'latest', + page = 1, + pageSize = 10 + } = params; + + const skip = (page - 1) * pageSize; + const where: any = { + isDeleted: false, + ...(subjectId && { subjectId }), + ...(questionType && { questionType }), + ...(keyword && { content: { contains: keyword } }) + }; + if (difficultyMin || difficultyMax) { + where.difficulty = {}; + if (difficultyMin) where.difficulty.gte = difficultyMin; + if (difficultyMax) where.difficulty.lte = difficultyMax; + } else if (typeof difficulty === 'number') { + where.difficulty = difficulty; + } + if (createdBy === 'me') where.createdBy = userId; + else if (createdBy) where.createdBy = createdBy; + + let orderBy: any = { createdAt: 'desc' }; + if (sortBy === 'popular') orderBy = { usageCount: 'desc' }; + + const [questions, totalCount] = await Promise.all([ + prisma.question.findMany({ + where, + select: { + id: true, + content: true, + questionType: true, + difficulty: true, + answer: true, + explanation: true, + createdAt: true, + createdBy: true, + knowledgePoints: { select: { knowledgePoint: { select: { name: true } } } } + }, + skip, + take: pageSize, + orderBy + }), + prisma.question.count({ where }) + ]); + + const items = questions.map(q => ({ + id: q.id, + content: q.content, + type: q.questionType, + difficulty: q.difficulty, + answer: q.answer, + parse: q.explanation, + knowledgePoints: q.knowledgePoints.map(kp => kp.knowledgePoint.name), + isMyQuestion: q.createdBy === userId + })); + + return { items, totalCount, pageIndex: page, pageSize }; + } + + async create(userId: string, data: { subjectId: string; content: string; questionType: string; difficulty?: number; answer: string; explanation?: string; optionsConfig?: any }) { + const { subjectId, content, questionType, difficulty = 3, answer, explanation, optionsConfig } = data; + if (!subjectId || !content || !questionType || !answer) throw new Error('Missing required fields'); + const questionId = uuidv4(); + const question = await prisma.question.create({ + data: { id: questionId, subjectId, content, questionType: questionType as $Enums.QuestionType, difficulty, answer, explanation, optionsConfig: optionsConfig || null, createdBy: userId, updatedBy: userId } + }); + return { id: question.id, message: 'Question created successfully' }; + } + + async update(userId: string, id: string, data: { content?: string; questionType?: string; difficulty?: number; answer?: string; explanation?: string; optionsConfig?: any }) { + const question = await prisma.question.findUnique({ where: { id } }); + if (!question) throw new Error('Question not found'); + if (question.createdBy !== userId) throw new Error('Permission denied'); + const { questionType: qt, ...rest } = data; + await prisma.question.update({ where: { id }, data: { ...rest, ...(qt && { questionType: qt as $Enums.QuestionType }), optionsConfig: data.optionsConfig || null, updatedBy: userId } }); + return { message: 'Question updated successfully' }; + } + + async softDelete(userId: string, id: string) { + const question = await prisma.question.findUnique({ where: { id } }); + if (!question) throw new Error('Question not found'); + if (question.createdBy !== userId) throw new Error('Permission denied'); + await prisma.question.update({ where: { id }, data: { isDeleted: true } }); + return { message: 'Question deleted successfully' }; + } + + parseText(text: string) { + if (!text) throw new Error('Text is required'); + const questions = text.split(/\n\s*\n/).map(block => { + const lines = block.trim().split('\n'); + const content = lines[0]; + const options = lines.slice(1).filter(l => /^[A-D]\./.test(l)); + return { + content, + type: options.length > 0 ? 'SingleChoice' : 'Subjective', + options: options.length > 0 ? options : undefined, + answer: 'A', + parse: '解析暂无' + }; + }); + return questions; + } +} + +export const questionService = new QuestionService(); diff --git a/backend/src/services/submission.service.ts b/backend/src/services/submission.service.ts new file mode 100644 index 0000000..ae49d2c --- /dev/null +++ b/backend/src/services/submission.service.ts @@ -0,0 +1,476 @@ +import prisma from '../utils/prisma'; +import { v4 as uuidv4 } from 'uuid'; +import { calculateRank } from '../utils/helpers'; +import { isClassMember } from '../utils/helpers'; + +export class SubmissionService { + async getStudentPaper(userId: string, assignmentId: string) { + const assignment = await prisma.assignment.findUnique({ + where: { id: assignmentId, isDeleted: false }, + include: { + exam: { + include: { + nodes: { + where: { isDeleted: false }, + include: { + question: { + include: { + knowledgePoints: { select: { knowledgePoint: { select: { name: true } } } } + } + } + }, + orderBy: { sortOrder: 'asc' } + } + } + } + } + }); + if (!assignment) throw new Error('Assignment not found'); + if (assignment.status === 'Archived') throw new Error('Assignment archived'); + const member = await isClassMember(userId, assignment.classId); + if (!member) throw new Error('Permission denied'); + const now = new Date(); + if (now < assignment.startTime) throw new Error('Assignment has not started yet'); + if (now > assignment.endTime && !assignment.allowLateSubmission) throw new Error('Assignment has ended'); + + let submission = await prisma.studentSubmission.findFirst({ + where: { assignmentId, studentId: userId, isDeleted: false }, + include: { details: true } + }); + if (!submission) { + submission = await prisma.studentSubmission.create({ + data: { id: uuidv4(), assignmentId, studentId: userId, submissionStatus: 'Pending', createdBy: userId, updatedBy: userId }, + include: { details: true } + }); + } + + // If not started, mark as started + if (!submission.startedAt) { + await prisma.studentSubmission.update({ + where: { id: submission.id }, + data: { startedAt: new Date(), updatedBy: userId } + }); + } + + const buildTree = (nodes: any[], parentId: string | null = null): any[] => { + return nodes + .filter((node) => node.parentNodeId === parentId) + .map((node) => { + const detail = submission!.details.find((d) => d.examNodeId === node.id); + return { + id: node.id, + nodeType: node.nodeType, + title: node.title, + description: node.description, + questionId: node.questionId, + questionContent: node.question?.content, + questionType: node.question?.questionType, + question: node.question + ? { + id: node.question.id, + content: node.question.content, + type: node.question.questionType, + difficulty: node.question.difficulty, + knowledgePoints: node.question.knowledgePoints.map((kp: any) => kp.knowledgePoint.name), + options: (() => { + const cfg: any = (node as any).question?.optionsConfig; + if (!cfg) return []; + try { + if (Array.isArray(cfg)) return cfg.map((v: any) => String(v)); + if (cfg.options && Array.isArray(cfg.options)) return cfg.options.map((v: any) => String(v)); + if (typeof cfg === 'object') { + return Object.keys(cfg) + .sort() + .map((k) => String(cfg[k])); + } + return []; + } catch { + return []; + } + })() + } + : undefined, + score: Number(node.score), + sortOrder: node.sortOrder, + studentAnswer: detail?.studentAnswer || null, + children: buildTree(nodes, node.id) + }; + }); + }; + + const rootNodes = buildTree(assignment.exam.nodes); + return { + examId: assignment.exam.id, + title: assignment.title, + duration: assignment.exam.suggestedDuration, + totalScore: Number(assignment.exam.totalScore), + startTime: assignment.startTime.toISOString(), + endTime: assignment.endTime.toISOString(), + submissionId: submission.id, + status: submission.submissionStatus, + rootNodes + }; + } + + async saveProgress( + userId: string, + assignmentId: string, + answers: Array<{ examNodeId: string; studentAnswer: any }> + ) { + if (!answers || !Array.isArray(answers)) throw new Error('Invalid answers data'); + const submission = await prisma.studentSubmission.findFirst({ + where: { assignmentId, studentId: userId, isDeleted: false }, + include: { assignment: true } + }); + if (!submission) throw new Error('Submission not found'); + if (submission.assignment.status === 'Archived') throw new Error('Assignment archived'); + const member = await isClassMember(userId, submission.assignment.classId); + if (!member) throw new Error('Permission denied'); + const now = new Date(); + if (now < submission.assignment.startTime) throw new Error('Assignment has not started yet'); + if (now > submission.assignment.endTime && !submission.assignment.allowLateSubmission) throw new Error('Cannot save progress after deadline'); + const allowedNodeIds = await prisma.examNode.findMany({ where: { examId: submission.assignment.examId, isDeleted: false }, select: { id: true } }); + const allowedSet = new Set(allowedNodeIds.map(n => n.id)); + for (const a of answers) { + if (!allowedSet.has(a.examNodeId)) throw new Error('Invalid exam node'); + } + + // Cannot save if already submitted or graded + if (submission.submissionStatus !== 'Pending') { + throw new Error('Cannot save progress for submitted assignment'); + } + + const updatePromises = answers.map(async (answer) => { + const { examNodeId, studentAnswer } = answer; + const existingDetail = await prisma.submissionDetail.findFirst({ + where: { submissionId: submission!.id, examNodeId, isDeleted: false } + }); + if (existingDetail) { + return prisma.submissionDetail.update({ + where: { id: existingDetail.id }, + data: { studentAnswer, updatedBy: userId } + }); + } else { + return prisma.submissionDetail.create({ + data: { id: uuidv4(), submissionId: submission!.id, examNodeId, studentAnswer, createdBy: userId, updatedBy: userId } + }); + } + }); + await Promise.all(updatePromises); + + await prisma.studentSubmission.update({ + where: { id: submission.id }, + data: { updatedBy: userId } + }); + + return { message: 'Progress saved successfully' }; + } + + async submitAnswers( + userId: string, + assignmentId: string, + answers: Array<{ examNodeId: string; studentAnswer: any }>, + timeSpent?: number + ) { + if (!answers || !Array.isArray(answers)) throw new Error('Invalid answers data'); + + // Fetch submission with exam nodes and questions for auto-grading + const submission = await prisma.studentSubmission.findFirst({ + where: { assignmentId, studentId: userId, isDeleted: false }, + include: { + assignment: { + include: { + exam: { + include: { + nodes: { + include: { question: true } + } + } + } + } + } + } + }); + + if (!submission) throw new Error('Submission not found'); + if (submission.assignment.status === 'Archived') throw new Error('Assignment archived'); + const member = await isClassMember(userId, submission.assignment.classId); + if (!member) throw new Error('Permission denied'); + const now = new Date(); + if (now < submission.assignment.startTime) throw new Error('Assignment has not started yet'); + if (now > submission.assignment.endTime && !submission.assignment.allowLateSubmission) throw new Error('Cannot submit after deadline'); + if (submission.submissionStatus !== 'Pending') throw new Error('Already submitted'); + + const allowedNodes = submission.assignment.exam.nodes; + const allowedSet = new Set(allowedNodes.map(n => n.id)); + for (const a of answers) { + if (!allowedSet.has(a.examNodeId)) throw new Error('Invalid exam node'); + } + + // Auto-grading logic + let allQuestionsGraded = true; + // Check if there are any questions in the exam. If no questions, it is effectively graded. + const questionNodes = allowedNodes.filter(n => n.nodeType === 'Question'); + if (questionNodes.length === 0) allQuestionsGraded = true; + + const updatePromises = answers.map(async (answer) => { + const { examNodeId, studentAnswer } = answer; + const node = allowedNodes.find(n => n.id === examNodeId); + + let score: number | undefined = undefined; + let judgement: any = undefined; // JudgementResult + + // Perform auto-grading if enabled and question exists + if (submission.assignment.autoScoreEnabled && node && node.question) { + const qType = node.question.questionType; + const standardAnswer = node.question.answer || ''; + const maxScore = Number(node.score); + const studAnsStr = String(studentAnswer || '').trim(); + + // Simple auto-grading for objective questions + if (qType === 'SingleChoice' || qType === 'TrueFalse') { + if (studAnsStr === standardAnswer.trim()) { + score = maxScore; + judgement = 'Correct'; + } else { + score = 0; + judgement = 'Incorrect'; + } + } else if (qType === 'MultipleChoice') { + // Normalize by sorting comma-separated values + // e.g. "A,B" vs "B, A" + const normalize = (s: string) => s.split(/[,,]/).map(x => x.trim()).filter(x => x).sort().join(','); + if (normalize(studAnsStr) === normalize(standardAnswer)) { + score = maxScore; + judgement = 'Correct'; + } else { + score = 0; + judgement = 'Incorrect'; + } + } else { + // Subjective or complex types require manual grading + // If any question cannot be auto-graded, the whole submission is not fully graded + allQuestionsGraded = false; + } + } else if (node && node.nodeType === 'Question') { + // If auto-score disabled or no question data, assume manual + allQuestionsGraded = false; + } + + const existingDetail = await prisma.submissionDetail.findFirst({ + where: { submissionId: submission!.id, examNodeId, isDeleted: false } + }); + + const data = { + studentAnswer: String(studentAnswer), // Ensure string + score: score, // specific score for this update + judgement: judgement, + updatedBy: userId + }; + + if (existingDetail) { + return prisma.submissionDetail.update({ + where: { id: existingDetail.id }, + data: data + }); + } else { + return prisma.submissionDetail.create({ + data: { + id: uuidv4(), + submissionId: submission!.id, + examNodeId, + ...data, + createdBy: userId + } + }); + } + }); + + await Promise.all(updatePromises); + + // Check if we missed any questions (unanswered questions) + // If the student didn't answer a question, it won't be in 'answers'. + // We need to check if all questions in the exam have a corresponding detail with a score. + // However, the loop above only processes submitted answers. + // If 'allQuestionsGraded' is true so far (meaning all SUBMITTED answers were auto-graded), + // we still need to check if there are any questions that were NOT submitted. + // But usually, frontend sends all answers (even empty). + // Let's check the database for completeness to be sure? + // For simplicity/performance, we rely on the flag 'allQuestionsGraded' derived from input. + // BUT, if there are unsubmitted questions, they remain ungraded. + // So strictly, we should count graded details vs total questions. + + // Let's calculate total score + const allDetails = await prisma.submissionDetail.findMany({ + where: { submissionId: submission.id, isDeleted: false } + }); + + const calculatedTotalScore = allDetails.reduce((acc, curr) => acc + (Number(curr.score) || 0), 0); + + // Determine final status + // If all questions are objective AND auto-score enabled, we can set to Graded. + // But we also need to ensure all questions were covered. + // If the loop set allQuestionsGraded = false, then it's Submitted. + // Also need to check if we have details for all questions? + // If a student leaves a SingleChoice blank, it should be graded as 0. + // If it's not in 'answers', it won't be created/updated. + // So we might have missing details. + // A robust implementation would fill in missing details as incorrect. + // For now, let's just stick to: if we encountered any non-auto-gradable question, status is Submitted. + // Otherwise, Graded. + + const finalStatus = allQuestionsGraded ? 'Graded' : 'Submitted'; + + await prisma.studentSubmission.update({ + where: { id: submission.id }, + data: { + submissionStatus: finalStatus, + submitTime: new Date(), + timeSpentSeconds: timeSpent || null, + totalScore: calculatedTotalScore, + updatedBy: userId + } + }); + + return { message: 'Answers submitted successfully', submissionId: submission.id }; + } + + async getSubmissionResult(userId: string, submissionId: string) { + const submission = await prisma.studentSubmission.findUnique({ + where: { id: submissionId, isDeleted: false }, + include: { + student: { select: { realName: true } }, + assignment: { + include: { + exam: { + include: { + nodes: { + where: { isDeleted: false }, + include: { question: { include: { knowledgePoints: { select: { knowledgePoint: { select: { name: true } } } } } } }, + orderBy: { sortOrder: 'asc' } + } + } + } + } + }, + details: { include: { examNode: { include: { question: true } } } } + } + }); + if (!submission) throw new Error('Submission not found'); + if (submission.studentId !== userId) throw new Error('Permission denied'); + + // Strictly allow viewing ONLY if assignment is Archived (Ended) + if (submission.assignment.status !== 'Archived') { + return { submissionId: submission.id, status: submission.submissionStatus, message: 'Results not published yet' }; + } + if (submission.submissionStatus !== 'Graded') { + return { submissionId: submission.id, status: submission.submissionStatus, message: 'Your submission has not been graded yet' }; + } + const totalScore = Number(submission.totalScore || 0); + const { rank, totalStudents, beatRate } = await calculateRank(submission.assignmentId, totalScore); + const nodes = submission.assignment.exam.nodes.map((node) => { + const detail = submission.details.find((d) => d.examNodeId === node.id); + return { + examNodeId: node.id, + questionId: node.questionId, + questionContent: node.question?.content, + questionType: node.question?.questionType, + question: node.question + ? { + id: node.question.id, + content: node.question.content, + type: node.question.questionType, + difficulty: node.question.difficulty, + answer: node.question.answer, + parse: node.question.explanation, + knowledgePoints: node.question.knowledgePoints.map((kp: any) => kp.knowledgePoint.name) + } + : undefined, + score: Number(node.score), + studentScore: detail?.score ? Number(detail.score) : null, + studentAnswer: detail?.studentAnswer || null, + autoCheckResult: detail?.judgement === 'Correct', + teacherComment: detail?.teacherComment || null + }; + }); + return { + submissionId: submission.id, + studentName: submission.student.realName, + totalScore, + rank, + totalStudents, + beatRate, + submitTime: submission.submitTime?.toISOString() || null, + nodes + }; + } + + async getSubmissionResultByAssignment(userId: string, assignmentId: string) { + const submission = await prisma.studentSubmission.findFirst({ + where: { assignmentId, studentId: userId, isDeleted: false }, + include: { + student: { select: { realName: true } }, + assignment: { + include: { + exam: { + include: { + nodes: { + where: { isDeleted: false }, + include: { question: { include: { knowledgePoints: { select: { knowledgePoint: { select: { name: true } } } } } } }, + orderBy: { sortOrder: 'asc' } + } + } + } + } + }, + details: { include: { examNode: { include: { question: true } } } } + } + }); + if (!submission) throw new Error('Submission not found'); + if (submission.assignment.status !== 'Archived') { + return { submissionId: submission.id, status: submission.submissionStatus, message: 'Results not published yet' }; + } + if (submission.submissionStatus !== 'Graded') { + return { submissionId: submission.id, status: submission.submissionStatus, message: 'Your submission has not been graded yet' }; + } + const totalScore = Number(submission.totalScore || 0); + const { rank, totalStudents, beatRate } = await calculateRank(submission.assignmentId, totalScore); + const nodes = submission.assignment.exam.nodes.map((node) => { + const detail = submission.details.find((d) => d.examNodeId === node.id); + return { + examNodeId: node.id, + questionId: node.questionId, + questionContent: node.question?.content, + questionType: node.question?.questionType, + question: node.question + ? { + id: node.question.id, + content: node.question.content, + type: node.question.questionType, + difficulty: node.question.difficulty, + answer: node.question.answer, + parse: node.question.explanation, + knowledgePoints: node.question.knowledgePoints.map((kp: any) => kp.knowledgePoint.name) + } + : undefined, + score: Number(node.score), + studentScore: detail?.score ? Number(detail.score) : null, + studentAnswer: detail?.studentAnswer || null, + autoCheckResult: detail?.judgement === 'Correct', + teacherComment: detail?.teacherComment || null + }; + }); + return { + submissionId: submission.id, + studentName: submission.student.realName, + totalScore, + rank, + totalStudents, + beatRate, + submitTime: submission.submitTime?.toISOString() || null, + nodes + }; + } +} + +export const submissionService = new SubmissionService(); diff --git a/package-lock.json b/package-lock.json index f25349f..b9b4f89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,8 @@ "@types/react": "^18.2.73", "@types/react-dom": "^18.2.22", "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.0", "postcss": "^8.4.38", "tailwindcss": "^3.4.1", "typescript": "^5.4.3" @@ -49,6 +51,188 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -88,12 +272,35 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@next/env": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.0.tgz", "integrity": "sha512-4+70ELtSbRtYUuyRpAJmKC8NHBW2x1HMje9KO2Xd7IkoyucmV9SjgO+qeWMC0JWkRQXgydv1O7yKOK8nu/rITQ==", "license": "MIT" }, + "node_modules/@next/eslint-plugin-next": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.0.tgz", + "integrity": "sha512-QkM01VPhwcupezVevy9Uyl1rmpg2PimhMjkb+ySmnPgSKUUM/PGGRQxdFgMpHv/JzQoC8kRySgKeM441GiizcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "10.3.10" + } + }, "node_modules/@next/swc-darwin-arm64": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.0.tgz", @@ -276,6 +483,41 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -292,6 +534,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -355,6 +608,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", @@ -378,6 +638,7 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -393,6 +654,483 @@ "@types/react": "^18.0.0" } }, + "node_modules/@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -421,6 +1159,210 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/autoprefixer": { "version": "10.4.22", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", @@ -459,6 +1401,49 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.8.31", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", @@ -482,6 +1467,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -515,6 +1511,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -540,6 +1537,66 @@ "node": ">=10.16.0" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -570,6 +1627,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -623,6 +1697,26 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -633,6 +1727,28 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -773,12 +1889,134 @@ "node": ">=12" } }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -786,6 +2024,19 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -793,6 +2044,19 @@ "dev": true, "license": "MIT" }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -803,6 +2067,28 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.260", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz", @@ -810,6 +2096,190 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -820,12 +2290,480 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.0.tgz", + "integrity": "sha512-N0eQkn/wz557mIpW4JQWGEv4wGU8zvJ7emLHMS15uC18jjaU4kx6leR4U9QYT/eNghUZT7N9lBlfd8E4N0cp1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "14.2.0", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.0.0-canary-7118f5dd7-20230705", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", + "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-equals": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", @@ -865,6 +2803,20 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -875,6 +2827,19 @@ "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -888,6 +2853,78 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -929,6 +2966,13 @@ } } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -954,6 +2998,140 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -967,12 +3145,193 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -986,6 +3345,77 @@ "node": ">= 0.4" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -995,6 +3425,60 @@ "node": ">=12" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1008,6 +3492,46 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -1024,6 +3548,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1034,6 +3593,52 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1047,6 +3652,32 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1057,12 +3688,236 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -1073,6 +3928,113 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1093,12 +4055,35 @@ "dev": true, "license": "MIT" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1111,6 +4096,13 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/lucide-react": { "version": "0.368.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.368.0.tgz", @@ -1120,6 +4112,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1144,6 +4146,39 @@ "node": ">=8.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -1159,6 +4194,13 @@ "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", "license": "MIT" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -1189,6 +4231,29 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/next": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/next/-/next-14.2.0.tgz", @@ -1313,6 +4378,240 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1320,6 +4619,33 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1359,6 +4685,16 @@ "node": ">= 6" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -1379,6 +4715,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -1522,6 +4859,16 @@ "dev": true, "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -1539,6 +4886,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -1565,6 +4922,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1577,6 +4935,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -1677,6 +5036,50 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -1698,6 +5101,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -1709,6 +5132,45 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -1733,6 +5195,61 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1742,6 +5259,190 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1751,6 +5452,27 @@ "node": ">=0.10.0" } }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -1759,6 +5481,239 @@ "node": ">=10.0.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -1805,6 +5760,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -1866,6 +5834,13 @@ "node": ">=14.0.0" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -1936,6 +5911,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1956,6 +5932,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -1963,18 +5952,136 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1983,6 +6090,25 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -1990,6 +6116,41 @@ "dev": true, "license": "MIT" }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -2021,6 +6182,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2049,6 +6220,242 @@ "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 1e77473..20d0cc6 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,19 @@ "version": "2.0.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -H 127.0.0.1 -p 8080", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { + "clsx": "^2.1.0", + "framer-motion": "^11.0.24", + "lucide-react": "^0.368.0", "next": "14.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "lucide-react": "^0.368.0", - "framer-motion": "^11.0.24", "recharts": "^2.12.4", - "clsx": "^2.1.0", "tailwind-merge": "^2.2.2" }, "devDependencies": { @@ -23,8 +23,10 @@ "@types/react": "^18.2.73", "@types/react-dom": "^18.2.22", "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.0", "postcss": "^8.4.38", "tailwindcss": "^3.4.1", "typescript": "^5.4.3" } -} \ No newline at end of file +} diff --git a/src/app/(dashboard)/assignments/[id]/analysis/page.tsx b/src/app/(dashboard)/assignments/[id]/analysis/page.tsx new file mode 100644 index 0000000..9607d62 --- /dev/null +++ b/src/app/(dashboard)/assignments/[id]/analysis/page.tsx @@ -0,0 +1,21 @@ +"use client"; + +import React from 'react'; +import { AssignmentAnalysis } from '@/features/assignment/components/AssignmentAnalysis'; +import { useRouter, useSearchParams } from 'next/navigation'; + +export default function AssignmentAnalysisPage({ params }: { params: { id: string } }) { + const router = useRouter(); + const searchParams = useSearchParams(); + const tab = searchParams.get('tab') as 'overview' | 'details' | null; + + return ( +
+ router.push('/assignments')} + initialTab={tab || 'details'} + /> +
+ ); +} diff --git a/src/app/(dashboard)/assignments/page.tsx b/src/app/(dashboard)/assignments/page.tsx index d778d93..20c6330 100644 --- a/src/app/(dashboard)/assignments/page.tsx +++ b/src/app/(dashboard)/assignments/page.tsx @@ -3,18 +3,16 @@ import React, { useState } from 'react'; import { useAuth } from '@/lib/auth-context'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence } from 'framer-motion'; import { TeacherAssignmentList } from '@/features/assignment/components/TeacherAssignmentList'; import { StudentAssignmentList } from '@/features/assignment/components/StudentAssignmentList'; import { CreateAssignmentModal } from '@/features/assignment/components/CreateAssignmentModal'; -import { AssignmentStats } from '@/features/assignment/components/AssignmentStats'; import { useRouter } from 'next/navigation'; export default function AssignmentsPage() { const { user } = useAuth(); const router = useRouter(); const [isCreating, setIsCreating] = useState(false); - const [analyzingId, setAnalyzingId] = useState(null); if (!user) return null; @@ -26,45 +24,31 @@ export default function AssignmentsPage() { }; const handleNavigateToPreview = (id: string) => { - router.push(`/student-exam/${id}`); + // Navigate to the new Analysis/Preview page + router.push(`/assignments/${id}/analysis?tab=details`); + }; + + const handleAnalyze = (id: string) => { + router.push(`/assignments/${id}/analysis?tab=overview`); }; const handleViewResult = (id: string) => { router.push(`/student-result/${id}`); }; - if (analyzingId && !isStudent) { - return ( - // Fix: cast props to any to avoid framer-motion type errors - - setAnalyzingId(null)} - /> - - ); - } - return ( <>
{isStudent ? ( router.push(`/student-exam/${id}`)} onViewResult={handleViewResult} /> ) : ( )} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index b219d70..8970ca2 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,48 +1,15 @@ -import { NextRequest } from 'next/server'; -import { successResponse, errorResponse, dbDelay } from '@/lib/server-utils'; +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001/api'; export async function POST(request: NextRequest) { - await dbDelay(); - - try { - const body = await request.json(); - const { username, password } = body; - - // Simple mock validation - if (!username) { - return errorResponse('Username is required'); - } - - let role = 'Teacher'; - let name = '李明'; - let id = 'u-tea-1'; - - if (username === 'student' || username.startsWith('s')) { - role = 'Student'; - name = '王小明'; - id = 'u-stu-1'; - } else if (username === 'admin') { - role = 'Admin'; - name = '系统管理员'; - id = 'u-adm-1'; - } - - const token = `mock-jwt-token-${id}-${Date.now()}`; - - return successResponse({ - token, - user: { - id, - realName: name, - studentId: username, - avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${name}`, - gender: 'Male', - schoolId: 's-1', - role - } - }); - } catch (e) { - return errorResponse('Invalid request body'); - } + const body = await request.json(); + const res = await fetch(`${API_BASE_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + const data = await res.json().catch(() => ({})); + return NextResponse.json(data, { status: res.status }); } diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index 3ce606a..999e3dc 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -1,28 +1,14 @@ -import { NextRequest } from 'next/server'; -import { successResponse, errorResponse, extractToken, dbDelay } from '@/lib/server-utils'; +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001/api'; export async function GET(request: NextRequest) { - await dbDelay(); - - const token = extractToken(request); - if (!token) { - return errorResponse('Unauthorized', 401); - } - - // In a real app, verify JWT here. - // For mock, we return a default user or parse the mock token if it contained info. - - return successResponse({ - id: "u-1", - realName: "李明 (Real API)", - studentId: "T2024001", - avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alex", - gender: "Male", - schoolId: "s-1", - role: "Teacher", - email: 'liming@school.edu', - phone: '13800138000', - bio: '来自真实 API 的数据' + const authHeader = request.headers.get('authorization') || ''; + const res = await fetch(`${API_BASE_URL}/auth/me`, { + method: 'GET', + headers: { Authorization: authHeader } }); + const data = await res.json().catch(() => ({})); + return NextResponse.json(data, { status: res.status }); } diff --git a/src/app/api/config/db/route.ts b/src/app/api/config/db/route.ts index 791eb37..17ff5ca 100644 --- a/src/app/api/config/db/route.ts +++ b/src/app/api/config/db/route.ts @@ -1,27 +1,15 @@ -import { NextRequest } from 'next/server'; -import { successResponse, errorResponse } from '@/lib/server-utils'; -import { db } from '@/lib/db'; +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001/api'; export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { host, port, user, password, database } = body; - - if (!host || !user) { - return errorResponse('Missing required fields'); - } - - await db.testConnection({ - host, - port: Number(port), - user, - password, - database - }); - - return successResponse({ message: 'Connection successful' }); - } catch (e: any) { - return errorResponse(e.message || 'Connection failed', 500); - } + const body = await request.json(); + const res = await fetch(`${API_BASE_URL}/config/db`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + const data = await res.json().catch(() => ({})); + return NextResponse.json(data, { status: res.status }); } diff --git a/src/app/api/org/classes/route.ts b/src/app/api/org/classes/route.ts index 060c886..243813c 100644 --- a/src/app/api/org/classes/route.ts +++ b/src/app/api/org/classes/route.ts @@ -1,23 +1,14 @@ -import { NextRequest } from 'next/server'; -import { successResponse, dbDelay } from '@/lib/server-utils'; +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001/api'; export async function GET(request: NextRequest) { - await dbDelay(); - - const { searchParams } = new URL(request.url); - const role = searchParams.get('role'); - - let classes = [ - { id: 'c-1', name: '高一 (10) 班', gradeName: '高一年级', teacherName: '李明', studentCount: 32, inviteCode: 'X7K9P' }, - { id: 'c-2', name: '高一 (12) 班', gradeName: '高一年级', teacherName: '张伟', studentCount: 28, inviteCode: 'M2L4Q' }, - { id: 'c-3', name: 'AP 微积分先修班', gradeName: '高三年级', teacherName: '李明', studentCount: 15, inviteCode: 'Z9J1W' }, - { id: 'c-4', name: '物理奥赛集训队', gradeName: '高二年级', teacherName: '王博士', studentCount: 20, inviteCode: 'H4R8T' }, - ]; - - if (role === 'Student') { - classes = classes.slice(0, 1); - } - - return successResponse(classes); + const url = new URL(request.url); + const role = url.searchParams.get('role') || ''; + const res = await fetch(`${API_BASE_URL}/org/classes?role=${encodeURIComponent(role)}`, { + method: 'GET' + }); + const data = await res.json().catch(() => ({})); + return NextResponse.json(data, { status: res.status }); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 33582ae..91d21a3 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -7,11 +7,11 @@ import Link from 'next/link'; import { motion, AnimatePresence } from 'framer-motion'; import { LayoutDashboard, BookOpen, FileQuestion, Users, Settings, LogOut, - Bell, Search, GraduationCap, ScrollText, ClipboardList, Database, + Bell, Search, GraduationCap, ScrollText, ClipboardList, Menu, X, CalendarDays, Terminal } from 'lucide-react'; import { useAuth } from '@/lib/auth-context'; -import { getApiMode, setApiMode } from '@/services/api'; +import { } from '@/services/api'; const NavItem = ({ icon: Icon, label, href, isActive }: any) => ( @@ -45,7 +45,6 @@ export const Sidebar = () => { const { user, logout } = useAuth(); const pathname = usePathname() || ''; const router = useRouter(); - const [isMock, setIsMock] = useState(getApiMode()); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const renderNavItems = () => { @@ -89,14 +88,6 @@ export const Sidebar = () => {
- - diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 1b83383..440eca5 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -7,6 +7,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes { size?: 'sm' | 'md' | 'lg'; loading?: boolean; icon?: React.ReactNode; + iconPosition?: 'left' | 'right'; } export const Button: React.FC = ({ @@ -15,6 +16,7 @@ export const Button: React.FC = ({ size = 'md', loading = false, icon, + iconPosition = 'left', className = '', disabled, ...props @@ -42,8 +44,9 @@ export const Button: React.FC = ({ {...props} > {loading && } - {!loading && icon} + {!loading && icon && iconPosition === 'left' && icon} {children} + {!loading && icon && iconPosition === 'right' && icon} ); }; diff --git a/src/features/assignment/components/AssignmentAnalysis.tsx b/src/features/assignment/components/AssignmentAnalysis.tsx new file mode 100644 index 0000000..cb373c9 --- /dev/null +++ b/src/features/assignment/components/AssignmentAnalysis.tsx @@ -0,0 +1,335 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, + PieChart, Pie, Cell, Legend +} from 'recharts'; +import { + ChevronDown, ChevronUp, Users, AlertCircle, CheckCircle, HelpCircle, + BarChart2, BookOpen, Target, ArrowLeft +} from 'lucide-react'; +import { AssignmentAnalysisDto } from '../../../../UI_DTO'; +import { assignmentService } from '@/services/api'; +import { Card } from '@/components/ui/Card'; +import { Loader2 } from 'lucide-react'; + +interface AssignmentAnalysisProps { + assignmentId: string; + onBack?: () => void; + initialTab?: 'overview' | 'details'; +} + +export const AssignmentAnalysis: React.FC = ({ assignmentId, onBack, initialTab = 'details' }) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState<'overview' | 'details'>(initialTab); + const [expandedQuestionId, setExpandedQuestionId] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const result = await assignmentService.getAssignmentAnalysis(assignmentId); + setData(result); + } catch (err: any) { + setError(err.message || 'Failed to load analysis data'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [assignmentId]); + + if (loading) { + return ( +
+ + 正在加载分析数据... +
+ ); + } + + if (error || !data) { + return ( +
+ +

无法加载数据

+

{error}

+ +
+ ); + } + + const toggleQuestion = (id: string) => { + setExpandedQuestionId(prev => prev === id ? null : id); + }; + + // Prepare chart data + const questionErrorData = data.questions.map(q => ({ + name: `Q${q.score ? '' : ''}${q.title.substring(0, 5)}...`, + errorCount: q.errorCount, + errorRate: Math.round(q.errorRate * 100), + fullTitle: q.title + })); + + const kpErrorData = data.knowledgePoints.map(kp => ({ + name: kp.name, + errorRate: Math.round(kp.errorRate * 100) + })); + + return ( +
+ {/* Header */} +
+
+
+ +

{data.overview.title}

+
+

试卷: {data.overview.examTitle}

+
+ +
+ + +
+
+ + {/* Stats Cards */} +
+ + + + +
+ + {/* Content Area */} + + {activeTab === 'overview' ? ( + + {/* Question Error Rate Chart */} + +

+ + 题目错误人数分布 +

+
+ + + + + + [name === 'errorRate' ? `${value}%` : value, name === 'errorRate' ? '错误率' : '错误人数']} + /> + + + +
+
+ + {/* Knowledge Point Analysis */} + +

+ + 知识点掌握薄弱项 +

+
+ + + + + + [`${value}%`, '错误率']} + /> + + + +
+
+
+ ) : ( + + {data.questions.map((q, idx) => ( + + {/* Question Header */} +
toggleQuestion(q.id)} + className="p-4 cursor-pointer flex items-start gap-4 hover:bg-gray-50/50 transition-colors" + > +
0.6 ? 'bg-red-100 text-red-600' : (q.errorRate > 0.3 ? 'bg-orange-100 text-orange-600' : 'bg-green-100 text-green-600') + }`}> + {idx + 1} +
+ +
+
+
+
+ 分值: {q.score} + 0.5 ? 'text-red-500' : 'text-gray-500'}`}> + 错误率: {Math.round(q.errorRate * 100)}% + + {expandedQuestionId === q.id ? : } +
+
+ + {/* Tags */} +
+ {q.knowledgePoints.map(kp => ( + + {kp} + + ))} +
+
+
+ + {/* Expanded Details */} + + {expandedQuestionId === q.id && ( + +
+ {/* Left Column: Stats & Answer */} +
+ {/* Stats */} +
+
+ 答题人数 + {q.totalAnswers} +
+
+ 错误人数 + {q.errorCount} +
+
+ + {/* Reference Answer */} +
+

+ + 参考答案 +

+
{q.correctAnswer || '无参考答案'}
+
+
+ + {/* Right Column: Student Errors */} +
+

+ + 错误学生列表 ({q.wrongSubmissions.length}) +

+ + {q.wrongSubmissions.length === 0 ? ( +
+ + 全班回答正确 +
+ ) : ( +
+ {q.wrongSubmissions.map((sub, i) => ( +
+
{sub.studentName}
+
+ {sub.studentAnswer || '未作答'} +
+
+ ))} +
+ )} +
+
+
+ )} +
+ + ))} + + )} + +
+ ); +}; + +// Helper Component for Stat Cards +const StatCard = ({ label, value, icon: Icon, color }: any) => { + const colorStyles = { + blue: 'bg-blue-50 text-blue-600', + green: 'bg-green-50 text-green-600', + orange: 'bg-orange-50 text-orange-600', + red: 'bg-red-50 text-red-600', + }; + + return ( +
+
+ +
+
+

{label}

+

{value}

+
+
+ ); +}; + +import { FileText } from 'lucide-react'; diff --git a/src/features/assignment/components/CreateAssignmentModal.tsx b/src/features/assignment/components/CreateAssignmentModal.tsx index 183e571..831b10a 100644 --- a/src/features/assignment/components/CreateAssignmentModal.tsx +++ b/src/features/assignment/components/CreateAssignmentModal.tsx @@ -8,12 +8,14 @@ import { examService, orgService, assignmentService } from '@/services/api'; import { useToast } from '@/components/ui/Toast'; interface CreateAssignmentModalProps { + isOpen?: boolean; onClose: () => void; onSuccess: () => void; + preSelectedExamId?: string; } -export const CreateAssignmentModal: React.FC = ({ onClose, onSuccess }) => { - const [step, setStep] = useState(1); +export const CreateAssignmentModal: React.FC = ({ onClose, onSuccess, preSelectedExamId }) => { + const [step, setStep] = useState(preSelectedExamId ? 2 : 1); const [loading, setLoading] = useState(false); const { showToast } = useToast(); @@ -29,9 +31,46 @@ export const CreateAssignmentModal: React.FC = ({ on }); useEffect(() => { - examService.getMyExams().then(res => setExams(res.items)); - orgService.getClasses().then(setClasses); - }, []); + const init = async () => { + try { + const [examsRes, classesData] = await Promise.all([ + examService.getExams({ scope: 'mine' }), // Fetch mine or all public? Maybe just mine for now or both + orgService.getClasses() + ]); + setExams(examsRes.items); + setClasses(classesData); + + if (preSelectedExamId) { + // Try to find in fetched exams, or fetch detail if not found (e.g. public exam) + const found = examsRes.items.find(e => e.id === preSelectedExamId); + if (found) { + setSelectedExam(found); + } else { + // Fetch detail if not in list + try { + const detail = await examService.getExamDetail(preSelectedExamId); + // Map detail to DTO roughly + setSelectedExam({ + id: detail.id, + title: detail.title, + subjectId: detail.subjectId, + totalScore: detail.totalScore, + duration: detail.duration, + questionCount: detail.questionCount, + status: detail.status as any, + createdAt: detail.createdAt + }); + } catch (e) { + console.error('Failed to load pre-selected exam', e); + } + } + } + } catch (e) { + console.error(e); + } + }; + init(); + }, [preSelectedExamId]); useEffect(() => { if (selectedExam && !config.title) { @@ -44,13 +83,57 @@ export const CreateAssignmentModal: React.FC = ({ on try { await assignmentService.publishAssignment({ examId: selectedExam?.id, - classIds: selectedClassIds, - ...config + classId: selectedClassIds[0], // Backend only accepts single classId currently. UI allows multiple but service logic implies one. + // If we want multiple, we need to loop here or update backend. + // Assuming for now user selects one or we just take the first one if API is singular. + // Looking at UI, it's a list of classes, so user might pick multiple. + // But realApi.ts passes 'classIds' to publishAssignment, wait... + // Let's check realApi.ts again. + // It sends `classIds: selectedClassIds`. + // But backend CreateAssignmentDto likely expects `classId` (singular) based on previous error checks? + // Actually, backend controller destructures `classId` from body. + // So if we send `classIds`, backend sees `classId` as undefined! + // We must iterate or update backend. + // For a quick fix to make it work: let's assume we loop over selected classes and call create for each, OR update backend to handle array. + // Updating backend is better but 'classId' is singular in DB assignment table usually. + // Let's loop here to be safe and support multiple classes. + + // Wait, the backend controller: const { classId ... } = req.body. + // So we must send `classId`. + + title: config.title, + startTime: config.startDate, + endTime: config.dueDate, + allowLateSubmission: true, + autoScoreEnabled: true }); + + // If multiple classes selected, we need to handle that. + // For now, let's change the logic to loop if there are multiple classes. + + for (const clsId of selectedClassIds) { + await assignmentService.publishAssignment({ + examId: selectedExam?.id, + classId: clsId, + title: config.title, + startTime: config.startDate, + endTime: config.dueDate, + allowLateSubmission: true, + autoScoreEnabled: true + }); + } + showToast('作业发布成功!', 'success'); onSuccess(); } catch (e) { - showToast('发布失败,请重试', 'error'); + console.error(e); + // Check for specific error messages + const msg = (e as any).message || '发布失败'; + if (msg.includes('Exam must be published')) { + showToast('发布失败:试卷必须先发布才能布置作业', 'error'); + } else { + showToast(`发布失败: ${msg}`, 'error'); + } } finally { setLoading(false); } diff --git a/src/features/assignment/components/EditAssignmentModal.tsx b/src/features/assignment/components/EditAssignmentModal.tsx new file mode 100644 index 0000000..cf29679 --- /dev/null +++ b/src/features/assignment/components/EditAssignmentModal.tsx @@ -0,0 +1,155 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { X, Calendar, CheckCircle2, Clock, FileText } from 'lucide-react'; +import { assignmentService } from '@/services/api'; +import { useToast } from '@/components/ui/Toast'; + +interface EditAssignmentModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; + assignment: { + id: string; + title: string; + dueDate: string; + status: string; + }; +} + +export const EditAssignmentModal: React.FC = ({ isOpen, onClose, onSuccess, assignment }) => { + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + title: '', + dueDate: '', + dueTime: '23:59' + }); + const { showToast } = useToast(); + + useEffect(() => { + if (isOpen && assignment) { + const date = new Date(assignment.dueDate); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + setFormData({ + title: assignment.title, + dueDate: `${year}-${month}-${day}`, + dueTime: `${hours}:${minutes}` + }); + } + }, [isOpen, assignment]); + + const handleSubmit = async () => { + if (!formData.title || !formData.dueDate || !formData.dueTime) { + showToast('请填写完整信息', 'error'); + return; + } + + try { + setLoading(true); + const endDateTime = new Date(`${formData.dueDate}T${formData.dueTime}`); + + await assignmentService.updateAssignment(assignment.id, { + title: formData.title, + endTime: endDateTime.toISOString() + }); + + showToast('作业信息更新成功', 'success'); + onSuccess(); + onClose(); + } catch (error: any) { + console.error(error); + showToast(error.message || '更新失败', 'error'); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+ + {/* Header */} +
+

编辑作业信息

+ +
+ + {/* Body */} +
+
+ +
+ + setFormData({...formData, title: e.target.value})} + className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all" + placeholder="请输入作业标题" + /> +
+
+ +
+ +
+
+ + setFormData({...formData, dueDate: e.target.value})} + className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all" + /> +
+
+ + setFormData({...formData, dueTime: e.target.value})} + className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all" + /> +
+
+

+ * 修改截止时间可能会影响“进行中”或“已截止”的状态判定。 +

+
+
+ + {/* Footer */} +
+ + +
+
+
+ ); +}; diff --git a/src/features/assignment/components/StudentAssignmentList.tsx b/src/features/assignment/components/StudentAssignmentList.tsx index 5c1b92d..51d00d1 100644 --- a/src/features/assignment/components/StudentAssignmentList.tsx +++ b/src/features/assignment/components/StudentAssignmentList.tsx @@ -1,10 +1,10 @@ "use client"; import React, { useState, useEffect } from 'react'; -import { AssignmentStudentViewDto } from '../../../../UI_DTO'; -import { assignmentService } from '@/services/api'; +import { AssignmentStudentViewDto, SubjectDto } from '../../../../UI_DTO'; +import { assignmentService, curriculumService } from '@/services/api'; import { Card } from '@/components/ui/Card'; -import { Clock, CheckCircle, Calendar, Play, Eye } from 'lucide-react'; +import { Clock, CheckCircle, Calendar, Play, Eye, BookOpen, User, FileText, AlertCircle, Filter } from 'lucide-react'; interface StudentAssignmentListProps { onStartExam: (id: string) => void; @@ -13,56 +13,171 @@ interface StudentAssignmentListProps { export const StudentAssignmentList: React.FC = ({ onStartExam, onViewResult }) => { const [assignments, setAssignments] = useState([]); + const [subjects, setSubjects] = useState([]); + const [filters, setFilters] = useState({ + subjectId: 'all', + examType: 'all', + status: 'all' + }); useEffect(() => { - assignmentService.getStudentAssignments().then(res => setAssignments(res.items)); + curriculumService.getSubjects().then(setSubjects); }, []); - return ( -
- {assignments.map((item, idx) => ( - -
- {item.status === 'Pending' ? : (item.status === 'Graded' ? item.score : )} -
- -
-
-

{item.title}

- - {item.status === 'Pending' ? '待完成' : (item.status === 'Graded' ? '已批改' : '已提交')} - -
-

试卷: {item.examTitle}

-
- 截止时间: {item.endTime} -
-
+ useEffect(() => { + const loadAssignments = async () => { + const res = await assignmentService.getStudentAssignments(filters); + setAssignments(res.items); + }; + loadAssignments(); + }, [filters]); -
- {item.status === 'Pending' ? ( - - ) : ( - - )} -
-
- ))} + const examTypes = ['Midterm', 'Final', 'Unit', 'Weekly', 'Uncategorized']; + const statuses = [ + { value: 'all', label: '全部状态' }, + { value: 'Pending', label: '待完成' }, + { value: 'Completed', label: '已完成' } + ]; + + return ( +
+ {/* Filters */} + +
+ + 筛选: +
+ + + + + +
+ {statuses.map(s => ( + + ))} +
+
+ + {/* List */} +
+ {assignments.length === 0 ? ( +
+ +

暂无相关作业

+
+ ) : ( + assignments.map((item, idx) => ( + +
+ {(() => { + if (item.status === 'Completed') { + return item.score !== null && item.score !== undefined ? item.score : 0; + } + if (item.status === 'Grading') { + // 如果是 Grading,检查是否真正提交了 + return item.isSubmitted ? : ; + } + if (item.status === 'Submitted') return ; + return ; + })()} +
+ +
+
+ {/* Status Tag */} +
+

{item.title}

+ + {(() => { + if (item.status === 'Pending') return '待完成'; + if (item.status === 'InProgress') return '进行中'; + if (item.status === 'Submitted') return '已提交'; + if (item.status === 'Grading') return '批改中'; + if (item.status === 'Completed') return '已完成'; + return item.status; + })()} + +
+

+ + 试卷: {item.examTitle} +

+
+ 截止: {new Date(item.endTime).toLocaleDateString()} + {item.duration && 限时: {item.duration}分钟} +
+
+ +
+ {item.subjectName && ( +
+ + 科目: {item.subjectName} +
+ )} + {item.teacherName && ( +
+ + 教师: {item.teacherName} +
+ )} + {item.questionCount !== undefined && ( +
+ + 题目: 共{item.questionCount}题 / 总分{item.totalScore} +
+ )} +
+
+ +
+ {(item.status === 'Pending' || item.status === 'InProgress') ? ( + + ) : ( + + )} +
+
+ )) + )} +
) } \ No newline at end of file diff --git a/src/features/assignment/components/TeacherAssignmentList.tsx b/src/features/assignment/components/TeacherAssignmentList.tsx index fe3ae96..3dad8c7 100644 --- a/src/features/assignment/components/TeacherAssignmentList.tsx +++ b/src/features/assignment/components/TeacherAssignmentList.tsx @@ -1,10 +1,15 @@ "use client"; import React, { useState, useEffect } from 'react'; -import { AssignmentTeacherViewDto } from '../../../../UI_DTO'; -import { assignmentService } from '@/services/api'; +import { AssignmentTeacherViewDto, ClassDto, SubjectDto } from '../../../../UI_DTO'; +import { assignmentService, orgService, curriculumService } from '@/services/api'; import { Card } from '@/components/ui/Card'; -import { Plus, FileText, Users, Calendar, Eye, BarChart3, ChevronRight } from 'lucide-react'; +import { Plus, FileText, Users, Calendar, Eye, BarChart3, ChevronRight, Filter, BookOpen, CheckCircle, Clock, PenTool, Settings, Archive } from 'lucide-react'; +import { Badge } from '@/components/ui/Badge'; +import { Button } from '@/components/ui/Button'; +import { SkeletonCard } from '@/components/ui/LoadingState'; +import { EditAssignmentModal } from './EditAssignmentModal'; +import { useToast } from '@/components/ui/Toast'; interface TeacherAssignmentListProps { onNavigateToGrading?: (id: string) => void; @@ -20,119 +25,272 @@ export const TeacherAssignmentList: React.FC = ({ setIsCreating }) => { const [assignments, setAssignments] = useState([]); + const [classes, setClasses] = useState([]); + const [subjects, setSubjects] = useState([]); + const [filters, setFilters] = useState({ + status: 'Active', // Default to 'In Progress' + classId: 'all', + examType: 'all', + subjectId: 'all' + }); + const [loading, setLoading] = useState(true); + const [editingAssignment, setEditingAssignment] = useState(null); + const { showToast } = useToast(); useEffect(() => { - assignmentService.getTeachingAssignments().then(res => setAssignments(res.items)); + // Load initial data for filters + orgService.getClasses().then(setClasses); + curriculumService.getSubjects().then(setSubjects); }, []); - const getStatusStyle = (status: string) => { + useEffect(() => { + const loadAssignments = async () => { + setLoading(true); + const res = await assignmentService.getTeachingAssignments(filters); + setAssignments(res.items); + setLoading(false); + }; + loadAssignments(); + }, [filters]); + + const getStatusVariant = (status: string, hasPending?: boolean) => { + if (hasPending) return 'warning'; // Needs Grading switch (status) { - case 'Active': return 'bg-blue-50 text-blue-600 border-blue-100'; - case 'Ended': return 'bg-gray-100 text-gray-600 border-gray-200'; - case 'Scheduled': return 'bg-orange-50 text-orange-600 border-orange-100'; - default: return 'bg-gray-50 text-gray-500'; + case 'Active': return 'info'; + case 'Ended': return 'default'; + default: return 'default'; } }; - const getStatusLabel = (status: string) => { + const getStatusLabel = (status: string, hasPending?: boolean) => { + if (hasPending) return '待批改'; switch (status) { case 'Active': return '进行中'; case 'Ended': return '已结束'; - case 'Scheduled': return '计划中'; default: return status; } } + const examTypes = ['Midterm', 'Final', 'Unit', 'Weekly', 'Uncategorized']; + const tabs = [ + { id: 'Active', label: '进行中' }, + { id: 'ToGrade', label: '待批改' }, + { id: 'Graded', label: '已结束' } + ]; + + const handleArchive = async (id: string) => { + if (!confirm('确定要归档并结束此作业吗?归档后学生将看到最终成绩。')) return; + try { + await assignmentService.archiveAssignment(id); + showToast('作业已归档', 'success'); + // Refresh list + setFilters({ ...filters }); + } catch (err: any) { + showToast(err.message || '归档失败', 'error'); + } + }; + return ( - <> -
+
+ {/* Header */} +
-

作业发布

+

作业管理

作业与测评管理

- +
-
- {assignments.map((item, idx) => { - const progress = Math.round((item.submittedCount / item.totalCount) * 100); - - return ( - -
- {progress}% -
+ {/* Filters */} + +
+ + 筛选: +
-
-
-

{item.title}

- - {getStatusLabel(item.status)} - -
-
- - 关联试卷: {item.examTitle} +
+ {tabs.map(tab => ( + + ))} +
+ + + + + + + + + {/* List */} + {loading ? ( +
+ {[1, 2, 3, 4, 5, 6].map(i => )} +
+ ) : assignments.length === 0 ? ( +
+ +

暂无相关作业

+

没有找到符合条件的作业

+ +
+ ) : ( +
+ {assignments.map((item, idx) => { + const progress = item.totalCount > 0 ? Math.round((item.submittedCount / item.totalCount) * 100) : 0; + + return ( + +
+
+
+ +
+
+ + {getStatusLabel(item.status, item.hasPendingGrading)} + +
+ + {new Date(item.createdAt).toLocaleDateString()} +
+
+
+ {item.status === 'Active' && ( + + )} + {item.status === 'Grading' && ( + + )}
+ +

+ {item.title} +

-
-
- - {item.className} +
+ + {item.className} + + {item.subjectName && ( + + {item.subjectName} + + )} + {item.examType && ( + + {item.examType} + + )} +
+ +
+
+
进度
+
{progress}%
-
- - 截止: {item.dueDate} +
+
提交
+
{item.submittedCount}/{item.totalCount}
+
+
+
截止
+
+ {new Date(item.dueDate).toLocaleDateString(undefined, {month:'numeric', day:'numeric'})} +
-
-
-
- 提交进度 - {item.submittedCount}/{item.totalCount} +
+ + +
-
-
-
-
- -
- - - -
- - ); - })} -
- + + ); + })} +
+ )} + + {editingAssignment && ( + setEditingAssignment(null)} + onSuccess={() => { + // Refresh list + setFilters({ ...filters }); + setEditingAssignment(null); + }} + assignment={{ + id: editingAssignment.id, + title: editingAssignment.title, + dueDate: editingAssignment.dueDate, + status: editingAssignment.status + }} + /> + )} +
); -} \ No newline at end of file +} diff --git a/src/features/auth/components/LoginForm.tsx b/src/features/auth/components/LoginForm.tsx index f30b054..325a606 100644 --- a/src/features/auth/components/LoginForm.tsx +++ b/src/features/auth/components/LoginForm.tsx @@ -10,7 +10,7 @@ interface LoginFormProps { } export const LoginForm: React.FC = ({ onLoginSuccess, onSwitch }) => { - const [username, setUsername] = useState('admin'); + const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); @@ -50,7 +50,7 @@ export const LoginForm: React.FC = ({ onLoginSuccess, onSwitch } value={username} onChange={(e) => setUsername(e.target.value)} className="w-full px-4 py-3.5 rounded-xl bg-gray-50/50 border border-gray-200 focus:bg-white focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 outline-none transition-all text-gray-900 font-medium placeholder:text-gray-400" - placeholder="请输入学号或工号" + placeholder="请输入邮箱或手机号" />
@@ -95,6 +95,23 @@ export const LoginForm: React.FC = ({ onLoginSuccess, onSwitch } +
+ + +
+

忘记密码? diff --git a/src/features/dashboard/components/StudentDashboard.tsx b/src/features/dashboard/components/StudentDashboard.tsx index 4ef1de3..6d99b8f 100644 --- a/src/features/dashboard/components/StudentDashboard.tsx +++ b/src/features/dashboard/components/StudentDashboard.tsx @@ -28,6 +28,7 @@ interface StudentDashboardProps { export const StudentDashboard = ({ user, onNavigate }: StudentDashboardProps) => { const [performanceData, setPerformanceData] = useState(null); const [radarData, setRadarData] = useState(null); + const [stats, setStats] = useState<{ completed: number; todo: number; average: number; studyDuration: number } | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -39,7 +40,7 @@ export const StudentDashboard = ({ user, onNavigate }: StudentDashboardProps) => setLoading(true); setError(null); - const [growth, radar] = await Promise.all([ + const [growth, radar, studentStats] = await Promise.all([ analyticsService.getStudentGrowth().catch(err => { console.error('Failed to load growth:', err); return null; @@ -47,11 +48,16 @@ export const StudentDashboard = ({ user, onNavigate }: StudentDashboardProps) => analyticsService.getStudentRadar().catch(err => { console.error('Failed to load radar:', err); return null; + }), + analyticsService.getStudentStats().catch(err => { + console.error('Failed to load student stats:', err); + return null; }) ]); setPerformanceData(growth); setRadarData(radar); + setStats(studentStats); } catch (err) { console.error('Failed to load dashboard data:', err); const errorMessage = getErrorMessage(err); @@ -113,13 +119,13 @@ export const StudentDashboard = ({ user, onNavigate }: StudentDashboardProps) => return (

onNavigate('assignments')} className="cursor-pointer"> - +
onNavigate('assignments')} className="cursor-pointer"> - +
- - + +
diff --git a/src/features/exam/components/ExamEditor.tsx b/src/features/exam/components/ExamEditor.tsx index 7f8fbeb..be7deb7 100644 --- a/src/features/exam/components/ExamEditor.tsx +++ b/src/features/exam/components/ExamEditor.tsx @@ -2,8 +2,8 @@ "use client"; import React, { useState, useEffect } from 'react'; -import { ExamDetailDto, ExamNodeDto, QuestionSummaryDto, ParsedQuestionDto } from '../../../../UI_DTO'; -import { examService, questionService } from '@/services/api'; +import { ExamDetailDto, ExamNodeDto, QuestionSummaryDto, ParsedQuestionDto, SubjectDto } from '../../../../UI_DTO'; +import { examService, questionService, curriculumService } from '@/services/api'; import { useToast } from '@/components/ui/Toast'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; @@ -11,7 +11,7 @@ import { ImportModal } from './ImportModal'; import { ArrowLeft, Save, Plus, Trash2, GripVertical, ChevronDown, ChevronUp, FileInput, Search, Filter, - Clock, Hash, Calculator, FolderPlus, FileText + Clock, Hash, Calculator, FolderPlus, FileText, BookOpen, Send } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; @@ -22,6 +22,7 @@ interface ExamEditorProps { export const ExamEditor: React.FC = ({ examId, onBack }) => { const [exam, setExam] = useState(null); + const [subjects, setSubjects] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [expandedNodes, setExpandedNodes] = useState>(new Set()); @@ -33,52 +34,63 @@ export const ExamEditor: React.FC = ({ examId, onBack }) => { // Init useEffect(() => { const init = async () => { - if (examId) { - const data = await examService.getExamDetail(examId); - setExam(data); - // Auto expand all group nodes - const allGroupIds = new Set(); - const collectGroupIds = (nodes: ExamNodeDto[]) => { - nodes.forEach(node => { - if (node.nodeType === 'Group') { - allGroupIds.add(node.id); - if (node.children) collectGroupIds(node.children); - } - }); - }; - collectGroupIds(data.rootNodes); - setExpandedNodes(allGroupIds); - } else { - // Create new template - const newExam: ExamDetailDto = { - id: '', - subjectId: 'sub-1', - title: '未命名试卷', - totalScore: 0, - duration: 120, - questionCount: 0, - status: 'Draft', - createdAt: new Date().toISOString().split('T')[0], - rootNodes: [ - { - id: 'node-1', - nodeType: 'Group', - title: '第一部分:选择题', - description: '请选出正确答案', - score: 0, - sortOrder: 1, - children: [] - } - ] - }; - setExam(newExam); - setExpandedNodes(new Set(['node-1'])); - setSelectedNodeId('node-1'); - } + try { + const [questions, subjectsData] = await Promise.all([ + questionService.search({}), + curriculumService.getSubjects() + ]); + + setQuestionBank(questions.items); + setSubjects(subjectsData); - const questions = await questionService.search({}); - setQuestionBank(questions.items); - setLoading(false); + if (examId) { + const data = await examService.getExamDetail(examId); + setExam(data); + // Auto expand all group nodes + const allGroupIds = new Set(); + const collectGroupIds = (nodes: ExamNodeDto[]) => { + nodes.forEach(node => { + if (node.nodeType === 'Group') { + allGroupIds.add(node.id); + if (node.children) collectGroupIds(node.children); + } + }); + }; + collectGroupIds(data.rootNodes); + setExpandedNodes(allGroupIds); + } else { + // Create new template + const newExam: ExamDetailDto = { + id: '', + subjectId: subjectsData.length > 0 ? subjectsData[0].id : '', + title: '未命名试卷', + totalScore: 0, + duration: 120, + questionCount: 0, + status: 'Draft', + createdAt: new Date().toISOString().split('T')[0], + rootNodes: [ + { + id: 'node-1', + nodeType: 'Group', + title: '第一部分:选择题', + description: '请选出正确答案', + score: 0, + sortOrder: 1, + children: [] + } + ] + }; + setExam(newExam); + setExpandedNodes(new Set(['node-1'])); + setSelectedNodeId('node-1'); + } + } catch (err) { + console.error(err); + showToast('Failed to load initial data', 'error'); + } finally { + setLoading(false); + } }; init(); }, [examId]); @@ -106,14 +118,34 @@ export const ExamEditor: React.FC = ({ examId, onBack }) => { } }, [exam?.rootNodes]); - const handleSave = async () => { + const handleSave = async (status: 'Draft' | 'Published' = 'Draft') => { if (!exam) return; + + if (!exam.subjectId) { + showToast('请选择所属学科', 'error'); + return; + } + + if (!exam.title.trim()) { + showToast('请输入试卷标题', 'error'); + return; + } + + if (status === 'Published' && exam.questionCount === 0) { + showToast('无法发布空试卷,请先添加题目', 'error'); + return; + } + setSaving(true); try { - await examService.saveExam(exam); - showToast('试卷保存成功', 'success'); + await examService.saveExam({ + ...exam, + status + }); + showToast(status === 'Published' ? '试卷已发布' : '草稿已保存', 'success'); if (!examId) onBack(); } catch (e) { + console.error(e); showToast('保存失败', 'error'); } finally { setSaving(false); @@ -415,41 +447,62 @@ export const ExamEditor: React.FC = ({ examId, onBack }) => {
- - -
+ + + +
@@ -460,7 +513,7 @@ export const ExamEditor: React.FC = ({ examId, onBack }) => { {exam.rootNodes.length === 0 ? (
-

试卷为空,点击右上角"添加分组"开始构建试卷

+

试卷为空,点击右上角 "添加分组" 开始构建试卷

) : (
diff --git a/src/features/exam/components/ExamList.tsx b/src/features/exam/components/ExamList.tsx index 25b8361..af110cf 100644 --- a/src/features/exam/components/ExamList.tsx +++ b/src/features/exam/components/ExamList.tsx @@ -2,125 +2,336 @@ "use client"; import React, { useEffect, useState } from 'react'; -import { ExamDto } from '../../../../UI_DTO'; -import { examService } from '@/services/api'; +import { ExamDto, SubjectDto } from '../../../../UI_DTO'; +import { examService, curriculumService } from '@/services/api'; import { Card } from '@/components/ui/Card'; -import { Search, Plus, FileText, Clock, BarChart3, PenTool } from 'lucide-react'; +import { Search, Plus, FileText, Clock, BarChart3, PenTool, Trash2, Send, User, Globe, Filter, ChevronLeft, ChevronRight, Users, Calendar } from 'lucide-react'; import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; import { LoadingState, SkeletonCard } from '@/components/ui/LoadingState'; import { ErrorState } from '@/components/ui/ErrorState'; import { getErrorMessage, getErrorType } from '@/utils/errorUtils'; import { useToast } from '@/components/ui/Toast'; +import { CreateAssignmentModal } from '@/features/assignment/components/CreateAssignmentModal'; -export const ExamList = ({ onEdit, onCreate, onStats }: { onEdit: (id: string) => void, onCreate: () => void, onStats: (id: string) => void }) => { +interface ExamListProps { + onEditExam: (id: string) => void; + onCreateExam: () => void; +} + +export const ExamList: React.FC = ({ onEditExam, onCreateExam }) => { const [exams, setExams] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [scope, setScope] = useState<'mine' | 'public'>('mine'); + const [publishExamId, setPublishExamId] = useState(null); + + const [subjects, setSubjects] = useState([]); + const [filters, setFilters] = useState({ + subjectId: 'all', + examType: 'all', + status: 'all' + }); + const [page, setPage] = useState(1); + const [totalCount, setTotalCount] = useState(0); + const pageSize = 20; + const { showToast } = useToast(); useEffect(() => { - const loadExams = async () => { - try { - setLoading(true); - setError(null); - const res = await examService.getMyExams(); - setExams(res.items); - } catch (err) { - console.error('Failed to load exams:', err); - const errorMessage = getErrorMessage(err); - setError(errorMessage); - showToast(errorMessage, 'error'); - } finally { - setLoading(false); - } - }; - loadExams(); + curriculumService.getSubjects().then(setSubjects); }, []); - const getStatusVariant = (status: string) => status === 'Published' ? 'success' : 'warning'; - const getStatusLabel = (status: string) => status === 'Published' ? '已发布' : '草稿'; + const fetchExams = async () => { + try { + setLoading(true); + setError(null); + const res = await examService.getExams({ + scope, + page, + pageSize, + subjectId: filters.subjectId !== 'all' ? filters.subjectId : undefined, + examType: filters.examType !== 'all' ? filters.examType : undefined, + status: filters.status !== 'all' ? filters.status : undefined + }); + setExams(res.items); + setTotalCount(res.totalCount); + } catch (err) { + console.error(err); + const msg = getErrorMessage(err); + setError(msg); + showToast(msg, 'error'); + } finally { + setLoading(false); + } + }; - if (loading) { - return ( -
-
-
-

考试引擎

-

创建、管理及发布考试

-
-
-
- - - -
-
- ); - } + useEffect(() => { + fetchExams(); + }, [scope, filters, page]); + + // Reset page when filters change + useEffect(() => { + setPage(1); + }, [scope, filters]); + + const handleDelete = async (id: string) => { + if (!confirm('确定要删除该试卷吗?此操作不可恢复。')) return; + try { + await examService.deleteExam(id); + setExams(prev => prev.filter(e => e.id !== id)); + setTotalCount(prev => prev - 1); + showToast('删除成功', 'success'); + } catch (e) { + console.error('Failed to delete exam:', e); + const msg = getErrorMessage(e); + showToast(`删除失败: ${msg}`, 'error'); + } + }; + + const handlePublishSuccess = () => { + showToast('作业发布成功', 'success'); + setPublishExamId(null); + }; + + const examTypes = ['Midterm', 'Final', 'Unit', 'Weekly', 'Uncategorized']; + const statuses = [ + { value: 'all', label: '全部状态' }, + { value: 'Published', label: '已发布' }, + { value: 'Draft', label: '草稿' } + ]; + + const totalPages = Math.ceil(totalCount / pageSize); if (error) { - return ( - window.location.reload()} - /> - ); + return ; } return (
-
-
-

考试引擎

-

创建、管理及发布考试

+ {/* Header & Scope Toggle */} +
+
+ +
+
-
- - -
- +
-
- {exams.map((exam, idx) => ( - -
- -
-
-
- {getStatusLabel(exam.status)} - 创建于: {exam.createdAt} -
-

{exam.title}

-
-
- - {exam.duration} 分钟 + {/* Filters */} + +
+ + 筛选: +
+ + + + + +
+ {statuses.map(s => ( + + ))} +
+
+ + {/* List */} + {loading ? ( +
+ {[1, 2, 3].map(i => )} +
+ ) : exams.length === 0 ? ( +
+ +

暂无试卷

+

没有找到符合条件的试卷

+ +
+ ) : ( + <> +
+ {exams.map((exam, idx) => ( + +
+
+
+ +
+
+ + {exam.status === 'Published' ? '已发布' : '草稿'} + +
+ + {new Date(exam.createdAt).toLocaleDateString()} +
+
+
-
- {exam.questionCount} 题 + +

+ {exam.title} +

+ +
+ {scope === 'public' && exam.creatorName && ( + + {exam.creatorName} + + )} + {/* Subject Name logic would require mapping subjectId to name, simpler to just show type/duration for now or fetch subject map */}
-
- 总分: {exam.totalScore} + +
+
+
时长
+
{exam.duration}m
+
+
+
总分
+
{exam.totalScore}
+
+
+
使用
+
{exam.usageCount || 0}
+
-
+ +
+ + + {exam.status === 'Draft' ? ( + + ) : ( + + )} + + {scope === 'mine' && ( + + )} +
+ + ))} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + + 第 {page} / {totalPages} 页 + +
-
- - -
-
- ))} -
+ )} + + )} + + {publishExamId && ( + setPublishExamId(null)} + preSelectedExamId={publishExamId} + onSuccess={handlePublishSuccess} + /> + )}
); }; diff --git a/src/lib/auth-context.tsx b/src/lib/auth-context.tsx index 32fb29f..c060287 100644 --- a/src/lib/auth-context.tsx +++ b/src/lib/auth-context.tsx @@ -2,7 +2,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { UserProfileDto } from '../../UI_DTO'; -import { authService, subscribeApiMode } from '@/services/api'; +import { authService } from '@/services/api'; import { useRouter, usePathname } from 'next/navigation'; interface AuthContextType { @@ -35,14 +35,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } }; - // Re-check auth when API mode changes (Strategy Pattern hook) - useEffect(() => { - return subscribeApiMode(() => { - setLoading(true); - checkAuth(); - }); - }, []); - useEffect(() => { checkAuth(); }, []); diff --git a/src/lib/db.ts b/src/lib/db.ts deleted file mode 100644 index 90cfee4..0000000 --- a/src/lib/db.ts +++ /dev/null @@ -1,35 +0,0 @@ - -// This file would typically use mysql2/promise -// import mysql from 'mysql2/promise'; - -// Mock DB Configuration storage (In-memory for demo, use env vars in prod) -let dbConfig = { - host: 'localhost', - port: 3306, - user: 'root', - password: '', - database: 'edunexus' -}; - -// Mock Connection Pool -export const db = { - query: async (sql: string, params?: any[]) => { - // In a real app: - // const connection = await mysql.createConnection(dbConfig); - // const [rows] = await connection.execute(sql, params); - // return rows; - - console.log(`[MockDB] Executing SQL: ${sql}`, params); - return []; - }, - testConnection: async (config: typeof dbConfig) => { - // Simulate connection attempt - await new Promise(resolve => setTimeout(resolve, 1000)); - if (config.host === 'error') throw new Error('Connection timed out'); - - // Update active config - dbConfig = config; - return true; - }, - getConfig: () => dbConfig -}; diff --git a/src/lib/server-utils.ts b/src/lib/server-utils.ts deleted file mode 100644 index abddc89..0000000 --- a/src/lib/server-utils.ts +++ /dev/null @@ -1,24 +0,0 @@ - -import { NextResponse } from 'next/server'; - -// Simulate database latency -export const dbDelay = () => new Promise(resolve => setTimeout(resolve, 500)); - -// Standardize JSON success response -export function successResponse(data: any, status = 200) { - return NextResponse.json(data, { status }); -} - -// Standardize JSON error response -export function errorResponse(message: string, status = 400) { - return NextResponse.json({ success: false, message }, { status }); -} - -// Helper to extract token from header -export function extractToken(request: Request): string | null { - const authHeader = request.headers.get('authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return null; - } - return authHeader.split(' ')[1]; -} diff --git a/src/services/api.ts b/src/services/api.ts index 8c64984..27db014 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,19 +1,6 @@ import * as realApi from './realApi'; -// API Mode Management (Deprecated: Mock mode is permanently disabled) -export const getApiMode = () => false; - -export const setApiMode = (isMock: boolean) => { - console.warn("[API] Mock mode is disabled. Always using Real API."); -}; - -export const subscribeApiMode = (listener: () => void) => { - // No-op as mode never changes - return () => {}; -}; - -// Export Real Services Directly export const authService = realApi.realAuthService; export const orgService = realApi.realOrgService; export const curriculumService = realApi.realCurriculumService; diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 5194b97..ab23042 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -6,7 +6,7 @@ import { TextbookDto, QuestionSummaryDto, PagedResult, ParsedQuestionDto, ExamDto, ExamDetailDto, ExamStatsDto, - AssignmentTeacherViewDto, AssignmentStudentViewDto, + AssignmentTeacherViewDto, AssignmentStudentViewDto, AssignmentAnalysisDto, StudentSubmissionSummaryDto, GradingPaperDto, StudentExamPaperDto, SubmitExamDto, StudentResultDto, ChartDataDto, RadarChartDto, ScoreDistributionDto, ScheduleDto, CreateScheduleDto, MessageDto, CreateMessageDto @@ -60,17 +60,23 @@ export interface IQuestionService { } export interface IExamService { - getMyExams(): Promise>; + getExams(filter?: { subjectId?: string; status?: string; scope?: 'mine' | 'public'; page?: number; pageSize?: number; examType?: string }): Promise>; getExamDetail(id: string): Promise; - saveExam(exam: ExamDetailDto): Promise; + createExam(data: any): Promise; + updateExam(id: string, data: any): Promise; + deleteExam(id: string): Promise; + saveExam(data: ExamDetailDto): Promise; getStats(id: string): Promise; } export interface IAssignmentService { - getTeachingAssignments(): Promise>; - getStudentAssignments(): Promise>; + getTeachingAssignments(filters?: { classId?: string; examType?: string; subjectId?: string; status?: string }): Promise>; + getStudentAssignments(filters?: { subjectId?: string; examType?: string; status?: string }): Promise>; publishAssignment(data: any): Promise; + updateAssignment(id: string, data: any): Promise; + archiveAssignment(id: string): Promise; getAssignmentStats(id: string): Promise; + getAssignmentAnalysis(id: string): Promise; } export interface IAnalyticsService { @@ -80,6 +86,7 @@ export interface IAnalyticsService { getStudentRadar(): Promise; getScoreDistribution(): Promise; getTeacherStats(): Promise<{ activeStudents: number; averageScore: number; pendingGrading: number; passRate: number }>; + getStudentStats(): Promise<{ completed: number; todo: number; average: number; studyDuration: number }>; } export interface QuestionFilterDto { @@ -112,8 +119,10 @@ export interface IGradingService { export interface ISubmissionService { getStudentPaper(assignmentId: string): Promise; - submitExam(data: SubmitExamDto): Promise; - getSubmissionResult(assignmentId: string): Promise; + submitAnswers(assignmentId: string, answers: Array<{ examNodeId: string; studentAnswer: any }>, timeSpent?: number): Promise<{ message: string; submissionId: string }>; + saveProgress(assignmentId: string, answers: Array<{ examNodeId: string; studentAnswer: any }>): Promise; + getSubmissionResult(submissionId: string): Promise; + getSubmissionResultByAssignment(assignmentId: string): Promise; } export interface ICommonService { diff --git a/src/services/mockApi.ts b/src/services/mockApi.ts deleted file mode 100644 index d8d5ee3..0000000 --- a/src/services/mockApi.ts +++ /dev/null @@ -1,696 +0,0 @@ - -import { - IAuthService, IOrgService, ICurriculumService, IQuestionService, - IExamService, IAssignmentService, IAnalyticsService, IGradingService, - ISubmissionService, ICommonService, IMessageService, IScheduleService -} from './interfaces'; -import { - LoginResultDto, UserProfileDto, RegisterDto, UpdateProfileDto, ChangePasswordDto, - ClassDto, CreateClassDto, ClassMemberDto, SubjectDto, CurriculumTreeDto, - UnitNodeDto, - TextbookDto, - QuestionSummaryDto, ParsedQuestionDto, PagedResult, ExamDto, ExamDetailDto, ExamStatsDto, - ExamNodeDto, AssignmentTeacherViewDto, AssignmentStudentViewDto, - StudentSubmissionSummaryDto, GradingPaperDto, StudentExamPaperDto, SubmitExamDto, StudentResultDto, - ChartDataDto, RadarChartDto, ScoreDistributionDto, ScheduleDto, CreateScheduleDto, - MessageDto, CreateMessageDto -} from '../../UI_DTO'; - -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -// --- Stateful Mock Data --- -let MOCK_CLASSES: ClassDto[] = [ - { id: 'c-1', name: '高一 (10) 班', gradeName: '高一年级', teacherName: '李明', studentCount: 32, inviteCode: 'X7K9P' }, - { id: 'c-2', name: '高一 (12) 班', gradeName: '高一年级', teacherName: '张伟', studentCount: 28, inviteCode: 'M2L4Q' }, - { id: 'c-3', name: 'AP 微积分先修班', gradeName: '高三年级', teacherName: '李明', studentCount: 15, inviteCode: 'Z9J1W' }, - { id: 'c-4', name: '物理奥赛集训队', gradeName: '高二年级', teacherName: '王博士', studentCount: 20, inviteCode: 'H4R8T' }, -]; - -let MOCK_STUDENT_CLASSES: ClassDto[] = [ - { id: 'c-1', name: '高一 (10) 班', gradeName: '高一年级', teacherName: '李明', studentCount: 32, inviteCode: 'X7K9P' } -]; - -let MOCK_MESSAGES: MessageDto[] = [ - { - id: 'msg-1', - title: '关于下周校运会的安排通知', - content: '各位同学、老师:\n\n下周一(11月6日)将举行第20届秋季运动会,请各班做好入场式准备。周一至周二停课两天,周三正常上课。\n\n教务处', - type: 'Announcement', - senderName: '教务处', - senderAvatar: 'https://api.dicebear.com/7.x/initials/svg?seed=AO', - createdAt: '2小时前', - isRead: false - }, - { - id: 'msg-2', - title: '数学期中考试成绩已发布', - content: '高一年级数学期中考试阅卷工作已结束,请各位同学前往“考试结果”查看详情。', - type: 'Notification', - senderName: '李明', - senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex', - createdAt: '昨天 14:00', - isRead: true - }, - { - id: 'msg-3', - title: '系统维护通知', - content: '系统将于本周日凌晨 02:00 - 04:00 进行例行维护,届时将无法访问,请留意。', - type: 'Alert', - senderName: '系统管理员', - createdAt: '2023-10-28', - isRead: true - } -]; - -let MOCK_SCHEDULE: ScheduleDto[] = [ - { id: 'sch-1', dayOfWeek: 1, period: 1, startTime: '08:00', endTime: '08:45', className: '高一 (10) 班', subject: '数学', room: 'A301', isToday: false }, - { id: 'sch-2', dayOfWeek: 1, period: 2, startTime: '09:00', endTime: '09:45', className: '高一 (12) 班', subject: '数学', room: 'A303', isToday: false }, - { id: 'sch-3', dayOfWeek: 2, period: 1, startTime: '08:00', endTime: '08:45', className: '高一 (10) 班', subject: '数学', room: 'A301', isToday: false }, - { id: 'sch-4', dayOfWeek: 2, period: 3, startTime: '10:00', endTime: '10:45', className: 'AP 微积分', subject: '微积分', room: 'B102', isToday: false }, - { id: 'sch-5', dayOfWeek: 3, period: 1, startTime: '08:00', endTime: '08:45', className: '高一 (10) 班', subject: '数学', room: 'A301', isToday: false }, - { id: 'sch-6', dayOfWeek: 3, period: 2, startTime: '09:00', endTime: '09:45', className: '高一 (12) 班', subject: '数学', room: 'A303', isToday: false }, - { id: 'sch-7', dayOfWeek: 4, period: 1, startTime: '08:00', endTime: '08:45', className: '高一 (10) 班', subject: '数学', room: 'A301', isToday: false }, - { id: 'sch-8', dayOfWeek: 5, period: 4, startTime: '11:00', endTime: '11:45', className: '奥赛集训', subject: '物理', room: 'Lab 1', isToday: false }, -]; - -export const mockAuthService: IAuthService = { - login: async (username: string): Promise => { - await delay(800); - - let role: 'Teacher' | 'Student' | 'Admin' = 'Teacher'; - let name = '李明'; - let id = 'u-tea-1'; - - if (username === 'student' || username === '123456' && username.startsWith('s')) { - role = 'Student'; - name = '王小明'; - id = 'u-stu-1'; - } else if (username === 'admin') { - role = 'Admin'; - name = '系统管理员'; - id = 'u-adm-1'; - } - - return { - token: "mock-jwt-token-12345", - user: { - id: id, - realName: name, - studentId: username, - avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${name}`, - gender: 'Male', - schoolId: 's-1', - role: role, - email: 'liming@school.edu', - phone: '13800138000', - bio: '热爱教育,专注数学教学创新。' - } - }; - }, - register: async (data: RegisterDto): Promise => { - await delay(1200); - return { - token: "mock-jwt-token-new-user", - user: { - id: `u-${Date.now()}`, - realName: data.realName, - studentId: data.studentId, - avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${data.realName}`, - gender: 'Male', - schoolId: 's-1', - role: data.role, - email: '', - phone: '', - bio: '新注册用户' - } - }; - }, - me: async (): Promise => { - await delay(500); - return { - id: "u-1", - realName: "李明", - studentId: "T2024001", - avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alex", - gender: "Male", - schoolId: "s-1", - role: "Teacher", - email: 'liming@school.edu', - phone: '13800138000', - bio: '热爱教育,专注数学教学创新。' - }; - }, - updateProfile: async (data: UpdateProfileDto): Promise => { - await delay(1000); - return { - id: "u-1", - realName: data.realName || "李明", - studentId: "T2024001", - avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alex", - gender: "Male", - schoolId: "s-1", - role: "Teacher", - email: data.email || 'liming@school.edu', - phone: data.phone || '13800138000', - bio: data.bio || '热爱教育,专注数学教学创新。' - }; - }, - changePassword: async (data: ChangePasswordDto): Promise => { - await delay(1200); - if (data.oldPassword !== '123456') { - throw new Error('旧密码错误'); - } - } -}; - -export const mockOrgService: IOrgService = { - getClasses: async (role?: string): Promise => { - await delay(600); - if (role === 'Student') { - return [...MOCK_STUDENT_CLASSES]; - } - return [...MOCK_CLASSES]; - }, - getClassMembers: async (classId: string): Promise => { - await delay(600); - return Array.from({ length: 32 }).map((_, i) => ({ - id: `stu-${i}`, - studentId: `2024${1000 + i}`, - realName: i % 2 === 0 ? `张${i + 1}` : `王${i + 1}`, - avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${classId}-stu-${i}`, - gender: Math.random() > 0.5 ? 'Male' : 'Female', - role: i === 0 ? 'Monitor' : (i < 5 ? 'Committee' : 'Student'), - recentTrend: [ - Math.floor(Math.random() * 20) + 80, - Math.floor(Math.random() * 20) + 80, - Math.floor(Math.random() * 20) + 80, - Math.floor(Math.random() * 20) + 80, - Math.floor(Math.random() * 20) + 80, - ], - status: i > 28 ? 'AtRisk' : (i < 5 ? 'Excellent' : 'Active'), - attendanceRate: i > 30 ? 85 : 98 - })); - }, - joinClass: async (inviteCode: string): Promise => { - await delay(1500); - const targetClass = MOCK_CLASSES.find(c => c.inviteCode === inviteCode); - if (!targetClass) throw new Error('无效的邀请码'); - - const alreadyJoined = MOCK_STUDENT_CLASSES.find(c => c.id === targetClass.id); - if (alreadyJoined) throw new Error('你已经加入了该班级'); - - MOCK_STUDENT_CLASSES.push(targetClass); - }, - createClass: async (data: CreateClassDto): Promise => { - await delay(1000); - const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; - let code = ''; - for (let i = 0; i < 5; i++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - const newClass: ClassDto = { - id: `c-new-${Date.now()}`, - name: data.name, - gradeName: data.gradeName, - teacherName: '李明', - studentCount: 0, - inviteCode: code - }; - - MOCK_CLASSES.push(newClass); - return newClass; - } -}; - -export const mockCurriculumService: ICurriculumService = { - getSubjects: async (): Promise => { - await delay(400); - return [ - { id: 'sub-1', name: '数学', code: 'MATH', icon: '📐' }, - { id: 'sub-2', name: '物理', code: 'PHYS', icon: '⚡' }, - { id: 'sub-3', name: '英语', code: 'ENG', icon: '🔤' }, - { id: 'sub-4', name: '化学', code: 'CHEM', icon: '🧪' }, - { id: 'sub-5', name: '历史', code: 'HIST', icon: '🏛️' }, - ]; - }, - getTree: async (id: string): Promise => { - await delay(600); - return { - textbook: { id: 'tb-1', name: '七年级数学上册', publisher: '人教版', versionYear: '2023', coverUrl: '' }, - units: [] - }; - }, - getTextbooksBySubject: async (subjectId: string): Promise => { - await delay(400); - return [ - { id: 'tb-1', name: '七年级数学上册', publisher: '人教版', versionYear: '2023', coverUrl: '' }, - { id: 'tb-2', name: '七年级数学下册', publisher: '人教版', versionYear: '2023', coverUrl: '' } - ]; - }, - // Stubs for CRUD - createTextbook: async () => { }, - updateTextbook: async () => { }, - deleteTextbook: async () => { }, - createUnit: async () => { }, - updateUnit: async () => { }, - deleteUnit: async () => { }, - createLesson: async () => { }, - updateLesson: async () => { }, - deleteLesson: async () => { }, - createKnowledgePoint: async () => { }, - updateKnowledgePoint: async () => { }, - deleteKnowledgePoint: async () => { } -}; - -export const mockQuestionService: IQuestionService = { - search: async (filter: any): Promise> => { - await delay(600); - const mockQuestions = [ - { - id: 'q-math-1', - type: '单选题', - difficulty: 2, - knowledgePoints: ['集合', '交集运算'], - content: `

已知集合 A = {1, 2, 3}, B = {2, 3, 4}, 则 AB = ( )

-
-
A. {1}
-
B. {2, 3}
-
C. {1, 2, 3, 4}
-
D. ∅
-
`, - answer: 'B', - parse: '集合 A 与 B 的公共元素为 2 和 3,故 A ∩ B = {2, 3}。' - }, - { - id: 'q-math-2', - type: '填空题', - difficulty: 3, - knowledgePoints: ['函数', '导数'], - content: `

函数 f(x) = xlnx 在点 x = 1 处的切线方程为 ______.

`, - answer: 'x - y - 1 = 0', - parse: 'f\'(x) = lnx + 1, f\'(1) = 1. 又 f(1)=0, 故切线方程为 y-0 = 1*(x-1), 即 x-y-1=0.' - } - ]; - const items = [...mockQuestions, ...mockQuestions]; - return { - totalCount: items.length, - pageIndex: 1, - pageSize: 10, - items: items.map((q, i) => ({ ...q, id: `${q.id}-${i}` })) - }; - }, - parseText: async (rawText: string): Promise => { - await delay(1200); - const parsedQuestions: ParsedQuestionDto[] = []; - const questionBlocks = rawText.split(/\n(?=\d+\.)/g).filter(b => b.trim().length > 0); - - questionBlocks.forEach(block => { - const stemMatch = block.match(/^\d+\.([\s\S]*?)(?=(?:A\.|Answer:|答案:|解析:|$))/); - let content = stemMatch ? stemMatch[1].trim() : block; - const optionsMatch = block.match(/([A-D])\.\s*([^\n]+)/g); - let type = '填空题'; - let optionsHTML = ''; - - if (optionsMatch && optionsMatch.length >= 4) { - type = '单选题'; - optionsHTML = `
- ${optionsMatch.map(opt => `
${opt.trim()}
`).join('')} -
`; - } - const answerMatch = block.match(/(?:Answer|答案)[::]\s*([^\n]+)/); - const answer = answerMatch ? answerMatch[1].trim() : ''; - const parseMatch = block.match(/(?:Parse|解析|Analysis)[::]\s*([\s\S]+)/); - const parse = parseMatch ? parseMatch[1].trim() : '暂无解析'; - content = `

${content}

${optionsHTML}`; - parsedQuestions.push({ content, type, answer, parse }); - }); - return parsedQuestions; - } - , - create: async (data: any): Promise => { - await delay(300); - return { id: `q-${Date.now()}` }; - }, - update: async (id: string, data: any): Promise => { - await delay(300); - return { id }; - }, - delete: async (id: string): Promise => { - await delay(300); - return { id }; - } -}; - -export const mockExamService: IExamService = { - getMyExams: async (): Promise> => { - await delay(700); - return { - totalCount: 5, - pageIndex: 1, - pageSize: 10, - items: [ - { id: 'e-1', subjectId: 'sub-1', title: '2024-2025学年第一学期期中数学考试', totalScore: 100, duration: 120, questionCount: 22, status: 'Published', createdAt: '2024-10-15' }, - { id: 'e-2', subjectId: 'sub-1', title: '第一单元随堂测试:集合与函数', totalScore: 25, duration: 30, questionCount: 8, status: 'Draft', createdAt: '2024-10-20' }, - ] - }; - }, - getExamDetail: async (id: string): Promise => { - await delay(1000); - return { - id, - subjectId: 'sub-1', - title: '2024-2025学年第一学期期中数学考试', - totalScore: 100, - duration: 120, - questionCount: 10, - status: 'Draft', - createdAt: '2024-10-15', - rootNodes: [ - { - id: 'node-1', - nodeType: 'Group', - title: '第一部分:选择题', - description: '本大题共 8 小题,每小题 5 分,共 40 分。', - score: 40, - sortOrder: 1, - children: [ - { - id: 'node-1-1', - nodeType: 'Question', - questionId: 'q-1', - questionContent: '已知集合 A={1,2}, B={2,3}, 则 A∩B=?', - questionType: '单选题', - score: 5, - sortOrder: 1 - }, - { - id: 'node-1-2', - nodeType: 'Question', - questionId: 'q-2', - questionContent: '函数 f(x) = x² + 2x - 3 的零点是?', - questionType: '单选题', - score: 5, - sortOrder: 2 - } - ] - }, - { - id: 'node-2', - nodeType: 'Group', - title: '第二部分:解答题', - description: '需要写出完整解题过程', - score: 60, - sortOrder: 2, - children: [ - { - id: 'node-2-1', - nodeType: 'Group', - title: '(一) 计算题', - score: 30, - sortOrder: 1, - children: [ - { - id: 'node-2-1-1', - nodeType: 'Question', - questionId: 'q-3', - questionContent: '计算:(1) 2x + 3 = 7', - questionType: '计算题', - score: 10, - sortOrder: 1 - }, - { - id: 'node-2-1-2', - nodeType: 'Question', - questionId: 'q-4', - questionContent: '计算:(2) 解方程组 ...', - questionType: '计算题', - score: 10, - sortOrder: 2 - } - ] - }, - { - id: 'node-2-2', - nodeType: 'Question', - questionId: 'q-5', - questionContent: '证明:等腰三角形两底角相等', - questionType: '证明题', - score: 30, - sortOrder: 2 - } - ] - } - ] - }; - }, - saveExam: async (exam: ExamDetailDto): Promise => { - await delay(800); - console.log('Saved exam:', exam); - }, - getStats: async (id: string): Promise => { - await delay(800); - return { - averageScore: 78.5, - passRate: 92.4, - maxScore: 100, - minScore: 42, - scoreDistribution: [{ range: '0-60', count: 2 }, { range: '90-100', count: 8 }], - wrongQuestions: [ - { id: 'q-1', content: '已知集合 A={1,2}, B={2,3}, 则 A∩B=?', errorRate: 45, difficulty: 2, type: '单选题' }, - ] - }; - } -}; - -export const mockAssignmentService: IAssignmentService = { - getTeachingAssignments: async (): Promise> => { - await delay(500); - return { - totalCount: 4, - pageIndex: 1, - pageSize: 10, - items: [ - { id: 'a-1', title: '期中考试模拟卷', examTitle: '2024-2025学年第一学期期中数学考试', className: '高一 (10) 班', submittedCount: 30, totalCount: 32, status: 'Active', dueDate: '2023-11-01' }, - ] - }; - }, - getStudentAssignments: async (): Promise> => { - await delay(500); - return { - totalCount: 3, - pageIndex: 1, - pageSize: 10, - items: [ - { id: 'a-1', title: '期中考试模拟卷', examTitle: '2024-2025学年第一学期期中数学考试', endTime: '2023-11-01', status: 'Pending' }, - ] - } - }, - publishAssignment: async (data: any): Promise => { - await delay(1000); - console.log('Published assignment:', data); - }, - getAssignmentStats: async (id: string): Promise => { - await delay(800); - return { - averageScore: 82.5, - passRate: 95.0, - maxScore: 100, - minScore: 58, - scoreDistribution: [], - wrongQuestions: [] - }; - } -}; - -export const mockAnalyticsService: IAnalyticsService = { - getClassPerformance: async (): Promise => { - await delay(700); - return { - labels: ['周一', '周二', '周三'], - datasets: [ - { label: '平均分', data: [78, 82, 80], borderColor: '#007AFF', backgroundColor: 'rgba(0, 122, 255, 0.1)' } - ] - }; - }, - getStudentGrowth: async (): Promise => { - await delay(700); - return { - labels: ['第一次', '第二次'], - datasets: [ - { label: '我的成绩', data: [82, 85], borderColor: '#34C759', backgroundColor: 'rgba(52, 199, 89, 0.1)', fill: true }, - { label: '班级平均', data: [75, 78], borderColor: '#8E8E93', backgroundColor: 'transparent', fill: false } - ] - }; - }, - getRadar: async (): Promise => { - await delay(700); - return { indicators: ['代数', '几何'], values: [85, 70] }; - }, - getStudentRadar: async (): Promise => { - await delay(700); - return { indicators: ['代数', '几何'], values: [90, 60] }; - }, - getScoreDistribution: async (): Promise => { - await delay(600); - return [{ range: '0-60', count: 2 }, { range: '90-100', count: 8 }]; - }, - getTeacherStats: async () => { - await delay(600); - return { - activeStudents: 1240, - averageScore: 84.5, - pendingGrading: 38, - passRate: 96 - }; - } -}; - -export const mockGradingService: IGradingService = { - getSubmissions: async (assignmentId: string): Promise => { - await delay(600); - return Array.from({ length: 15 }).map((_, i) => ({ - id: `sub-${i}`, - studentName: `学生 ${i + 1}`, - studentId: `20240${i < 10 ? '0' + i : i}`, - avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`, - status: i < 5 ? 'Graded' : 'Submitted', - score: i < 5 ? 85 + (i % 10) : undefined, - submitTime: '2024-10-24 14:30' - })); - }, - getPaper: async (submissionId: string): Promise => { - await delay(800); - return { - submissionId, - studentName: '王小明', - nodes: [ - { - examNodeId: 'node-1', - questionId: 'q1', - questionContent: '已知集合 A={x|x²-2x-3<0}, B={x|y=ln(2-x)}, 求 A∩B.', - questionType: '计算题', - score: 10, - studentAnswer: 'https://placehold.co/600x300/png?text=Student+Handwriting+Here', - studentScore: undefined - } - ] - }; - } - , - submitGrade: async (submissionId: string, grades: any[]): Promise<{ message: string; totalScore: number }> => { - await delay(600); - const totalScore = grades.reduce((sum, g) => sum + (g.score || 0), 0); - return { message: 'ok', totalScore }; - } -}; - -export const mockSubmissionService: ISubmissionService = { - getStudentPaper: async (assignmentId: string): Promise => { - await delay(1200); - return { - examId: 'e-101', - title: '2024-2025学年第一学期期中数学考试', - duration: 90, - totalScore: 100, - rootNodes: [ - { - id: 'node-1', - nodeType: 'Group', - title: '一、选择题', - score: 40, - sortOrder: 1, - children: [ - { - id: 'node-1-1', - nodeType: 'Question', - questionId: 'q-1', - questionContent: '已知集合 A={1,2,3}, B={2,3,4}, 则 A∩B=( )', - questionType: '单选题', - score: 5, - sortOrder: 1 - } - ] - } - ] - }; - }, - submitExam: async (data: SubmitExamDto): Promise => { - await delay(1500); - console.log('Submitted:', data); - }, - getSubmissionResult: async (assignmentId: string): Promise => { - await delay(800); - return { - submissionId: 'sub-my-1', - studentName: '我', - totalScore: 88, - rank: 5, - beatRate: 85, - nodes: [ - { - examNodeId: 'node-1', - questionId: 'q-1', - questionContent: '已知集合 A={1,2,3}, B={2,3,4}, 则 A∩B=( )', - questionType: '单选题', - score: 5, - studentScore: 5, - studentAnswer: '{2,3}', - autoCheckResult: true - } - ] - } - } -} - -export const mockCommonService: ICommonService = { - getSchedule: async (): Promise => { - await delay(300); - const today = new Date().getDay() || 7; - return MOCK_SCHEDULE.filter(s => s.dayOfWeek === today).map(s => ({ ...s, isToday: true })); - } -} - -export const mockMessageService: IMessageService = { - getMessages: async (): Promise => { - await delay(500); - return [...MOCK_MESSAGES]; - }, - markAsRead: async (id: string): Promise => { - await delay(200); - const msg = MOCK_MESSAGES.find(m => m.id === id); - if (msg) msg.isRead = true; - }, - createMessage: async (data: CreateMessageDto): Promise => { - await delay(800); - MOCK_MESSAGES.unshift({ - id: `msg-${Date.now()}`, - title: data.title, - content: data.content, - type: data.type as any, - senderName: '我', - senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex', - createdAt: '刚刚', - isRead: true - }); - } -}; - -export const mockScheduleService: IScheduleService = { - getWeekSchedule: async (): Promise => { - await delay(600); - return [...MOCK_SCHEDULE]; - }, - addEvent: async (data: CreateScheduleDto): Promise => { - await delay(800); - MOCK_SCHEDULE.push({ - id: `sch-${Date.now()}`, - ...data, - isToday: false - }); - }, - deleteEvent: async (id: string): Promise => { - await delay(500); - MOCK_SCHEDULE = MOCK_SCHEDULE.filter(s => s.id !== id); - } -}; diff --git a/src/services/realApi.ts b/src/services/realApi.ts index 37190a2..7f4a768 100644 --- a/src/services/realApi.ts +++ b/src/services/realApi.ts @@ -10,7 +10,7 @@ import { } from '../../UI_DTO'; -const API_BASE_URL = 'http://localhost:3001/api'; // 直接连接到后端服务器 +const API_BASE_URL = 'http://127.0.0.1:8081/api'; const DEFAULT_TIMEOUT = 30000; // 30 秒超时 // Helper to handle requests with timeout @@ -180,8 +180,28 @@ export const realQuestionService: IQuestionService = { }; export const realExamService: IExamService = { - getMyExams: () => request('/exams'), + getExams: (filter) => { + const query = new URLSearchParams(); + if (filter?.subjectId) query.append('subjectId', filter.subjectId); + if (filter?.status) query.append('status', filter.status); + if (filter?.scope) query.append('scope', filter.scope); + if (filter?.page) query.append('page', String(filter.page)); + if (filter?.pageSize) query.append('pageSize', String(filter.pageSize)); + if (filter?.examType) query.append('examType', filter.examType); + return request(`/exams?${query.toString()}`); + }, getExamDetail: (id) => request(`/exams/${id}`), + createExam: (data) => request('/exams', { + method: 'POST', + body: JSON.stringify(data) + }), + updateExam: (id, data) => request(`/exams/${id}`, { + method: 'PUT', + body: JSON.stringify(data) + }), + deleteExam: (id) => request(`/exams/${id}`, { + method: 'DELETE' + }), saveExam: async (exam: ExamDetailDto) => { // Determine if create or update if (exam.id) { @@ -219,13 +239,34 @@ export const realExamService: IExamService = { }; export const realAssignmentService: IAssignmentService = { - getTeachingAssignments: () => request('/assignments/teaching'), - getStudentAssignments: () => request('/assignments/learning'), + getTeachingAssignments: (filters) => { + const query = new URLSearchParams(); + if (filters?.classId) query.append('classId', filters.classId); + if (filters?.examType) query.append('examType', filters.examType); + if (filters?.subjectId) query.append('subjectId', filters.subjectId); + if (filters?.status) query.append('status', filters.status); + return request(`/assignments/teaching?${query.toString()}`); + }, + getStudentAssignments: (filters) => { + const query = new URLSearchParams(); + if (filters?.subjectId) query.append('subjectId', filters.subjectId); + if (filters?.examType) query.append('examType', filters.examType); + if (filters?.status) query.append('status', filters.status); + return request(`/assignments/learning?${query.toString()}`); + }, publishAssignment: (data) => request('/assignments', { method: 'POST', body: JSON.stringify(data) }), - getAssignmentStats: (id) => request(`/assignments/${id}/stats`) + updateAssignment: (id, data) => request(`/assignments/${id}`, { + method: 'PUT', + body: JSON.stringify(data) + }), + archiveAssignment: (id) => request(`/assignments/${id}/archive`, { + method: 'POST' + }), + getAssignmentStats: (id) => request(`/assignments/${id}/stats`), + getAssignmentAnalysis: (id) => request(`/assignments/${id}/analysis`) }; @@ -241,14 +282,16 @@ export const realGradingService: IGradingService = { export const realSubmissionService: ISubmissionService = { getStudentPaper: (id) => request(`/submissions/${id}/paper`), - submitExam: (data) => { - const answersArray = Object.entries(data.answers || {}).map(([examNodeId, studentAnswer]) => ({ examNodeId, studentAnswer: (typeof studentAnswer === 'object' ? JSON.stringify(studentAnswer) : studentAnswer) })); - return request(`/submissions/${data.assignmentId}/submit`, { - method: 'POST', - body: JSON.stringify({ answers: answersArray, timeSpent: data.timeSpent }) - }); - }, - getSubmissionResult: (assignmentId: string) => request(`/submissions/by-assignment/${assignmentId}/result`), + submitAnswers: (assignmentId, answers, timeSpent) => request(`/submissions/${assignmentId}/submit`, { + method: 'POST', + body: JSON.stringify({ answers, timeSpent }) + }), + saveProgress: (assignmentId, answers) => request(`/submissions/${assignmentId}/save`, { + method: 'POST', + body: JSON.stringify({ answers }) + }), + getSubmissionResult: (submissionId) => request(`/submissions/${submissionId}/result`), + getSubmissionResultByAssignment: (assignmentId) => request(`/submissions/by-assignment/${assignmentId}/result`), }; export const realCommonService: ICommonService = { @@ -279,5 +322,6 @@ export const realAnalyticsService: IAnalyticsService = { getRadar: () => request('/analytics/radar'), getStudentRadar: () => request('/analytics/student/radar'), getScoreDistribution: () => request('/analytics/distribution'), - getTeacherStats: () => request('/analytics/teacher-stats') + getTeacherStats: () => request('/analytics/teacher-stats'), + getStudentStats: () => request('/analytics/student/stats') }; diff --git a/src/types.ts b/src/types.ts index c14f336..4921b78 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,344 +1 @@ - -// 0. Common -export interface ResultDto { - success: boolean; - message: string; - data?: any; -} - -export interface PagedResult { - items: T[]; - totalCount: number; - pageIndex: number; - pageSize: number; -} - -// 1. Auth & User -export interface UserProfileDto { - id: string; - realName: string; - studentId: string; - avatarUrl: string; - gender: string; - schoolId: string; - role: 'Admin' | 'Teacher' | 'Student'; - email?: string; - phone?: string; - bio?: string; -} - -export interface RegisterDto { - realName: string; - studentId: string; // 学号/工号 - password: string; - role: 'Teacher' | 'Student'; -} - -export interface UpdateProfileDto { - realName?: string; - email?: string; - phone?: string; - bio?: string; -} - -export interface ChangePasswordDto { - oldPassword: string; - newPassword: string; -} - -export interface LoginResultDto { - token: string; - user: UserProfileDto; -} - -// 2. Org -export interface SchoolDto { - id: string; - name: string; - regionCode: string; - address: string; -} - -export interface ClassDto { - id: string; - name: string; - inviteCode: string; - gradeName: string; - teacherName: string; - studentCount: number; -} - -export interface CreateClassDto { - name: string; - gradeName: string; -} - -export interface ClassMemberDto { - id: string; - studentId: string; - realName: string; - avatarUrl: string; - gender: 'Male' | 'Female'; - role: 'Student' | 'Monitor' | 'Committee'; // 班长/委员等 - recentTrend: number[]; // Last 5 scores/performances - status: 'Active' | 'AtRisk' | 'Excellent'; - attendanceRate: number; -} - -export interface SchoolStructureDto { - school: SchoolDto; - grades: GradeNodeDto[]; -} - -export interface GradeNodeDto { - id: string; - name: string; - classes: ClassDto[]; -} - -// 3. Curriculum -export interface SubjectDto { - id: string; - name: string; - code: string; - icon?: string; -} - -export interface TextbookDto { - id: string; - name: string; - publisher: string; - versionYear: string; - coverUrl: string; -} - -export interface CurriculumTreeDto { - textbook: TextbookDto; - children: UnitNodeDto[]; -} - -export interface UnitNodeDto { - id: string; - name: string; - type: 'unit' | 'lesson' | 'point'; - children?: UnitNodeDto[]; - difficulty?: number; -} - -// 4. Question -export interface QuestionSummaryDto { - id: string; - content: string; // HTML - type: string; - difficulty: number; - knowledgePoints: string[]; -} - -export interface ParsedQuestionDto { - content: string; - type: string; - options?: string[]; - answer?: string; - parse?: string; -} - -export interface QuestionFilterDto { - subjectId?: string; - type?: number; - difficulty?: number; - keyword?: string; -} - -// 5. Exam -export interface ExamDto { - id: string; - title: string; - totalScore: number; - duration: number; - questionCount: number; - status: 'Draft' | 'Published'; - createdAt: string; -} - -export interface ExamQuestionNodeDto { - id: string; // node id (unique in exam structure) - questionId: string; // ref to QuestionSummaryDto - content: string; - type: string; - score: number; -} - -export interface ExamSectionDto { - id: string; - title: string; - description?: string; - questions: ExamQuestionNodeDto[]; -} - -export interface ExamDetailDto extends ExamDto { - sections: ExamSectionDto[]; -} - -export interface WrongQuestionAnalysisDto { - id: string; - content: string; - errorRate: number; // 0-100 - difficulty: number; - type: string; -} - -export interface ExamStatsDto { - averageScore: number; - passRate: number; - maxScore: number; - minScore: number; - scoreDistribution: { range: string; count: number }[]; - wrongQuestions: WrongQuestionAnalysisDto[]; -} - -// 6. Assignment -export interface AssignmentTeacherViewDto { - id: string; - title: string; - className: string; - submittedCount: number; - totalCount: number; - status: 'Active' | 'Ended' | 'Scheduled'; - dueDate: string; - examTitle: string; -} - -export interface AssignmentStudentViewDto { - id: string; - title: string; - examTitle: string; - endTime: string; - status: 'Pending' | 'Graded' | 'Submitted'; - score?: number; -} - -// 7. Submission / Student Exam -export interface StudentExamPaperDto { - examId: string; - title: string; - duration: number; // minutes - totalScore: number; - sections: { - id: string; - title: string; - questions: { - id: string; // questionId - content: string; - type: string; // '单选题' | '多选题' | '填空题' | '简答题' - score: number; - options?: string[]; // JSON strings or simple array if processed - }[]; - }[]; -} - -export interface SubmitExamDto { - assignmentId: string; - answers: Record; - timeSpent?: number; -} - -// 8. Grading & Results -export interface StudentSubmissionSummaryDto { - id: string; // submissionId - studentName: string; - studentId: string; - avatarUrl: string; - status: 'Submitted' | 'Graded' | 'Late'; - score?: number; - submitTime: string; -} - -export interface GradingPaperDto { - submissionId: string; - studentName: string; - nodes: GradingNodeDto[]; -} - -export interface GradingNodeDto { - questionId: string; - questionContent: string; - questionType: string; - score: number; // max score - studentScore?: number; // current score - studentAnswer?: string; // Text or Image URL - teacherAnnotation?: string; // JSON for canvas - autoCheckResult?: boolean; -} - -export interface StudentResultDto extends GradingPaperDto { - totalScore: number; - rank: number; - beatRate: number; -} - -// 9. Analytics -export interface ChartDataDto { - labels: string[]; - datasets: { - label: string; - data: number[]; - borderColor?: string; - backgroundColor?: string; - fill?: boolean; - }[]; -} - -export interface RadarChartDto { - indicators: string[]; - values: number[]; -} - -export interface ScoreDistributionDto { - range: string; // e.g. "90-100" - count: number; -} - -// 10. Common / Dashboard -export interface ScheduleDto { - id: string; - startTime: string; - endTime: string; - className: string; - subject: string; - room: string; - isToday: boolean; - dayOfWeek?: number; // 1 = Monday, 7 = Sunday - period?: number; // 1-8 -} - -export interface CreateScheduleDto { - subject: string; - className: string; - room: string; - dayOfWeek: number; - period: number; - startTime: string; - endTime: string; -} - -// 11. Messages -export interface MessageDto { - id: string; - title: string; - content: string; - type: 'Announcement' | 'Notification' | 'Alert'; - senderName: string; - senderAvatar?: string; - createdAt: string; - isRead: boolean; -} - -export interface CreateMessageDto { - title: string; - content: string; - type: 'Announcement' | 'Notification'; - targetClassIds?: string[]; // Optional: if empty, broadcast to all managed classes -} - -// UI Types -export type ViewState = 'login' | 'dashboard' | 'curriculum' | 'questions' | 'classes' | 'exams' | 'assignments' | 'settings' | 'grading' | 'student-exam' | 'student-result' | 'messages' | 'schedule'; +export * from '../UI_DTO'; diff --git a/src/views/ExamEngine.tsx b/src/views/ExamEngine.tsx index 0059a69..fce7af8 100644 --- a/src/views/ExamEngine.tsx +++ b/src/views/ExamEngine.tsx @@ -1,58 +1,48 @@ import React, { useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; import { ExamList } from '@/features/exam/components/ExamList'; import { ExamEditor } from '@/features/exam/components/ExamEditor'; import { ExamStats } from '@/features/exam/components/ExamStats'; -export const ExamEngine: React.FC = () => { - const [view, setView] = useState<'list' | 'editor' | 'stats'>('list'); - const [selectedId, setSelectedId] = useState(); +export const ExamEngine = () => { + const [viewState, setViewState] = useState<'list' | 'editor' | 'stats'>('list'); + const [selectedExamId, setSelectedExamId] = useState(undefined); - return ( -
- - {view === 'list' && ( - - { setSelectedId(undefined); setView('editor'); }} - onEdit={(id) => { setSelectedId(id); setView('editor'); }} - onStats={(id) => { setSelectedId(id); setView('stats'); }} - /> - - )} - {view === 'editor' && ( - - setView('list')} - /> - - )} - {view === 'stats' && selectedId && ( - - setView('list')} - /> - - )} - + const handleEditExam = (id: string) => { + setSelectedExamId(id); + setViewState('editor'); + }; + + const handleCreateExam = () => { + setSelectedExamId(undefined); + setViewState('editor'); + }; + + const handleStats = (id: string) => { + setSelectedExamId(id); + setViewState('stats'); + }; + + const handleBack = () => { + setViewState('list'); + setSelectedExamId(undefined); + }; + + if (viewState === 'editor') { + return ; + } + + if (viewState === 'stats' && selectedExamId) { + return ; + } + + return ( +
+
+
- ); +
+ ); }; diff --git a/src/views/StudentExamRunner.tsx b/src/views/StudentExamRunner.tsx index efc2414..facfab8 100644 --- a/src/views/StudentExamRunner.tsx +++ b/src/views/StudentExamRunner.tsx @@ -7,7 +7,7 @@ import { RunnerQuestionCard } from '@/features/exam/components/runner/RunnerQues import { ExamHeader } from '@/features/exam/components/runner/ExamHeader'; import { AnswerSheet } from '@/features/exam/components/runner/AnswerSheet'; import { SubmitConfirmModal } from '@/features/exam/components/runner/SubmitConfirmModal'; -import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Loader2, CheckCircle } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; interface StudentExamRunnerProps { @@ -26,17 +26,24 @@ export const StudentExamRunner: React.FC = ({ assignment const [showSubmitModal, setShowSubmitModal] = useState(false); const [submitting, setSubmitting] = useState(false); const { showToast } = useToast(); + const [lastSavedTime, setLastSavedTime] = useState(null); useEffect(() => { submissionService.getStudentPaper(assignmentId).then(data => { setPaper(data); - setTimeLeft(data.duration * 60); - // Recursively collect all question nodes + // Initialize answers if user had some saved progress (TODO: Backend should return saved answers) + // For now assuming backend logic will populate initial answers later if we implement resume functionality. + // Actually, getStudentPaper does return studentAnswer in rootNodes! + // We need to extract them. + const initialAnswers: Record = {}; const flattenNodes = (nodes: typeof data.rootNodes): any[] => { const questions: any[] = []; nodes.forEach(node => { if (node.nodeType === 'Question') { questions.push({ ...node, id: node.id }); + if (node.studentAnswer) { + initialAnswers[node.id] = node.studentAnswer; + } } else if (node.children) { questions.push(...flattenNodes(node.children)); } @@ -45,9 +52,33 @@ export const StudentExamRunner: React.FC = ({ assignment }; const allQuestions = flattenNodes(data.rootNodes); setFlatQuestions(allQuestions); + setAnswers(initialAnswers); + setTimeLeft(data.duration * 60); }); }, [assignmentId]); + // Auto-save logic + useEffect(() => { + const saveInterval = setInterval(async () => { + if (Object.keys(answers).length > 0 && !submitting) { + // Transform answers for API + const answersArray = Object.entries(answers).map(([examNodeId, studentAnswer]) => ({ + examNodeId, + studentAnswer: (typeof studentAnswer === 'object' ? JSON.stringify(studentAnswer) : studentAnswer) + })); + + try { + await submissionService.saveProgress(assignmentId, answersArray); + setLastSavedTime(new Date()); + } catch (err) { + console.error('Auto-save failed', err); + } + } + }, 30000); // Save every 30 seconds + + return () => clearInterval(saveInterval); + }, [assignmentId, answers, submitting]); + useEffect(() => { if (!paper) return; const timer = setInterval(() => { @@ -84,12 +115,12 @@ export const StudentExamRunner: React.FC = ({ assignment setShowSubmitModal(false); setSubmitting(true); try { - const submitData: SubmitExamDto = { - assignmentId, - answers, - timeSpent: paper ? paper.duration * 60 - timeLeft : 0 - }; - await submissionService.submitExam(submitData); + const answerArray = Object.entries(answers).map(([key, value]) => ({ + examNodeId: key, + studentAnswer: value + })); + const timeSpent = paper ? paper.duration * 60 - timeLeft : 0; + await submissionService.submitAnswers(assignmentId, answerArray, timeSpent); showToast('试卷提交成功!正在跳转...', 'success'); setTimeout(onExit, 2000); } catch (e) { @@ -133,6 +164,13 @@ export const StudentExamRunner: React.FC = ({ assignment onToggleSheet={() => setIsSheetOpen(true)} onSubmit={handleSubmitClick} /> + + {lastSavedTime && ( +
+ + 已自动保存 {lastSavedTime.toLocaleTimeString()} +
+ )}
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo new file mode 100644 index 0000000..0cc9f6b --- /dev/null +++ b/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/prop-types/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/next/dist/shared/lib/amp.d.ts","./node_modules/next/amp.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/next/dist/server/get-page-files.d.ts","./node_modules/@types/react/canary.d.ts","./node_modules/@types/react/experimental.d.ts","./node_modules/@types/react-dom/index.d.ts","./node_modules/@types/react-dom/canary.d.ts","./node_modules/@types/react-dom/experimental.d.ts","./node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/next/dist/server/config.d.ts","./node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/next/dist/server/body-streams.d.ts","./node_modules/next/dist/server/future/route-kind.d.ts","./node_modules/next/dist/server/future/route-definitions/route-definition.d.ts","./node_modules/next/dist/server/future/route-matches/route-match.d.ts","./node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/next/dist/server/request-meta.d.ts","./node_modules/next/dist/server/lib/revalidate.d.ts","./node_modules/next/dist/server/config-shared.d.ts","./node_modules/next/dist/server/base-http/index.d.ts","./node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/next/dist/server/node-environment.d.ts","./node_modules/next/dist/server/require-hook.d.ts","./node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/next/dist/lib/page-types.d.ts","./node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/next/dist/server/render-result.d.ts","./node_modules/next/dist/server/future/helpers/i18n-provider.d.ts","./node_modules/next/dist/server/web/next-url.d.ts","./node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/next/dist/server/web/types.d.ts","./node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/next/dist/lib/constants.d.ts","./node_modules/next/dist/build/index.d.ts","./node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/next/dist/server/base-http/node.d.ts","./node_modules/next/dist/server/font-utils.d.ts","./node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/next/dist/server/future/route-modules/route-module.d.ts","./node_modules/next/dist/server/load-components.d.ts","./node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/next/dist/server/future/route-definitions/locale-route-definition.d.ts","./node_modules/next/dist/server/future/route-definitions/pages-route-definition.d.ts","./node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/next/dist/client/with-router.d.ts","./node_modules/next/dist/client/router.d.ts","./node_modules/next/dist/client/route-loader.d.ts","./node_modules/next/dist/client/page-loader.d.ts","./node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/next/dist/server/future/route-definitions/app-page-route-definition.d.ts","./node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/next/dist/build/webpack/loaders/next-app-loader.d.ts","./node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/next/dist/client/components/static-generation-async-storage.external.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/next/dist/client/components/request-async-storage.external.d.ts","./node_modules/next/dist/server/app-render/create-error-handler.d.ts","./node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","./node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/future/route-modules/app-page/module.compiled.d.ts","./node_modules/@types/react/jsx-runtime.d.ts","./node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/next/dist/client/components/router-reducer/create-initial-router-state.d.ts","./node_modules/next/dist/client/components/app-router.d.ts","./node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/next/dist/client/components/action-async-storage.external.d.ts","./node_modules/next/dist/client/components/client-page.d.ts","./node_modules/next/dist/client/components/search-params.d.ts","./node_modules/next/dist/client/components/not-found-boundary.d.ts","./node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/next/dist/server/future/route-modules/app-page/module.d.ts","./node_modules/next/dist/server/app-render/types.d.ts","./node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/future/route-modules/pages/module.compiled.d.ts","./node_modules/next/dist/build/templates/pages.d.ts","./node_modules/next/dist/server/future/route-modules/pages/module.d.ts","./node_modules/next/dist/server/render.d.ts","./node_modules/next/dist/server/future/route-definitions/pages-api-route-definition.d.ts","./node_modules/next/dist/server/future/route-matches/pages-api-route-match.d.ts","./node_modules/next/dist/server/future/route-matchers/route-matcher.d.ts","./node_modules/next/dist/server/future/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/next/dist/server/future/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/next/dist/server/future/normalizers/normalizer.d.ts","./node_modules/next/dist/server/future/normalizers/locale-route-normalizer.d.ts","./node_modules/next/dist/server/future/normalizers/request/pathname-normalizer.d.ts","./node_modules/next/dist/server/future/normalizers/request/suffix.d.ts","./node_modules/next/dist/server/future/normalizers/request/rsc.d.ts","./node_modules/next/dist/server/future/normalizers/request/prefix.d.ts","./node_modules/next/dist/server/future/normalizers/request/postponed.d.ts","./node_modules/next/dist/server/future/normalizers/request/prefetch-rsc.d.ts","./node_modules/next/dist/server/future/normalizers/request/next-data.d.ts","./node_modules/next/dist/server/base-server.d.ts","./node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/next/dist/server/next-server.d.ts","./node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/next/dist/trace/types.d.ts","./node_modules/next/dist/trace/trace.d.ts","./node_modules/next/dist/trace/shared.d.ts","./node_modules/next/dist/trace/index.d.ts","./node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/next/dist/build/webpack-config.d.ts","./node_modules/next/dist/build/webpack/plugins/define-env-plugin.d.ts","./node_modules/next/dist/build/swc/index.d.ts","./node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/next/dist/telemetry/storage.d.ts","./node_modules/next/dist/server/lib/types.d.ts","./node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/next/dist/server/next.d.ts","./node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/next/types/index.d.ts","./node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/@next/env/dist/index.d.ts","./node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/next/dist/pages/_app.d.ts","./node_modules/next/app.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/next/cache.d.ts","./node_modules/next/dist/shared/lib/runtime-config.external.d.ts","./node_modules/next/config.d.ts","./node_modules/next/dist/pages/_document.d.ts","./node_modules/next/document.d.ts","./node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/next/dynamic.d.ts","./node_modules/next/dist/pages/_error.d.ts","./node_modules/next/error.d.ts","./node_modules/next/dist/shared/lib/head.d.ts","./node_modules/next/head.d.ts","./node_modules/next/dist/client/components/draft-mode.d.ts","./node_modules/next/dist/client/components/headers.d.ts","./node_modules/next/headers.d.ts","./node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/next/dist/client/image-component.d.ts","./node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/next/image.d.ts","./node_modules/next/dist/client/link.d.ts","./node_modules/next/link.d.ts","./node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/next/dist/client/components/redirect.d.ts","./node_modules/next/dist/client/components/not-found.d.ts","./node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/next/dist/client/components/navigation.d.ts","./node_modules/next/navigation.d.ts","./node_modules/next/router.d.ts","./node_modules/next/dist/client/script.d.ts","./node_modules/next/script.d.ts","./node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/next/server.d.ts","./node_modules/next/types/global.d.ts","./node_modules/next/types/compiled.d.ts","./node_modules/next/index.d.ts","./node_modules/next/image-types/global.d.ts","./next-env.d.ts","./model.ts","./ui_dto.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/tailwindcss/types/generated/corepluginlist.d.ts","./node_modules/tailwindcss/types/generated/colors.d.ts","./node_modules/tailwindcss/types/config.d.ts","./node_modules/tailwindcss/types/index.d.ts","./tailwind.config.ts","./backend/node_modules/@types/send/index.d.ts","./backend/node_modules/@types/qs/index.d.ts","./backend/node_modules/@types/range-parser/index.d.ts","./backend/node_modules/@types/express-serve-static-core/index.d.ts","./backend/dist/index.d.ts","./backend/node_modules/@types/http-errors/index.d.ts","./backend/node_modules/@types/mime/index.d.ts","./backend/node_modules/@types/serve-static/node_modules/@types/send/index.d.ts","./backend/node_modules/@types/serve-static/index.d.ts","./backend/node_modules/@types/connect/index.d.ts","./backend/node_modules/@types/body-parser/index.d.ts","./backend/node_modules/@types/express/index.d.ts","./backend/dist/middleware/auth.middleware.d.ts","./backend/dist/controllers/analytics.controller.d.ts","./backend/dist/controllers/assignment.controller.d.ts","./backend/dist/controllers/auth.controller.d.ts","./backend/dist/controllers/common.controller.d.ts","./backend/dist/controllers/config.controller.d.ts","./backend/dist/controllers/curriculum.controller.d.ts","./backend/dist/controllers/exam.controller.d.ts","./backend/dist/controllers/grading.controller.d.ts","./backend/dist/controllers/org.controller.d.ts","./backend/dist/controllers/question.controller.d.ts","./backend/dist/controllers/submission.controller.d.ts","./backend/dist/routes/analytics.routes.d.ts","./backend/dist/routes/assignment.routes.d.ts","./backend/dist/routes/auth.routes.d.ts","./backend/dist/routes/common.routes.d.ts","./backend/dist/routes/config.routes.d.ts","./backend/dist/routes/curriculum.routes.d.ts","./backend/dist/routes/exam.routes.d.ts","./backend/dist/routes/grading.routes.d.ts","./backend/dist/routes/org.routes.d.ts","./backend/dist/routes/question.routes.d.ts","./backend/dist/routes/submission.routes.d.ts","./backend/dist/services/analytics.service.d.ts","./backend/node_modules/@prisma/client/runtime/library.d.ts","./backend/node_modules/.prisma/client/index.d.ts","./backend/dist/services/assignment.service.d.ts","./backend/dist/services/auth.service.d.ts","./backend/dist/services/common.service.d.ts","./backend/dist/services/config.service.d.ts","./backend/dist/services/curriculum.service.d.ts","./backend/dist/services/exam.service.d.ts","./backend/node_modules/.prisma/client/default.d.ts","./backend/node_modules/@prisma/client/default.d.ts","./backend/dist/services/grading.service.d.ts","./backend/dist/services/org.service.d.ts","./backend/dist/services/question.service.d.ts","./backend/dist/services/submission.service.d.ts","./backend/dist/utils/helpers.d.ts","./backend/dist/utils/prisma.d.ts","./backend/node_modules/@types/bcryptjs/index.d.ts","./backend/node_modules/uuid/dist/esm-browser/types.d.ts","./backend/node_modules/uuid/dist/esm-browser/max.d.ts","./backend/node_modules/uuid/dist/esm-browser/nil.d.ts","./backend/node_modules/uuid/dist/esm-browser/parse.d.ts","./backend/node_modules/uuid/dist/esm-browser/stringify.d.ts","./backend/node_modules/uuid/dist/esm-browser/v1.d.ts","./backend/node_modules/uuid/dist/esm-browser/v1tov6.d.ts","./backend/node_modules/uuid/dist/esm-browser/v35.d.ts","./backend/node_modules/uuid/dist/esm-browser/v3.d.ts","./backend/node_modules/uuid/dist/esm-browser/v4.d.ts","./backend/node_modules/uuid/dist/esm-browser/v5.d.ts","./backend/node_modules/uuid/dist/esm-browser/v6.d.ts","./backend/node_modules/uuid/dist/esm-browser/v6tov1.d.ts","./backend/node_modules/uuid/dist/esm-browser/v7.d.ts","./backend/node_modules/uuid/dist/esm-browser/validate.d.ts","./backend/node_modules/uuid/dist/esm-browser/version.d.ts","./backend/node_modules/uuid/dist/esm-browser/index.d.ts","./backend/prisma/seed.ts","./backend/node_modules/@types/cors/index.d.ts","./backend/node_modules/dotenv/lib/main.d.ts","./backend/node_modules/@types/ms/index.d.ts","./backend/node_modules/@types/jsonwebtoken/index.d.ts","./backend/src/middleware/auth.middleware.ts","./backend/src/utils/prisma.ts","./backend/src/services/auth.service.ts","./backend/src/controllers/auth.controller.ts","./backend/src/routes/auth.routes.ts","./backend/src/services/exam.service.ts","./backend/src/controllers/exam.controller.ts","./backend/src/routes/exam.routes.ts","./backend/src/services/analytics.service.ts","./backend/src/controllers/analytics.controller.ts","./backend/src/routes/analytics.routes.ts","./backend/src/services/common.service.ts","./backend/src/controllers/common.controller.ts","./backend/src/routes/common.routes.ts","./backend/src/services/config.service.ts","./backend/src/controllers/config.controller.ts","./backend/src/routes/config.routes.ts","./backend/src/utils/helpers.ts","./backend/src/services/org.service.ts","./backend/src/controllers/org.controller.ts","./backend/src/routes/org.routes.ts","./backend/src/services/curriculum.service.ts","./backend/src/controllers/curriculum.controller.ts","./backend/src/routes/curriculum.routes.ts","./backend/src/services/question.service.ts","./backend/src/controllers/question.controller.ts","./backend/src/routes/question.routes.ts","./backend/src/services/assignment.service.ts","./backend/src/controllers/assignment.controller.ts","./backend/src/routes/assignment.routes.ts","./backend/src/services/submission.service.ts","./backend/src/controllers/submission.controller.ts","./backend/src/routes/submission.routes.ts","./backend/src/services/grading.service.ts","./backend/src/controllers/grading.controller.ts","./backend/src/routes/grading.routes.ts","./backend/src/index.ts","./src/middleware.ts","./src/types.ts","./src/app/api/auth/login/route.ts","./src/app/api/auth/me/route.ts","./src/app/api/config/db/route.ts","./src/app/api/org/classes/route.ts","./src/services/interfaces.ts","./src/services/realapi.ts","./src/services/api.ts","./src/utils/errorutils.ts","./node_modules/next/dist/compiled/@next/font/dist/types.d.ts","./node_modules/next/dist/compiled/@next/font/dist/google/index.d.ts","./node_modules/next/font/google/index.d.ts","./src/lib/auth-context.tsx","./node_modules/motion-dom/dist/index.d.ts","./node_modules/motion-utils/dist/index.d.ts","./node_modules/framer-motion/dist/index.d.ts","./node_modules/lucide-react/dist/lucide-react.d.ts","./src/components/ui/toast.tsx","./src/app/layout.tsx","./src/app/loading.tsx","./src/app/not-found.tsx","./src/app/page.tsx","./src/components/sidebar.tsx","./src/components/errorboundary.tsx","./src/app/(dashboard)/layout.tsx","./src/components/ui/card.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/loadingstate.tsx","./src/features/assignment/components/editassignmentmodal.tsx","./src/features/assignment/components/teacherassignmentlist.tsx","./src/features/assignment/components/studentassignmentlist.tsx","./src/features/assignment/components/createassignmentmodal.tsx","./src/app/(dashboard)/assignments/page.tsx","./node_modules/recharts/types/container/surface.d.ts","./node_modules/recharts/types/container/layer.d.ts","./node_modules/@types/d3-time/index.d.ts","./node_modules/@types/d3-scale/index.d.ts","./node_modules/victory-vendor/d3-scale.d.ts","./node_modules/recharts/types/cartesian/xaxis.d.ts","./node_modules/recharts/types/cartesian/yaxis.d.ts","./node_modules/recharts/types/util/types.d.ts","./node_modules/recharts/types/component/defaultlegendcontent.d.ts","./node_modules/recharts/types/util/payload/getuniqpayload.d.ts","./node_modules/recharts/types/component/legend.d.ts","./node_modules/recharts/types/component/defaulttooltipcontent.d.ts","./node_modules/recharts/types/component/tooltip.d.ts","./node_modules/recharts/types/component/responsivecontainer.d.ts","./node_modules/recharts/types/component/cell.d.ts","./node_modules/recharts/types/component/text.d.ts","./node_modules/recharts/types/component/label.d.ts","./node_modules/recharts/types/component/labellist.d.ts","./node_modules/recharts/types/component/customized.d.ts","./node_modules/recharts/types/shape/sector.d.ts","./node_modules/@types/d3-path/index.d.ts","./node_modules/@types/d3-shape/index.d.ts","./node_modules/victory-vendor/d3-shape.d.ts","./node_modules/recharts/types/shape/curve.d.ts","./node_modules/recharts/types/shape/rectangle.d.ts","./node_modules/recharts/types/shape/polygon.d.ts","./node_modules/recharts/types/shape/dot.d.ts","./node_modules/recharts/types/shape/cross.d.ts","./node_modules/recharts/types/shape/symbols.d.ts","./node_modules/recharts/types/polar/polargrid.d.ts","./node_modules/recharts/types/polar/polarradiusaxis.d.ts","./node_modules/recharts/types/polar/polarangleaxis.d.ts","./node_modules/recharts/types/polar/pie.d.ts","./node_modules/recharts/types/polar/radar.d.ts","./node_modules/recharts/types/polar/radialbar.d.ts","./node_modules/recharts/types/cartesian/brush.d.ts","./node_modules/recharts/types/util/ifoverflowmatches.d.ts","./node_modules/recharts/types/cartesian/referenceline.d.ts","./node_modules/recharts/types/cartesian/referencedot.d.ts","./node_modules/recharts/types/cartesian/referencearea.d.ts","./node_modules/recharts/types/cartesian/cartesianaxis.d.ts","./node_modules/recharts/types/cartesian/cartesiangrid.d.ts","./node_modules/recharts/types/cartesian/line.d.ts","./node_modules/recharts/types/cartesian/area.d.ts","./node_modules/recharts/types/util/barutils.d.ts","./node_modules/recharts/types/cartesian/bar.d.ts","./node_modules/recharts/types/cartesian/zaxis.d.ts","./node_modules/recharts/types/cartesian/errorbar.d.ts","./node_modules/recharts/types/cartesian/scatter.d.ts","./node_modules/recharts/types/util/getlegendprops.d.ts","./node_modules/recharts/types/util/chartutils.d.ts","./node_modules/recharts/types/chart/accessibilitymanager.d.ts","./node_modules/recharts/types/chart/types.d.ts","./node_modules/recharts/types/chart/generatecategoricalchart.d.ts","./node_modules/recharts/types/chart/linechart.d.ts","./node_modules/recharts/types/chart/barchart.d.ts","./node_modules/recharts/types/chart/piechart.d.ts","./node_modules/recharts/types/chart/treemap.d.ts","./node_modules/recharts/types/chart/sankey.d.ts","./node_modules/recharts/types/chart/radarchart.d.ts","./node_modules/recharts/types/chart/scatterchart.d.ts","./node_modules/recharts/types/chart/areachart.d.ts","./node_modules/recharts/types/chart/radialbarchart.d.ts","./node_modules/recharts/types/chart/composedchart.d.ts","./node_modules/recharts/types/chart/sunburstchart.d.ts","./node_modules/recharts/types/shape/trapezoid.d.ts","./node_modules/recharts/types/numberaxis/funnel.d.ts","./node_modules/recharts/types/chart/funnelchart.d.ts","./node_modules/recharts/types/util/global.d.ts","./node_modules/recharts/types/index.d.ts","./src/features/assignment/components/assignmentanalysis.tsx","./src/app/(dashboard)/assignments/[id]/analysis/page.tsx","./src/features/class/components/classcard.tsx","./src/features/class/components/studentrow.tsx","./src/features/class/components/classanalysis.tsx","./src/features/class/components/classsettings.tsx","./src/features/class/components/joinclassmodal.tsx","./src/features/class/components/createclassmodal.tsx","./src/views/classmanagement.tsx","./src/app/(dashboard)/classes/page.tsx","./src/views/consoleconfig.tsx","./src/app/(dashboard)/consoleconfig/page.tsx","./src/features/curriculum/components/treenode.tsx","./src/features/curriculum/components/knowledgegraph.tsx","./src/components/ui/errorstate.tsx","./src/components/ui/modal.tsx","./src/features/curriculum/components/curriculumnodemodal.tsx","./src/features/curriculum/components/deleteconfirmmodal.tsx","./src/features/curriculum/components/textbookmodal.tsx","./src/views/curriculum.tsx","./src/app/(dashboard)/curriculum/page.tsx","./src/features/dashboard/components/statcard.tsx","./src/features/dashboard/components/schedulelist.tsx","./src/features/dashboard/components/notificationlist.tsx","./src/components/ui/charttooltip.tsx","./src/features/dashboard/components/teacherdashboard.tsx","./src/features/dashboard/components/studentdashboard.tsx","./src/app/(dashboard)/dashboard/page.tsx","./src/features/exam/components/examlist.tsx","./src/features/exam/components/importmodal.tsx","./src/features/exam/components/exameditor.tsx","./src/features/exam/components/examstats.tsx","./src/views/examengine.tsx","./src/app/(dashboard)/exams/page.tsx","./src/features/message/components/createmessagemodal.tsx","./src/views/messages.tsx","./src/app/(dashboard)/messages/page.tsx","./src/features/question/components/subjectselector.tsx","./src/features/question/components/questioncard.tsx","./src/features/question/components/questionmodal.tsx","./src/views/questionbank.tsx","./src/app/(dashboard)/questions/page.tsx","./src/features/schedule/components/timetable.tsx","./src/features/schedule/components/eventmodal.tsx","./src/views/schedule.tsx","./src/app/(dashboard)/schedule/page.tsx","./src/features/settings/components/profilesettings.tsx","./src/features/settings/components/securitysettings.tsx","./src/features/settings/components/preferencesettings.tsx","./src/views/settings.tsx","./src/app/(dashboard)/settings/page.tsx","./src/features/exam/components/result/resultheader.tsx","./src/features/exam/components/result/resultlist.tsx","./src/views/studentresult.tsx","./src/app/(dashboard)/student-result/[id]/page.tsx","./src/app/(fullscreen)/layout.tsx","./src/features/grading/components/submissionsidebar.tsx","./src/features/grading/components/gradingtoolbar.tsx","./src/features/grading/components/questiongradingrow.tsx","./src/features/grading/components/gradingboard.tsx","./src/views/grading.tsx","./src/app/(fullscreen)/grading/[id]/page.tsx","./src/components/ui/handwritingpad.tsx","./src/features/exam/components/runner/runnerquestioncard.tsx","./src/features/exam/components/runner/examheader.tsx","./src/features/exam/components/runner/answersheet.tsx","./src/features/exam/components/runner/submitconfirmmodal.tsx","./src/views/studentexamrunner.tsx","./src/app/(fullscreen)/student-exam/[id]/page.tsx","./src/features/auth/components/loginform.tsx","./src/features/auth/components/registerform.tsx","./src/app/login/page.tsx","./src/features/assignment/components/assignmentstats.tsx","./.next/types/app/layout.ts","./.next/types/app/page.ts","./.next/types/app/(dashboard)/layout.ts","./.next/types/app/(dashboard)/assignments/page.ts","./.next/types/app/(dashboard)/dashboard/page.ts","./.next/types/app/(fullscreen)/layout.ts","./.next/types/app/(fullscreen)/grading/[id]/page.ts","./node_modules/@types/d3-array/index.d.ts","./node_modules/@types/d3-color/index.d.ts","./node_modules/@types/d3-ease/index.d.ts","./node_modules/@types/d3-interpolate/index.d.ts","./node_modules/@types/d3-timer/index.d.ts","./node_modules/@types/json5/index.d.ts"],"fileIdsList":[[99,145,354,578],[99,145,354,676],[99,145,354,569],[99,145,354,710],[99,145,354,704],[99,145,354,563],[99,145,354,566],[99,145,443,444],[99,145,443],[99,145,435],[99,145],[99,145,468,469],[99,145,469],[99,145,477],[99,145,468,477],[99,145,468],[99,145,476],[99,145,159,193,441],[99,145,159,193],[99,145,156,159,193,432,433,434],[99,145,435,440,442],[99,145,150,193,505],[99,145,157,175,193],[99,145,159,193,437,439],[99,145,157,175,193,438],[99,145,186,193],[99,145,485,486,487,488,489,490,491,493,494,495,496,497,498,499,500],[99,145,485],[99,145,485,492],[99,145,477,484,501],[99,145,443,507,515],[99,145,443,507,534],[99,145,443,507,509],[99,145,443,507,518],[99,145,443,521],[99,145,443,507,528],[99,145,443,507,512],[99,145,443,507,540],[99,145,443,507,525],[99,145,443,507,531],[99,145,443,507,537],[99,145,443,503,504,511,514,517,520,523,527,530,533,536,539,542],[99,145,443,506],[99,145,443,507,516],[99,145,443,507,535],[99,145,443,507,510],[99,145,443,507,519],[99,145,443,522],[99,145,443,507,529],[99,145,443,507,513],[99,145,443,507,541],[99,145,443,507,526],[99,145,443,507,532],[99,145,443,507,538],[99,145,501,508],[99,145,484,501,506,508],[99,145,508],[99,145,477,508],[99,145,501,508,524],[99,145,477,501,508],[99,145,402,403],[99,145,730],[99,145,581],[99,145,599],[99,142,145],[99,144,145],[145],[99,145,150,178],[99,145,146,151,156,164,175,186],[99,145,146,147,156,164],[94,95,96,99,145],[99,145,148,187],[99,145,149,150,157,165],[99,145,150,175,183],[99,145,151,153,156,164],[99,144,145,152],[99,145,153,154],[99,145,155,156],[99,144,145,156],[99,145,156,157,158,175,186],[99,145,156,157,158,171,175,178],[99,145,153,156,159,164,175,186],[99,145,156,157,159,160,164,175,183,186],[99,145,159,161,175,183,186],[97,98,99,100,101,102,103,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192],[99,145,156,162],[99,145,163,186,191],[99,145,153,156,164,175],[99,145,165],[99,145,166],[99,144,145,167],[99,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192],[99,145,169],[99,145,170],[99,145,156,171,172],[99,145,171,173,187,189],[99,145,156,175,176,178],[99,145,177,178],[99,145,175,176],[99,145,178],[99,145,179],[99,142,145,175,180],[99,145,156,181,182],[99,145,181,182],[99,145,150,164,175,183],[99,145,184],[99,145,164,185],[99,145,159,170,186],[99,145,150,187],[99,145,175,188],[99,145,163,189],[99,145,190],[99,140,145],[99,140,145,156,158,167,175,178,186,189,191],[99,145,175,192],[87,99,145,197,198,199],[87,99,145,197,198],[87,99,145],[87,91,99,145,196,355,398],[87,91,99,145,195,355,398],[84,85,86,99,145],[87,99,145,283,558,559],[92,99,145],[99,145,359],[99,145,361,362,363],[99,145,365],[99,145,202,212,218,220,355],[99,145,202,209,211,214,232],[99,145,212],[99,145,212,333],[99,145,266,282,296,401],[99,145,304],[99,145,202,212,219,252,262,330,331,401],[99,145,219,401],[99,145,212,262,263,264,401],[99,145,212,219,252,401],[99,145,401],[99,145,202,219,220,401],[99,144,145,193],[87,99,145,283,284,285,301,302],[87,99,145,283],[99,145,274],[99,145,273,275,375],[87,99,145,283,284,299],[99,145,279,302,387],[99,145,385,386],[99,145,226,384],[99,144,145,193,226,273,274,275],[87,99,145,299,301,302],[99,145,299,301],[99,145,299,300,302],[99,145,170,193],[99,144,145,193,211,213,269,270,271],[87,99,145,203,378],[87,99,145,186,193],[87,99,145,219,250],[87,99,145,219],[99,145,248,253],[87,99,145,249,358],[99,145,554],[87,91,99,145,159,193,195,196,355,396,397],[99,145,355],[99,145,201],[99,145,348,349,350,351,352,353],[99,145,350],[87,99,145,249,283,358],[87,99,145,283,356,358],[87,99,145,283,358],[99,145,159,193,213,358],[99,145,159,193,210,211,222,240,272,276,277,298,299],[99,145,272],[99,145,270,272,276,284,286,287,288,289,290,291,292,293,294,295,401],[99,145,271],[87,99,145,170,193,211,212,240,242,244,269,298,302,355,401],[99,145,159,193,213,214,226,227,273],[99,145,159,193,212,214],[99,145,159,175,193,210,213,214],[99,145,159,170,186,193,210,211,212,213,214,219,222,223,233,234,236,239,240,242,243,244,268,269,299,307,309,312,314,317,319,320,321],[99,145,159,175,193],[99,145,202,203,204,210,211,355,358,401],[99,145,159,175,186,193,207,332,334,335,401],[99,145,170,186,193,207,210,213,230,234,236,237,238,242,269,312,322,324,330,344,345],[99,145,212,216,269],[99,145,210,212],[99,145,223,313],[99,145,315],[99,145,313],[99,145,315,318],[99,145,315,316],[99,145,206,207],[99,145,206,245],[99,145,206],[99,145,208,223,311],[99,145,310],[99,145,207,208],[99,145,208,308],[99,145,207],[99,145,298],[99,145,159,193,210,222,241,260,266,278,281,297,299],[99,145,254,255,256,257,258,259,279,280,302,356],[99,145,306],[99,145,159,193,210,222,241,246,303,305,307,355,358],[99,145,159,186,193,203,210,212,268],[99,145,265],[99,145,159,193,338,343],[99,145,233,268,358],[99,145,326,330,344,347],[99,145,159,216,330,338,339,347],[99,145,202,212,233,243,341],[99,145,159,193,212,219,243,325,326,336,337,340,342],[99,145,194,240,241,355,358],[99,145,159,170,186,193,208,210,211,213,216,221,222,230,233,234,236,237,238,239,242,244,268,269,309,322,323,358],[99,145,159,193,210,212,216,324,346],[99,145,159,193,211,213],[87,99,145,159,170,193,201,203,210,211,214,222,239,240,242,244,306,355,358],[99,145,159,170,186,193,205,208,209,213],[99,145,206,267],[99,145,159,193,206,211,222],[99,145,159,193,212,223],[99,145,226],[99,145,225],[99,145,227],[99,145,212,224,226,230],[99,145,212,224,226],[99,145,159,193,205,212,213,219,227,228,229],[87,99,145,299,300,301],[99,145,261],[87,99,145,203],[87,99,145,236],[87,99,145,194,239,244,355,358],[99,145,203,378,379],[87,99,145,253],[87,99,145,170,186,193,201,247,249,251,252,358],[99,145,213,219,236],[99,145,235],[87,99,145,157,159,170,193,201,253,262,355,356,357],[83,87,88,89,90,99,145,195,196,355,398],[99,145,150],[99,145,327,328,329],[99,145,327],[99,145,367],[99,145,369],[99,145,371],[99,145,555],[99,145,373],[99,145,376],[99,145,380],[91,93,99,145,355,360,364,366,368,370,372,374,377,381,383,389,390,392,399,400,401],[99,145,382],[99,145,388],[99,145,249],[99,145,391],[99,144,145,227,228,229,230,393,394,395,398],[99,145,193],[87,91,99,145,159,161,170,193,195,196,197,199,201,214,347,354,358,398],[99,145,422],[99,145,420,422],[99,145,411,419,420,421,423,425],[99,145,409],[99,145,412,417,422,425],[99,145,408,425],[99,145,412,413,416,417,418,425],[99,145,412,413,414,416,417,425],[99,145,409,410,411,412,413,417,418,419,421,422,423,425],[99,145,425],[99,145,407,409,410,411,412,413,414,416,417,418,419,420,421,422,423,424],[99,145,407,425],[99,145,412,414,415,417,418,425],[99,145,416,425],[99,145,417,418,422,425],[99,145,410,420],[87,99,145,584,585,586,602,605],[87,99,145,584,585,586,595,603,623],[87,99,145,583,586],[87,99,145,586],[87,99,145,584,585,586],[87,99,145,584,585,586,621,624,627],[87,99,145,584,585,586,595,602,605],[87,99,145,584,585,586,595,603,615],[87,99,145,584,585,586,595,605,615],[87,99,145,584,585,586,595,615],[87,99,145,584,585,586,590,596,602,607,625,626],[99,145,586],[87,99,145,586,630,631,632],[87,99,145,586,629,630,631],[87,99,145,586,603],[87,99,145,586,629],[87,99,145,586,595],[87,99,145,586,587,588],[87,99,145,586,588,590],[99,145,579,580,584,585,586,587,589,590,591,592,593,594,595,596,597,598,602,603,604,605,606,607,608,609,610,611,612,613,614,616,617,618,619,620,621,622,624,625,626,627,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647],[87,99,145,586,644],[87,99,145,586,598],[87,99,145,586,605,609,610],[87,99,145,586,596,598],[87,99,145,586,601],[87,99,145,586,624],[87,99,145,586,601,628],[87,99,145,589,629],[87,99,145,583,584,585],[99,145,427,428],[99,145,426,429],[99,112,116,145,186],[99,112,145,175,186],[99,107,145],[99,109,112,145,183,186],[99,145,164,183],[99,107,145,193],[99,109,112,145,164,186],[99,104,105,108,111,145,156,175,186],[99,112,119,145],[99,104,110,145],[99,112,133,134,145],[99,108,112,145,178,186,193],[99,133,145,193],[99,106,107,145,193],[99,112,145],[99,106,107,108,109,110,111,112,113,114,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,134,135,136,137,138,139,145],[99,112,127,145],[99,112,119,120,145],[99,110,112,120,121,145],[99,111,145],[99,104,107,112,145],[99,112,116,120,121,145],[99,116,145],[99,110,112,115,145,186],[99,104,109,112,119,145],[99,145,175],[99,107,112,133,145,191,193],[99,145,582],[99,145,600],[87,99,145,389,649],[87,99,145,389,557,560,575,576,577],[87,99,145,557,657],[87,99,145,659],[87,99,145,668],[87,99,145,389,557,561,674,675],[87,99,145,681],[87,99,145,389,557,561,567,568],[87,99,145,557,684],[87,99,145,689],[87,99,145,557,693],[87,99,145,698],[87,99,145,389,702],[87,99,145,389,709],[87,99,145,557,561],[87,99,145,389,716],[99,145,399],[99,145,402,556,557,562],[99,145,561],[87,99,145,557,560,718,719],[99,145,383,561],[87,99,145,389,557,561],[87,99,145,561],[87,99,145,383,389,552,557,560,561],[87,99,145,560],[87,99,145,560,561],[87,99,145,406,552,560,561,570,648],[87,99,145,406,552,561,570,572,648],[87,99,145,406,552,560,561,562],[87,99,145,552,560,561,562],[87,99,145,406,552,561,570],[87,99,145,406,552,561,562,570,571,572,573,574],[87,99,145,552,560,561],[87,99,145,406,552,570,648],[87,99,145,406,561,570],[87,99,145,545,561,562,572],[87,99,145,552,560,561,562,572],[87,99,145,406,561,571],[87,99,145,561,572,664],[87,99,145,406,560,561],[87,99,145,406,552,553,561,570],[87,99,145,406,561],[87,99,145,561,570],[87,99,145,406,552,553,561,562,570,573,648,663,670,672,673],[87,99,145,406,552,553,561,562,570,573,648,663,670,671,672,673],[87,99,145,406,552,560,561,562,570,572,678],[87,99,145,406,552,553,561,562,570,571,572,573,577,663],[87,99,145,406,552,560,561,570,572],[87,99,145,561,711],[87,99,145,560,561,572],[87,99,145,406,570,707],[87,99,145,406,560,561,570],[87,99,145,406],[87,99,145,406,552,560,561,562,572],[87,99,145,545,552,561,562],[87,99,145,552,561,562],[87,99,145,389,406,552],[99,145,551],[99,145,406],[99,145,406,550],[87,99,145,406,552,560,561,570,572,651,652,653,654,655,656],[87,99,145,561,562,570,572],[87,99,145,406,552,553,560,561,562,570,573,661,662,663,665,666,667],[87,99,145,677,679,680],[87,99,145,406,552,553,562,573,663,705,706,708],[87,99,145,406,552,560,561,570,572,683],[87,99,145,406,552,553,560,561,562,570,573,663,666,686,687,688],[87,99,145,406,552,560,561,562,572,691,692],[87,99,145,406,552,560,561,570,695,696,697],[87,99,145,406,552,560,561,562,570,712,713,714,715],[87,99,145,406,552,561,700,701],[99,145,430]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"0990a7576222f248f0a3b888adcb7389f957928ce2afb1cd5128169086ff4d29","impliedFormat":1},{"version":"eb5b19b86227ace1d29ea4cf81387279d04bb34051e944bc53df69f58914b788","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"87d9d29dbc745f182683f63187bf3d53fd8673e5fca38ad5eaab69798ed29fbc","impliedFormat":1},{"version":"7a3aa194cfd5919c4da251ef04ea051077e22702638d4edcb9579e9101653519","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc69795d9954ee4ad57545b10c7bf1a7260d990231b1685c147ea71a6faa265c","impliedFormat":1},{"version":"8bc6c94ff4f2af1f4023b7bb2379b08d3d7dd80c698c9f0b07431ea16101f05f","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"57194e1f007f3f2cbef26fa299d4c6b21f4623a2eddc63dfeef79e38e187a36e","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"ba481bca06f37d3f2c137ce343c7d5937029b2468f8e26111f3c9d9963d6568d","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d9ef24f9a22a88e3e9b3b3d8c40ab1ddb0853f1bfbd5c843c37800138437b61","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"2cbe0621042e2a68c7cbce5dfed3906a1862a16a7d496010636cdbdb91341c0f","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2677634fe27e87348825bb041651e22d50a613e2fdf6a4a3ade971d71bac37e","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"8c0bcd6c6b67b4b503c11e91a1fb91522ed585900eab2ab1f61bba7d7caa9d6f","impliedFormat":1},{"version":"8cd19276b6590b3ebbeeb030ac271871b9ed0afc3074ac88a94ed2449174b776","affectsGlobalScope":true,"impliedFormat":1},{"version":"696eb8d28f5949b87d894b26dc97318ef944c794a9a4e4f62360cd1d1958014b","impliedFormat":1},{"version":"3f8fa3061bd7402970b399300880d55257953ee6d3cd408722cb9ac20126460c","impliedFormat":1},{"version":"35ec8b6760fd7138bbf5809b84551e31028fb2ba7b6dc91d95d098bf212ca8b4","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"68bd56c92c2bd7d2339457eb84d63e7de3bd56a69b25f3576e1568d21a162398","affectsGlobalScope":true,"impliedFormat":1},{"version":"3e93b123f7c2944969d291b35fed2af79a6e9e27fdd5faa99748a51c07c02d28","impliedFormat":1},{"version":"9d19808c8c291a9010a6c788e8532a2da70f811adb431c97520803e0ec649991","impliedFormat":1},{"version":"87aad3dd9752067dc875cfaa466fc44246451c0c560b820796bdd528e29bef40","impliedFormat":1},{"version":"4aacb0dd020eeaef65426153686cc639a78ec2885dc72ad220be1d25f1a439df","impliedFormat":1},{"version":"f0bd7e6d931657b59605c44112eaf8b980ba7f957a5051ed21cb93d978cf2f45","impliedFormat":1},{"version":"8db0ae9cb14d9955b14c214f34dae1b9ef2baee2fe4ce794a4cd3ac2531e3255","affectsGlobalScope":true,"impliedFormat":1},{"version":"15fc6f7512c86810273af28f224251a5a879e4261b4d4c7e532abfbfc3983134","impliedFormat":1},{"version":"58adba1a8ab2d10b54dc1dced4e41f4e7c9772cbbac40939c0dc8ce2cdb1d442","impliedFormat":1},{"version":"2fd4c143eff88dabb57701e6a40e02a4dbc36d5eb1362e7964d32028056a782b","impliedFormat":1},{"version":"714435130b9015fae551788df2a88038471a5a11eb471f27c4ede86552842bc9","impliedFormat":1},{"version":"855cd5f7eb396f5f1ab1bc0f8580339bff77b68a770f84c6b254e319bbfd1ac7","impliedFormat":1},{"version":"5650cf3dace09e7c25d384e3e6b818b938f68f4e8de96f52d9c5a1b3db068e86","impliedFormat":1},{"version":"1354ca5c38bd3fd3836a68e0f7c9f91f172582ba30ab15bb8c075891b91502b7","affectsGlobalScope":true,"impliedFormat":1},{"version":"27fdb0da0daf3b337c5530c5f266efe046a6ceb606e395b346974e4360c36419","impliedFormat":1},{"version":"2d2fcaab481b31a5882065c7951255703ddbe1c0e507af56ea42d79ac3911201","impliedFormat":1},{"version":"afbe24ab0d74694372baa632ecb28bb375be53f3be53f9b07ecd7fc994907de5","impliedFormat":1},{"version":"ca867399f7db82df981d6915bcbb2d81131d7d1ef683bc782b59f71dda59bc85","affectsGlobalScope":true,"impliedFormat":1},{"version":"00877fef624f3171c2e44944fb63a55e2a9f9120d7c8b5eb4181c263c9a077cf","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e043a1bc8fbf2a255bccf9bf27e0f1caf916c3b0518ea34aa72357c0afd42ec","impliedFormat":1},{"version":"b4f70ec656a11d570e1a9edce07d118cd58d9760239e2ece99306ee9dfe61d02","impliedFormat":1},{"version":"3bc2f1e2c95c04048212c569ed38e338873f6a8593930cf5a7ef24ffb38fc3b6","impliedFormat":1},{"version":"6e70e9570e98aae2b825b533aa6292b6abd542e8d9f6e9475e88e1d7ba17c866","impliedFormat":1},{"version":"f9d9d753d430ed050dc1bf2667a1bab711ccbb1c1507183d794cc195a5b085cc","impliedFormat":1},{"version":"9eece5e586312581ccd106d4853e861aaaa1a39f8e3ea672b8c3847eedd12f6e","impliedFormat":1},{"version":"47ab634529c5955b6ad793474ae188fce3e6163e3a3fb5edd7e0e48f14435333","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"0225ecb9ed86bdb7a2c7fd01f1556906902929377b44483dc4b83e03b3ef227d","affectsGlobalScope":true,"impliedFormat":1},{"version":"74cf591a0f63db318651e0e04cb55f8791385f86e987a67fd4d2eaab8191f730","impliedFormat":1},{"version":"5eab9b3dc9b34f185417342436ec3f106898da5f4801992d8ff38ab3aff346b5","impliedFormat":1},{"version":"12ed4559eba17cd977aa0db658d25c4047067444b51acfdcbf38470630642b23","affectsGlobalScope":true,"impliedFormat":1},{"version":"f3ffabc95802521e1e4bcba4c88d8615176dc6e09111d920c7a213bdda6e1d65","impliedFormat":1},{"version":"f9ab232778f2842ffd6955f88b1049982fa2ecb764d129ee4893cbc290f41977","impliedFormat":1},{"version":"ae56f65caf3be91108707bd8dfbccc2a57a91feb5daabf7165a06a945545ed26","impliedFormat":1},{"version":"a136d5de521da20f31631a0a96bf712370779d1c05b7015d7019a9b2a0446ca9","impliedFormat":1},{"version":"c3b41e74b9a84b88b1dca61ec39eee25c0dbc8e7d519ba11bb070918cfacf656","affectsGlobalScope":true,"impliedFormat":1},{"version":"4737a9dc24d0e68b734e6cfbcea0c15a2cfafeb493485e27905f7856988c6b29","affectsGlobalScope":true,"impliedFormat":1},{"version":"36d8d3e7506b631c9582c251a2c0b8a28855af3f76719b12b534c6edf952748d","impliedFormat":1},{"version":"1ca69210cc42729e7ca97d3a9ad48f2e9cb0042bada4075b588ae5387debd318","impliedFormat":1},{"version":"f5ebe66baaf7c552cfa59d75f2bfba679f329204847db3cec385acda245e574e","impliedFormat":1},{"version":"ed59add13139f84da271cafd32e2171876b0a0af2f798d0c663e8eeb867732cf","affectsGlobalScope":true,"impliedFormat":1},{"version":"05db535df8bdc30d9116fe754a3473d1b6479afbc14ae8eb18b605c62677d518","impliedFormat":1},{"version":"b1810689b76fd473bd12cc9ee219f8e62f54a7d08019a235d07424afbf074d25","impliedFormat":1},{"version":"8caa5c86be1b793cd5f599e27ecb34252c41e011980f7d61ae4989a149ff6ccc","impliedFormat":1},{"version":"91b0f6d01993021ecbe01eb076db6a3cf1b66359c1d99104f43436010e81afb5","impliedFormat":1},{"version":"d1bd4e51810d159899aad1660ccb859da54e27e08b8c9862b40cd36c1d9ff00f","impliedFormat":1},{"version":"17ed71200119e86ccef2d96b73b02ce8854b76ad6bd21b5021d4269bec527b5f","impliedFormat":1},{"version":"1cfa8647d7d71cb03847d616bd79320abfc01ddea082a49569fda71ac5ece66b","impliedFormat":1},{"version":"bb7a61dd55dc4b9422d13da3a6bb9cc5e89be888ef23bbcf6558aa9726b89a1c","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"cfe4ef4710c3786b6e23dae7c086c70b4f4835a2e4d77b75d39f9046106e83d3","impliedFormat":1},{"version":"cbea99888785d49bb630dcbb1613c73727f2b5a2cf02e1abcaab7bcf8d6bf3c5","impliedFormat":1},{"version":"98817124fd6c4f60e0b935978c207309459fb71ab112cf514f26f333bf30830e","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"2dad084c67e649f0f354739ec7df7c7df0779a28a4f55c97c6b6883ae850d1ce","impliedFormat":1},{"version":"fa5bbc7ab4130dd8cdc55ea294ec39f76f2bc507a0f75f4f873e38631a836ca7","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"cf86de1054b843e484a3c9300d62fbc8c97e77f168bbffb131d560ca0474d4a8","impliedFormat":1},{"version":"196c960b12253fde69b204aa4fbf69470b26daf7a430855d7f94107a16495ab0","impliedFormat":1},{"version":"528637e771ee2e808390d46a591eaef375fa4b9c99b03749e22b1d2e868b1b7c","impliedFormat":1},{"version":"bf24f6d35f7318e246010ffe9924395893c4e96d34324cde77151a73f078b9ad","impliedFormat":1},{"version":"596ccf4070268c4f5a8c459d762d8a934fa9b9317c7bf7a953e921bc9d78ce3c","impliedFormat":1},{"version":"10595c7ff5094dd5b6a959ccb1c00e6a06441b4e10a87bc09c15f23755d34439","impliedFormat":1},{"version":"9620c1ff645afb4a9ab4044c85c26676f0a93e8c0e4b593aea03a89ccb47b6d0","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"a9af0e608929aaf9ce96bd7a7b99c9360636c31d73670e4af09a09950df97841","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"08ed0b3f0166787f84a6606f80aa3b1388c7518d78912571b203817406e471da","impliedFormat":1},{"version":"47e5af2a841356a961f815e7c55d72554db0c11b4cba4d0caab91f8717846a94","impliedFormat":1},{"version":"9a1a0dc84fecc111e83281743f003e1ae9048e0f83c2ae2028d17bc58fd93cc7","impliedFormat":1},{"version":"f5f541902bf7ae0512a177295de9b6bcd6809ea38307a2c0a18bfca72212f368","impliedFormat":1},{"version":"e8da637cbd6ed1cf6c36e9424f6bcee4515ca2c677534d4006cbd9a05f930f0c","impliedFormat":1},{"version":"ca1b882a105a1972f82cc58e3be491e7d750a1eb074ffd13b198269f57ed9e1b","impliedFormat":1},{"version":"fc3e1c87b39e5ba1142f27ec089d1966da168c04a859a4f6aab64dceae162c2b","impliedFormat":1},{"version":"3867ca0e9757cc41e04248574f4f07b8f9e3c0c2a796a5eb091c65bfd2fc8bdb","impliedFormat":1},{"version":"61888522cec948102eba94d831c873200aa97d00d8989fdfd2a3e0ee75ec65a2","impliedFormat":1},{"version":"4e10622f89fea7b05dd9b52fb65e1e2b5cbd96d4cca3d9e1a60bb7f8a9cb86a1","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"59bf32919de37809e101acffc120596a9e45fdbab1a99de5087f31fdc36e2f11","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"5322a4f762d356093e8c2903a190ddbaa548420c891affa71093b85c5c2fbe94","impliedFormat":1},{"version":"c40c848daad198266370c1c72a7a8c3d18d2f50727c7859fcfefd3ff69a7f288","impliedFormat":1},{"version":"ac60bbee0d4235643cc52b57768b22de8c257c12bd8c2039860540cab1fa1d82","impliedFormat":1},{"version":"973b59a17aaa817eb205baf6c132b83475a5c0a44e8294a472af7793b1817e89","impliedFormat":1},{"version":"ada39cbb2748ab2873b7835c90c8d4620723aedf323550e8489f08220e477c7f","impliedFormat":1},{"version":"6e5f5cee603d67ee1ba6120815497909b73399842254fc1e77a0d5cdc51d8c9c","impliedFormat":1},{"version":"8dba67056cbb27628e9b9a1cba8e57036d359dceded0725c72a3abe4b6c79cd4","impliedFormat":1},{"version":"70f3814c457f54a7efe2d9ce9d2686de9250bb42eb7f4c539bd2280a42e52d33","impliedFormat":1},{"version":"5cbd32af037805215112472e35773bad9d4e03f0e72b1129a0d0c12d9cd63cc7","impliedFormat":1},{"version":"ef61792acbfa8c27c9bd113f02731e66229f7d3a169e3c1993b508134f1a58e0","impliedFormat":1},{"version":"afcb759e8e3ad6549d5798820697002bc07bdd039899fad0bf522e7e8a9f5866","impliedFormat":1},{"version":"f6404e7837b96da3ea4d38c4f1a3812c96c9dcdf264e93d5bdb199f983a3ef4b","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"566e5fb812082f8cf929c6727d40924843246cf19ee4e8b9437a6315c4792b03","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"903e299a28282fa7b714586e28409ed73c3b63f5365519776bf78e8cf173db36","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"dd3900b24a6a8745efeb7ad27629c0f8a626470ac229c1d73f1fe29d67e44dca","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"ec29be0737d39268696edcec4f5e97ce26f449fa9b7afc2f0f99a86def34a418","impliedFormat":1},{"version":"68a06fb972b2c7e671bf090dc5a5328d22ba07d771376c3d9acd9e7ed786a9db","impliedFormat":1},{"version":"ec6cba1c02c675e4dd173251b156792e8d3b0c816af6d6ad93f1a55d674591aa","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"d729408dfde75b451530bcae944cf89ee8277e2a9df04d1f62f2abfd8b03c1e1","impliedFormat":1},{"version":"e15d3c84d5077bb4a3adee4c791022967b764dc41cb8fa3cfa44d4379b2c95f5","impliedFormat":1},{"version":"78244a2a8ab1080e0dd8fc3633c204c9a4be61611d19912f4b157f7ef7367049","impliedFormat":1},{"version":"e1fc1a1045db5aa09366be2b330e4ce391550041fc3e925f60998ca0b647aa97","impliedFormat":1},{"version":"d3f5861c48322adc023d3277e592635402ac008c5beae2e447b335fbf0da56c2","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"31fb49ef3aa3d76f0beb644984e01eab0ea222372ea9b49bb6533be5722d756c","impliedFormat":1},{"version":"44c431d059195791915a2aee58f5cef4b333b9996bd256ae55f82ffcf3aa6de4","impliedFormat":1},{"version":"3556cfbab7b43da96d15a442ddbb970e1f2fc97876d055b6555d86d7ac57dae5","impliedFormat":1},{"version":"437751e0352c6e924ddf30e90849f1d9eb00ca78c94d58d6a37202ec84eb8393","impliedFormat":1},{"version":"48e8af7fdb2677a44522fd185d8c87deff4d36ee701ea003c6c780b1407a1397","impliedFormat":1},{"version":"24e1319bd695b0971f0eec3d4e011b7aa996f77b9d854d2703657cf355cac2a7","impliedFormat":1},{"version":"f9812cfc220ecf7557183379531fa409acd249b9e5b9a145d0d52b76c20862de","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b068371563d0396a065ed64b049cffeb4eed89ad433ae7730fc31fb1e00ebf3","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"680793958f6a70a44c8d9ae7d46b7a385361c69ac29dcab3ed761edce1c14ab8","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"913ddbba170240070bd5921b8f33ea780021bdf42fbdfcd4fcb2691b1884ddde","impliedFormat":1},{"version":"74c105214ddd747037d2a75da6588ec8aa1882f914e1f8a312c528f86feca2b9","impliedFormat":1},{"version":"5fe23bd829e6be57d41929ac374ee9551ccc3c44cee893167b7b5b77be708014","impliedFormat":1},{"version":"4d85f80132e24d9a5b5c5e0734e4ecd6878d8c657cc990ecc70845ef384ca96f","impliedFormat":1},{"version":"438c7513b1df91dcef49b13cd7a1c4720f91a36e88c1df731661608b7c055f10","impliedFormat":1},{"version":"ad444a874f011d3a797f1a41579dbfcc6b246623f49c20009f60e211dbd5315e","impliedFormat":1},{"version":"efaa078e392f9abda3ee8ade3f3762ab77f9c50b184e6883063a911742a4c96a","impliedFormat":1},{"version":"54a8bb487e1dc04591a280e7a673cdfb272c83f61e28d8a64cf1ac2e63c35c51","impliedFormat":1},{"version":"021a9498000497497fd693dd315325484c58a71b5929e2bbb91f419b04b24cea","impliedFormat":1},{"version":"9385cdc09850950bc9b59cca445a3ceb6fcca32b54e7b626e746912e489e535e","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"84124384abae2f6f66b7fbfc03862d0c2c0b71b826f7dbf42c8085d31f1d3f95","impliedFormat":1},{"version":"63a8e96f65a22604eae82737e409d1536e69a467bb738bec505f4f97cce9d878","impliedFormat":1},{"version":"3fd78152a7031315478f159c6a5872c712ece6f01212c78ea82aef21cb0726e2","impliedFormat":1},{"version":"3a6ed8e1d630cfa1f7edf0dc46a6e20ca6c714dbe754409699008571dfe473a6","impliedFormat":1},{"version":"512fc15cca3a35b8dbbf6e23fe9d07e6f87ad03c895acffd3087ce09f352aad0","impliedFormat":1},{"version":"9a0946d15a005832e432ea0cd4da71b57797efb25b755cc07f32274296d62355","impliedFormat":1},{"version":"a52ff6c0a149e9f370372fc3c715d7f2beee1f3bab7980e271a7ab7d313ec677","impliedFormat":1},{"version":"fd933f824347f9edd919618a76cdb6a0c0085c538115d9a287fa0c7f59957ab3","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"6a1aa3e55bdc50503956c5cd09ae4cd72e3072692d742816f65c66ca14f4dfdd","impliedFormat":1},{"version":"ab75cfd9c4f93ffd601f7ca1753d6a9d953bbedfbd7a5b3f0436ac8a1de60dfa","impliedFormat":1},{"version":"59c68235df3905989afa0399381c1198313aaaf1ed387f57937eb616625dff15","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"1364f64d2fb03bbb514edc42224abd576c064f89be6a990136774ecdd881a1da","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"950fb67a59be4c2dbe69a5786292e60a5cb0e8612e0e223537784c731af55db1","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"07ca44e8d8288e69afdec7a31fa408ce6ab90d4f3d620006701d5544646da6aa","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"4e4475fba4ed93a72f167b061cd94a2e171b82695c56de9899275e880e06ba41","impliedFormat":1},{"version":"97c5f5d580ab2e4decd0a3135204050f9b97cd7908c5a8fbc041eadede79b2fa","impliedFormat":1},{"version":"49b2375c586882c3ac7f57eba86680ff9742a8d8cb2fe25fe54d1b9673690d41","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"0ccace0fe001af57d74279b66fa35cff2bb6aefc57c5c080be7281474161447b","impliedFormat":1},{"version":"3ecfccf916fea7c6c34394413b55eb70e817a73e39b4417d6573e523784e3f8e","impliedFormat":1},{"version":"c05bc82af01e673afc99bdffd4ebafde22ab027d63e45be9e1f1db3bc39e2fc0","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"f416c9c3eee9d47ff49132c34f96b9180e50485d435d5748f0e8b72521d28d2e","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"14e5cdec6f8ae82dfd0694e64903a0a54abdfe37e1d966de3d4128362acbf35f","impliedFormat":1},{"version":"bbc183d2d69f4b59fd4dd8799ffdf4eb91173d1c4ad71cce91a3811c021bf80c","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"8dbc4134a4b3623fc476be5f36de35c40f2768e2e3d9ed437e0d5f1c4cd850f6","impliedFormat":1},{"version":"4e06330a84dec7287f7ebdd64978f41a9f70a668d3b5edc69d5d4a50b9b376bb","impliedFormat":1},{"version":"65bfa72967fbe9fc33353e1ac03f0480aa2e2ea346d61ff3ea997dfd850f641a","impliedFormat":1},{"version":"8f88c6be9803fe5aaa80b00b27f230c824d4b8a33856b865bea5793cb52bb797","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"872caaa31423f4345983d643e4649fb30f548e9883a334d6d1c5fff68ede22d4","impliedFormat":1},{"version":"94404c4a878fe291e7578a2a80264c6f18e9f1933fbb57e48f0eb368672e389c","impliedFormat":1},{"version":"5c1b7f03aa88be854bc15810bfd5bd5a1943c5a7620e1c53eddd2a013996343e","impliedFormat":1},{"version":"09dfc64fcd6a2785867f2368419859a6cc5a8d4e73cbe2538f205b1642eb0f51","impliedFormat":1},{"version":"bcf6f0a323653e72199105a9316d91463ad4744c546d1271310818b8cef7c608","impliedFormat":1},{"version":"01aa917531e116485beca44a14970834687b857757159769c16b228eb1e49c5f","impliedFormat":1},{"version":"351475f9c874c62f9b45b1f0dc7e2704e80dfd5f1af83a3a9f841f9dfe5b2912","impliedFormat":1},{"version":"ac457ad39e531b7649e7b40ee5847606eac64e236efd76c5d12db95bf4eacd17","impliedFormat":1},{"version":"187a6fdbdecb972510b7555f3caacb44b58415da8d5825d03a583c4b73fde4cf","impliedFormat":1},{"version":"d4c3250105a612202289b3a266bb7e323db144f6b9414f9dea85c531c098b811","impliedFormat":1},{"version":"95b444b8c311f2084f0fb51c616163f950fb2e35f4eaa07878f313a2d36c98a4","impliedFormat":1},{"version":"741067675daa6d4334a2dc80a4452ca3850e89d5852e330db7cb2b5f867173b1","impliedFormat":1},{"version":"f8acecec1114f11690956e007d920044799aefeb3cece9e7f4b1f8a1d542b2c9","impliedFormat":1},{"version":"131b1475d2045f20fb9f43b7aa6b7cb51f25250b5e4c6a1d4aa3cf4dd1a68793","impliedFormat":1},{"version":"3a17f09634c50cce884721f54fd9e7b98e03ac505889c560876291fcf8a09e90","impliedFormat":1},{"version":"32531dfbb0cdc4525296648f53b2b5c39b64282791e2a8c765712e49e6461046","impliedFormat":1},{"version":"0ce1b2237c1c3df49748d61568160d780d7b26693bd9feb3acb0744a152cd86d","impliedFormat":1},{"version":"e489985388e2c71d3542612685b4a7db326922b57ac880f299da7026a4e8a117","impliedFormat":1},{"version":"e1437c5f191edb7a494f7bbbc033b97d72d42e054d521402ee194ac5b6b7bf49","impliedFormat":1},{"version":"04d3aad777b6af5bd000bfc409907a159fe77e190b9d368da4ba649cdc28d39e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fd1b9d883b9446f1e1da1e1033a6a98995c25fbf3c10818a78960e2f2917d10c","impliedFormat":1},{"version":"19252079538942a69be1645e153f7dbbc1ef56b4f983c633bf31fe26aeac32cd","impliedFormat":1},{"version":"bc11f3ac00ac060462597add171220aed628c393f2782ac75dd29ff1e0db871c","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"313c85c332bb6892d5f7c624dc39107ca7a6b2f1b3212db86dbbefbe7f8ddd5a","impliedFormat":1},{"version":"3b0b1d352b8d2e47f1c4df4fb0678702aee071155b12ef0185fce9eb4fa4af1e","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"a344403e7a7384e0e7093942533d309194ad0a53eca2a3100c0b0ab4d3932773","impliedFormat":1},{"version":"b7fff2d004c5879cae335db8f954eb1d61242d9f2d28515e67902032723caeab","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb18bf4a61a17b4a6199eb3938ecfa4a59eb7c40843ad4a82b975ab6f7e3d925","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"e9b6fc05f536dfddcdc65dbcf04e09391b1c968ab967382e48924f5cb90d88e1","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"2b664c3cc544d0e35276e1fb2d4989f7d4b4027ffc64da34ec83a6ccf2e5c528","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"3cd8f0464e0939b47bfccbb9bb474a6d87d57210e304029cd8eb59c63a81935d","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"3026abd48e5e312f2328629ede6e0f770d21c3cd32cee705c450e589d015ee09","impliedFormat":1},{"version":"8b140b398a6afbd17cc97c38aea5274b2f7f39b1ae5b62952cfe65bf493e3e75","impliedFormat":1},{"version":"7663d2c19ce5ef8288c790edba3d45af54e58c84f1b37b1249f6d49d962f3d91","impliedFormat":1},{"version":"30112425b2cf042fca1c79c19e35f88f44bfb2e97454527528cd639dd1a460ca","impliedFormat":1},{"version":"00bd6ebe607246b45296aa2b805bd6a58c859acecda154bfa91f5334d7c175c6","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"504f37ba38bfea8394ec4f397c9a2ade7c78055e41ef5a600073b515c4fd0fc9","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"87ac2fb61e629e777f4d161dff534c2023ee15afd9cb3b1589b9b1f014e75c58","impliedFormat":1},{"version":"13c8b4348db91e2f7d694adc17e7438e6776bc506d5c8f5de9ad9989707fa3fe","impliedFormat":1},{"version":"3c1051617aa50b38e9efaabce25e10a5dd9b1f42e372ef0e8a674076a68742ed","impliedFormat":1},{"version":"07a3e20cdcb0f1182f452c0410606711fbea922ca76929a41aacb01104bc0d27","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"4cd4b6b1279e9d744a3825cbd7757bbefe7f0708f3f1069179ad535f19e8ed2c","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"c0eeaaa67c85c3bb6c52b629ebbfd3b2292dc67e8c0ffda2fc6cd2f78dc471e6","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"b95a6f019095dd1d48fd04965b50dfd63e5743a6e75478343c46d2582a5132bf","impliedFormat":99},{"version":"c2008605e78208cfa9cd70bd29856b72dda7ad89df5dc895920f8e10bcb9cd0a","impliedFormat":99},{"version":"b97cb5616d2ab82a98ec9ada7b9e9cabb1f5da880ec50ea2b8dc5baa4cbf3c16","impliedFormat":99},{"version":"d23df9ff06ae8bf1dcb7cc933e97ae7da418ac77749fecee758bb43a8d69f840","affectsGlobalScope":true,"impliedFormat":1},{"version":"040c71dde2c406f869ad2f41e8d4ce579cc60c8dbe5aa0dd8962ac943b846572","affectsGlobalScope":true,"impliedFormat":1},{"version":"3586f5ea3cc27083a17bd5c9059ede9421d587286d5a47f4341a4c2d00e4fa91","impliedFormat":1},{"version":"a6df929821e62f4719551f7955b9f42c0cd53c1370aec2dd322e24196a7dfe33","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"8964d295a9047c3a222af813b7d37deb57b835fd0942d89222e7def0aed136cc","260a7259ae9e903262261776c083231b8f3f635567bb44342da042782f0ea17f","f83fc447866ac8df284a1395fd76eda881a1862ad8af42c218eead80d82ab02c",{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"8885cf05f3e2abf117590bbb951dcf6359e3e5ac462af1c901cfd24c6a6472e2","impliedFormat":1},{"version":"333caa2bfff7f06017f114de738050dd99a765c7eb16571c6d25a38c0d5365dc","impliedFormat":1},{"version":"e61df3640a38d535fd4bc9f4a53aef17c296b58dc4b6394fd576b808dd2fe5e6","impliedFormat":1},{"version":"459920181700cec8cbdf2a5faca127f3f17fd8dd9d9e577ed3f5f3af5d12a2e4","impliedFormat":1},{"version":"4719c209b9c00b579553859407a7e5dcfaa1c472994bd62aa5dd3cc0757eb077","impliedFormat":1},{"version":"7ec359bbc29b69d4063fe7dad0baaf35f1856f914db16b3f4f6e3e1bca4099fa","impliedFormat":1},{"version":"70790a7f0040993ca66ab8a07a059a0f8256e7bb57d968ae945f696cbff4ac7a","impliedFormat":1},{"version":"d1b9a81e99a0050ca7f2d98d7eedc6cda768f0eb9fa90b602e7107433e64c04c","impliedFormat":1},{"version":"a022503e75d6953d0e82c2c564508a5c7f8556fad5d7f971372d2d40479e4034","impliedFormat":1},{"version":"b215c4f0096f108020f666ffcc1f072c81e9f2f95464e894a5d5f34c5ea2a8b1","impliedFormat":1},{"version":"644491cde678bd462bb922c1d0cfab8f17d626b195ccb7f008612dc31f445d2d","impliedFormat":1},{"version":"dfe54dab1fa4961a6bcfba68c4ca955f8b5bbeb5f2ab3c915aa7adaa2eabc03a","impliedFormat":1},{"version":"1251d53755b03cde02466064260bb88fd83c30006a46395b7d9167340bc59b73","impliedFormat":1},{"version":"47865c5e695a382a916b1eedda1b6523145426e48a2eae4647e96b3b5e52024f","impliedFormat":1},{"version":"4cdf27e29feae6c7826cdd5c91751cc35559125e8304f9e7aed8faef97dcf572","impliedFormat":1},{"version":"331b8f71bfae1df25d564f5ea9ee65a0d847c4a94baa45925b6f38c55c7039bf","impliedFormat":1},{"version":"2a771d907aebf9391ac1f50e4ad37952943515eeea0dcc7e78aa08f508294668","impliedFormat":1},{"version":"0146fd6262c3fd3da51cb0254bb6b9a4e42931eb2f56329edd4c199cb9aaf804","impliedFormat":1},{"version":"183f480885db5caa5a8acb833c2be04f98056bdcc5fb29e969ff86e07efe57ab","impliedFormat":99},{"version":"b558c9a18ea4e6e4157124465c3ef1063e64640da139e67be5edb22f534f2f08","impliedFormat":1},{"version":"01374379f82be05d25c08d2f30779fa4a4c41895a18b93b33f14aeef51768692","impliedFormat":1},{"version":"b0dee183d4e65cf938242efaf3d833c6b645afb35039d058496965014f158141","impliedFormat":1},{"version":"c0bbbf84d3fbd85dd60d040c81e8964cc00e38124a52e9c5dcdedf45fea3f213","impliedFormat":1},"f992d9a17647515c5dbbf0ef1276b2dda6b08e266689e9dd277255ec9261d72b",{"version":"d34aa8df2d0b18fb56b1d772ff9b3c7aea7256cf0d692f969be6e1d27b74d660","impliedFormat":1},{"version":"baac9896d29bcc55391d769e408ff400d61273d832dd500f21de766205255acb","impliedFormat":1},{"version":"2f5747b1508ccf83fad0c251ba1e5da2f5a30b78b09ffa1cfaf633045160afed","impliedFormat":1},{"version":"f429b61e369208ef7668ebf1dc63176e506fbfaea7b0ecc13d586a5839ebb071","affectsGlobalScope":true,"impliedFormat":1},"4654f92731de7c18eb496ebf4134d15c881a25c2d6579affd293cde1951e4ced",{"version":"b71c603a539078a5e3a039b20f2b0a0d1708967530cf97dec8850a9ca45baa2b","impliedFormat":1},{"version":"d3f2d715f57df3f04bf7b16dde01dec10366f64fce44503c92b8f78f614c1769","impliedFormat":1},{"version":"cb90077223cc1365fa21ef0911a1f9b8f2f878943523d97350dc557973ca3823","impliedFormat":1},{"version":"18f1541b81b80d806120a3489af683edfb811deb91aeca19735d9bb2613e6311","impliedFormat":1},{"version":"104c67f0da1bdf0d94865419247e20eded83ce7f9911a1aa75fc675c077ca66e","impliedFormat":1},{"version":"cc0d0b339f31ce0ab3b7a5b714d8e578ce698f1e13d7f8c60bfb766baeb1d35c","impliedFormat":1},{"version":"8e04cf0688e0d921111659c2b55851957017148fa7b977b02727477d155b3c47","impliedFormat":1},"80351f50586b0d4119e7d646912b2fe70281a6219dcc2c6a2a2a5c6bc17ea560","8f55f6078d7ca1aa0bb6684a7af6219e2d92b9c6bc2951e7ed8de5fd019d521d","cb3fb64124dadf6762e956a7eb150596e9d50d2aed9acb769d96b01f7bd151f0","388c06a14d4f3dcbf500d28896bbfe4331af0aa2712f0683a06556dec8757cdc","83625f511fb9db262c026effbdd424a04f5d0e7f2bf48096ff5b36ed8b7f9422","66c9bb45f19a6d55118eda02ff96ef0eae0213f33b08f322ce7c62dfbcdde969","e182734a7e27f4862cd496426d60163488acb5899f5d7520f6d6b957ef28baf3","19e817586bb2e2c1ced4aea83c4924867326ba71e373061c68f68655f79d9dad","3a769ed05b87282b728f3539f5a90caba250dcc3793844f028444b2b46ecf980","226b511d19a9d186e304e1a06224497279c86e6d4f03323c8e9db608650d4bc8","15fc6f57e8b9283134607f19924a0c6d92e2fe7e5212937dd347e3d29ba1d12c","147efce4f92e1cb1feda8c518704b11d9e6a2e0682a9fb83f52dc70e77d039ea","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","63b51e68ea8ca96777bf13aa74bf9e1f188a741c089e91383eab6db3825711e6",{"version":"21247c958d397091ec30e63b27294baa1d1434c333da4fda697743190311dc62","impliedFormat":1},{"version":"1bf192f8aeb5b71917cf3e7d78849f0d4cfd94cfef401eebeefd2551349c2aa2","impliedFormat":1},"9d144d2b246c9607994c845cea78d594e32289d0d364ea5bbe997855e08c7980","7fe15e15f87ab9b60e324dd026cf479be49f65fd5f8da8ef93e76b5021a4f2df","eb03b345da9a347b1ac051b89c8fa2cd89168babace82cc77d19effd46a27937","3755320b091b1d9082c073d73eaf696d831e42aa843f58e501d3b07ff00a4ea1","7a88eeb73342261b082bc97477abd4dfe43cc308a47672d91250ff1570c0c0e2","0e0a0147928d512e70ba8e06646d8024f3592f32b89e34fc1ba67d41b67dce9d",{"version":"d5eb5865d4cbaa9985cc3cfb920b230cdcf3363f1e70903a08dc4baab80b0ce1","impliedFormat":1},{"version":"51ebca098538b252953b1ef83c165f25b52271bfb6049cd09d197dddd4cd43c5","impliedFormat":1},"eeea430937b49b47f0a204a8332056d5ed2004d97b3edcf249538672973a77a8","bc5b2f51b8bfc14772763a91ff97e2dca781bdb3abdf6d24c6b06c30b1e44bc7","ae738a8389710541d74dedbce8f10350d18b08ad34e7874385b7c92d9b697afb","77420e2625d991cd2feb48cdc957a58ab57dd97042d14147f93c62c80a56b6c0","6fb8fb352b2889d82189d19848e5a09694f12078700af9c052fa900a32799d7f","15c11e844c8303676112ae11666da9f283e86457e91dc875b27774168ca5f7e9",{"version":"a3d3f704c5339a36da3ca8c62b29072f87e86c783b8452d235992142ec71aa2d","impliedFormat":1},{"version":"cff399d99c68e4fafdd5835d443a980622267a39ac6f3f59b9e3d60d60c4f133","impliedFormat":99},{"version":"6ada175c0c585e89569e8feb8ff6fc9fc443d7f9ca6340b456e0f94cbef559bf","impliedFormat":99},{"version":"e56e4d95fad615c97eb0ae39c329a4cda9c0af178273a9173676cc9b14b58520","impliedFormat":99},{"version":"73e8dfd5e7d2abc18bdb5c5873e64dbdd1082408dd1921cad6ff7130d8339334","impliedFormat":99},{"version":"fc820b2f0c21501f51f79b58a21d3fa7ae5659fc1812784dbfbb72af147659ee","impliedFormat":99},{"version":"4f041ef66167b5f9c73101e5fd8468774b09429932067926f9b2960cc3e4f99d","impliedFormat":99},{"version":"31501b8fc4279e78f6a05ca35e365e73c0b0c57d06dbe8faecb10c7254ce7714","impliedFormat":99},{"version":"7bc76e7d4bbe3764abaf054aed3a622c5cdbac694e474050d71ce9d4ab93ea4b","impliedFormat":99},{"version":"ff4e9db3eb1e95d7ba4b5765e4dc7f512b90fb3b588adfd5ca9b0d9d7a56a1ae","impliedFormat":99},{"version":"f205fd03cd15ea054f7006b7ef8378ef29c315149da0726f4928d291e7dce7b9","impliedFormat":99},{"version":"d683908557d53abeb1b94747e764b3bd6b6226273514b96a942340e9ce4b7be7","impliedFormat":99},{"version":"7c6d5704e2f236fddaf8dbe9131d998a4f5132609ef795b78c3b63f46317f88a","impliedFormat":99},{"version":"d05bd4d28c12545827349b0ac3a79c50658d68147dad38d13e97e22353544496","impliedFormat":99},{"version":"b6436d90a5487d9b3c3916b939f68e43f7eaca4b0bb305d897d5124180a122b9","impliedFormat":99},{"version":"04ace6bedd6f59c30ea6df1f0f8d432c728c8bc5c5fd0c5c1c80242d3ab51977","impliedFormat":99},{"version":"57a8a7772769c35ba7b4b1ba125f0812deec5c7102a0d04d9e15b1d22880c9e8","impliedFormat":99},{"version":"badcc9d59770b91987e962f8e3ddfa1e06671b0e4c5e2738bbd002255cad3f38","impliedFormat":99},"3a22aa9accb2234d0fdc291c053b66bce0f461c5e47ef9a924a3bfe0d6358213",{"version":"25be1eb939c9c63242c7a45446edb20c40541da967f43f1aa6a00ed53c0552db","impliedFormat":1},{"version":"0c5f112b6d3377b9e8214d8920e1a69d8098b881d941f2ab3ca45234d13d68de","impliedFormat":1},{"version":"fb893a0dfc3c9fb0f9ca93d0648694dd95f33cbad2c0f2c629f842981dfd4e2e","impliedFormat":1},{"version":"95da3c365e3d45709ad6e0b4daa5cdaf05e9076ba3c201e8f8081dd282c02f57","impliedFormat":1},"ae32eba87d67d7b02f257e45c0a277e55568a0c7f79197038174ea6adf370332","23c9425d5ee62a31ed071a8188d2070cf07a283b64831e9b95ca8451c2e26cc0","912aa38f77b3aaf0b2de06910c26dde3ad162423b5e23efd5f1aa3f4f886836d","2c2de810a576fca480d47f7812d4adcee286b9ae7d1788b6dac8771aaf57ded7","b42fe49c7eaffbab8cb8a6136bd1c48b476216db25b51d0de908fe4c1eb1e2c5","f5679d099c531abd32276065014cc8cb3ed9491182be9e3716bd6b5bbb7a3cf9","dfe55a4c9b2d3272e85110885bb9a60d443ad80ffc557bb6855090e43e0be322","f3c52df32f1bb17fdd488533c66ad538ef2e18ee237e23158025c854104eb779","9e6ffabd6c7d65fa128a1dfa84b2220b36b6a7965d686cf940a1fa7ff4483be9","02d86a10bde87ca26412ef3364a5ad1f14e091cd5ac872c9efbf971aba404621","5f365b61ad798120b79bb402bcfe3652ecdf02cc5aff802990dec031fb2306ab","b45c98aa3d226cead3316abe9bee92e2661ca75161e37bacb4f7558f95acdddb","31ed03e56b40fbbf938d6210fff05ea4ef16dec01b6257fe4750bea06b8ac30e","d947c6bdefee7aeccd55ae78a7e1679b46ac8dc81d0d509dcfc7644c7917ffab","f5413efbc0d3320ac8ea83b6ceb204c0aff47a17e0c38ea571d3c19d157e4760","1f59548eeb8a069996dcb7fc51461f093bb222f19db997acf43717d78246c707","0f3e5658cbde693d0b3e0f218912eb6e188898bef6788a7860f20546b33bb1c5","53883c08a4c2c6ba01856f480fc10df75274829a23ac2ba29ffcf12e3ef7fedf","021500adc07ad659fb61e4ce9164d880c5d731f3ceee49309e49c93c6ad87e14","c9c829f4c873eb46e5c148f99e23954ad3bd187d2f57804598828e8037eb118e","5c9cfcec4d1fd23441eabc602308d0ee6232fd93b77b55bf7a66645f9157a71c","dafcd5b6d24e431de49f9546851b9557fbcf87d11600525f5a2a3fbd224498fb","10bca34725a1bc118bbd53d629d846faeb62af7f0aa31e5f3b67f3c968e46707","588d72dd31ea69dfdd01e1cec84e40c68d31efb99fd903ac5b940d5b7c5246eb","4c9e04638fab9dc348be0eaac6022008e153a91351c1d4c46acea9c0fef2c2e6","5acd07c7cddbda0c13b9f20639467db21426faad0c8448bd45fc4d51c075a8e5","128b697e0a2e06df4f6de293fa774d5379183eb106aa6a404173d09d17888530","2182c00ee4f8b0a51ced6b0f80383c45bfd47e65294cd3217ae8161e2c470fa8","ca104e6a88f029cf7064f4923330624df37e8a00399bceecc9139b88ecb9d4d3","6e27819c761a28c424097182bec05dc6e6470c4b17bde53332aa399ef372f50f","01bf41ef0d8c97adeb8210a40ea87300ec9f5dc3f32b1c1e79e098f6f6882c14","502b21ad08b966f0b4ac40667f882ec7649eb440c0a0f1231eb8316da5cb7eb7","d63ede6e90b20a880463506104a952ae38e13f5a10c6f6e7b8342e708fd247ff","d64b6e947eefa0cf4f4a43f155cc5a8c595f9172073fb36a3f7027c35fc602ee","749a1ebe5e45a52a38426721d781f6092069a1f1eef7fc85fdbde6fab32b93ce","f9137b11b0a6f5462155d9f5fc12f7b679057600d0ed8cb72450ce286ae6df42","2130fc57b5cbee5dbfe656dfaec84e4cdd4903295f4e8fe193b5b396deb6abd6","a0b40e6cbc881d9f1d0e813710b74dd57b537912a45abf7fd08e9606ad6c329c","5f964bc2b67d9215bc31f0e0a932f3a4c6264a4bd57d6babb3486da76fab91db","9634d466b69ce6f07c68b5051e7a5d38506137a07b43e2f066ee105a1e592fbb","f994eb7e77b7347cd0b0f70e27f8787bfe0f48f40953499a94fa580ee22b6619","4b95066e5293fbeaf9f949cf57d3041e70461decf7dbecc04a5367e55094064d","4042d962a25770f6b2b9930d6cd4bfdf5c41633d28a9dcbdeb3222ee580f8b70","b18feec0d6c7661411e3bc6d7144c0de17eafeab58035da7cc81623ed113b763","6b8ea1d3b48b128a4697135a3fdae9bb6ab27a5d9c972aea3be04b122511093e","fc2e3481afb1f44df19a1f4c9b8f24087add3e026d83faf6c31d3dfa7b53eefb","9fc0e9d4979cc4e637313254bdf61dda6276a0eaf70a4eafea51bdcec5fcd2b5",{"version":"fe93c474ab38ac02e30e3af073412b4f92b740152cf3a751fdaee8cbea982341","impliedFormat":1},{"version":"999eca296f47822e24c3d5f92a77ce99b7b3da008ef210f973d540105197ff4c","impliedFormat":1},{"version":"1e00b8bf9e3766c958218cd6144ffe08418286f89ff44ba5a2cc830c03dd22c7","impliedFormat":1},"be62d77714a5db9f108d14a5d6fd23cac9b9694253af1a4901d2d0d86231a083",{"version":"38479e9851ea5f43f60baaa6bc894a49dba0a74dd706ce592d32bcb8b59e3be9","affectsGlobalScope":true,"impliedFormat":1},{"version":"9592f843d45105b9335c4cd364b9b2562ce4904e0895152206ac4f5b2d1bb212","impliedFormat":1},{"version":"f9ff719608ace88cae7cb823f159d5fb82c9550f2f7e6e7d0f4c6e41d4e4edb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc931dba4321d011bba1a381ec3dc77f033c229ab798f00cab6d54cf05f4c060","impliedFormat":1},"06f0047619fb1f4072c17b8ab87c565f500eda3c7932dfbde13f121ca7f789e5","6daca400a86ed1cdee56a2e59610ee10d022de8ef3aeff31ee87004d9365bd34","fedd91c10dda6e69bc1adf90ab5772db421bea4475f42f21d604f932c21014c6","6d3de4aaf40b4ba257d8ad06f2b0f60cbf5b9ed4c97261755dead40fffc93d23","bdd8814d32a604e3e0d0a287cf35cbaaf74a9b8ea97abf2ccb81112b178059e4","f2c1113429be2d8a37acc357006f6355ec29e11f2f92eb218a1d8e9a54ee0071","6db7cb75341e1ab6bc442790e82b6ded50a16e622b9a3da31afbc243029711df","4846d1321eb5753ff2e629874ab169c657f005182afc021ce41427c84fa426f3","17efb27147eb247ddcb60329072586115a8ee6bfafc87462a288bd86f0d135af","13e1e74378e30d6f542f7b552f9651b5546e1af1b953cfffab86779bed5df3e0","1c1079081e9a2fa9aadb69198fbeae61c735f1f830be86171849f131d274725c","0102819dbfd8540f0d1244ae696dad642b4bad4c09f690bdf4f92041944d351e","0140c3c20576e8b18836352926664d142a81bda0e0bef21a25678824c5202eca","0b9369eacc2b8f4326df0dfa3489ad0d60a1ccfc9ccb32603ce17f9e0bca9690","0ddaa90325e37f48f70355bcc7caf6c4a873d001a9e53b2ba438f37ace8e7c22","2cd888424efab99f9582e3baef0df14451f9f5a51ebfa23c6c7f7bea54661d0a","1a0e26a46dbc9344ecb37cd3b079e7bdc627e1a9abb74a202750088a62fe5edd",{"version":"7e3373dde2bba74076250204bd2af3aa44225717435e46396ef076b1954d2729","impliedFormat":1},{"version":"1c3dfad66ff0ba98b41c98c6f41af096fc56e959150bc3f44b2141fb278082fd","impliedFormat":1},{"version":"56208c500dcb5f42be7e18e8cb578f257a1a89b94b3280c506818fed06391805","impliedFormat":1},{"version":"0c94c2e497e1b9bcfda66aea239d5d36cd980d12a6d9d59e66f4be1fa3da5d5a","impliedFormat":1},{"version":"eb9271b3c585ea9dc7b19b906a921bf93f30f22330408ffec6df6a22057f3296","impliedFormat":1},{"version":"0205ee059bd2c4e12dcadc8e2cbd0132e27aeba84082a632681bd6c6c61db710","impliedFormat":1},{"version":"a694d38afadc2f7c20a8b1d150c68ac44d1d6c0229195c4d52947a89980126bc","impliedFormat":1},{"version":"9f1e00eab512de990ba27afa8634ca07362192063315be1f8166bc3dcc7f0e0f","impliedFormat":1},{"version":"9674788d4c5fcbd55c938e6719177ac932c304c94e0906551cc57a7942d2b53b","impliedFormat":1},{"version":"86dac6ce3fcd0a069b67a1ac9abdbce28588ea547fd2b42d73c1a2b7841cf182","impliedFormat":1},{"version":"4d34fbeadba0009ed3a1a5e77c99a1feedec65d88c4d9640910ff905e4e679f7","impliedFormat":1},{"version":"9d90361f495ed7057462bcaa9ae8d8dbad441147c27716d53b3dfeaea5bb7fc8","impliedFormat":1},{"version":"8fcc5571404796a8fe56e5c4d05049acdeac9c7a72205ac15b35cb463916d614","impliedFormat":1},{"version":"a3b3a1712610260c7ab96e270aad82bd7b28a53e5776f25a9a538831057ff44c","impliedFormat":1},{"version":"33a2af54111b3888415e1d81a7a803d37fada1ed2f419c427413742de3948ff5","impliedFormat":1},{"version":"d5a4fca3b69f2f740e447efb9565eecdbbe4e13f170b74dd4a829c5c9a5b8ebf","impliedFormat":1},{"version":"56f1e1a0c56efce87b94501a354729d0a0898508197cb50ab3e18322eb822199","impliedFormat":1},{"version":"8960e8c1730aa7efb87fcf1c02886865229fdbf3a8120dd08bb2305d2241bd7e","impliedFormat":1},{"version":"27bf82d1d38ea76a590cbe56873846103958cae2b6f4023dc59dd8282b66a38a","impliedFormat":1},{"version":"0daaab2afb95d5e1b75f87f59ee26f85a5f8d3005a799ac48b38976b9b521e69","impliedFormat":1},{"version":"2c378d9368abcd2eba8c29b294d40909845f68557bc0b38117e4f04fc56e5f9c","impliedFormat":1},{"version":"bb220eaac1677e2ad82ac4e7fd3e609a0c7b6f2d6d9c673a35068c97f9fcd5cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c60b14c297cc569c648ddaea70bc1540903b7f4da416edd46687e88a543515a1","impliedFormat":1},{"version":"94a802503ca276212549e04e4c6b11c4c14f4fa78722f90f7f0682e8847af434","impliedFormat":1},{"version":"9c0217750253e3bf9c7e3821e51cff04551c00e63258d5e190cf8bd3181d5d4a","impliedFormat":1},{"version":"5c2e7f800b757863f3ddf1a98d7521b8da892a95c1b2eafb48d652a782891677","impliedFormat":1},{"version":"21317aac25f94069dbcaa54492c014574c7e4d680b3b99423510b51c4e36035f","impliedFormat":1},{"version":"c61d8275c35a76cb12c271b5fa8707bb46b1e5778a370fd6037c244c4df6a725","impliedFormat":1},{"version":"c7793cb5cd2bef461059ca340fbcd19d7ddac7ab3dcc6cd1c90432fca260a6ae","impliedFormat":1},{"version":"fd3bf6d545e796ebd31acc33c3b20255a5bc61d963787fc8473035ea1c09d870","impliedFormat":1},{"version":"c7af51101b509721c540c86bb5fc952094404d22e8a18ced30c38a79619916fa","impliedFormat":1},{"version":"59c8f7d68f79c6e3015f8aee218282d47d3f15b85e5defc2d9d1961b6ffed7a0","impliedFormat":1},{"version":"93a2049cbc80c66aa33582ec2648e1df2df59d2b353d6b4a97c9afcbb111ccab","impliedFormat":1},{"version":"d04d359e40db3ae8a8c23d0f096ad3f9f73a9ef980f7cb252a1fdc1e7b3a2fb9","impliedFormat":1},{"version":"84aa4f0c33c729557185805aae6e0df3bd084e311da67a10972bbcf400321ff0","impliedFormat":1},{"version":"cf6cbe50e3f87b2f4fd1f39c0dc746b452d7ce41b48aadfdb724f44da5b6f6ed","impliedFormat":1},{"version":"3cf494506a50b60bf506175dead23f43716a088c031d3aa00f7220b3fbcd56c9","impliedFormat":1},{"version":"f2d47126f1544c40f2b16fc82a66f97a97beac2085053cf89b49730a0e34d231","impliedFormat":1},{"version":"724ac138ba41e752ae562072920ddee03ba69fe4de5dafb812e0a35ef7fb2c7e","impliedFormat":1},{"version":"e4eb3f8a4e2728c3f2c3cb8e6b60cadeb9a189605ee53184d02d265e2820865c","impliedFormat":1},{"version":"f16cb1b503f1a64b371d80a0018949135fbe06fb4c5f78d4f637b17921a49ee8","impliedFormat":1},{"version":"f4808c828723e236a4b35a1415f8f550ff5dec621f81deea79bf3a051a84ffd0","impliedFormat":1},{"version":"3b810aa3410a680b1850ab478d479c2f03ed4318d1e5bf7972b49c4d82bacd8d","impliedFormat":1},{"version":"0ce7166bff5669fcb826bc6b54b246b1cf559837ea9cc87c3414cc70858e6097","impliedFormat":1},{"version":"6ea095c807bc7cc36bc1774bc2a0ef7174bf1c6f7a4f6b499170b802ce214bfe","impliedFormat":1},{"version":"3549400d56ee2625bb5cc51074d3237702f1f9ffa984d61d9a2db2a116786c22","impliedFormat":1},{"version":"5327f9a620d003b202eff5db6be0b44e22079793c9a926e0a7a251b1dbbdd33f","impliedFormat":1},{"version":"b60f6734309d20efb9b0e0c7e6e68282ee451592b9c079dd1a988bb7a5eeb5e7","impliedFormat":1},{"version":"f4187a4e2973251fd9655598aa7e6e8bba879939a73188ee3290bb090cc46b15","impliedFormat":1},{"version":"44c1a26f578277f8ccef3215a4bd642a0a4fbbaf187cf9ae3053591c891fdc9c","impliedFormat":1},{"version":"a5989cd5e1e4ca9b327d2f93f43e7c981f25ee12a81c2ebde85ec7eb30f34213","impliedFormat":1},{"version":"f65b8fa1532dfe0ef2c261d63e72c46fe5f089b28edcd35b3526328d42b412b8","impliedFormat":1},{"version":"1060083aacfc46e7b7b766557bff5dafb99de3128e7bab772240877e5bfe849d","impliedFormat":1},{"version":"d61a3fa4243c8795139e7352694102315f7a6d815ad0aeb29074cfea1eb67e93","impliedFormat":1},{"version":"1f66b80bad5fa29d9597276821375ddf482c84cfb12e8adb718dc893ffce79e0","impliedFormat":1},{"version":"1ed8606c7b3612e15ff2b6541e5a926985cbb4d028813e969c1976b7f4133d73","impliedFormat":1},{"version":"c086ab778e9ba4b8dbb2829f42ef78e2b28204fc1a483e42f54e45d7a96e5737","impliedFormat":1},{"version":"dd0b9b00a39436c1d9f7358be8b1f32571b327c05b5ed0e88cc91f9d6b6bc3c9","impliedFormat":1},{"version":"a951a7b2224a4e48963762f155f5ad44ca1145f23655dde623ae312d8faeb2f2","impliedFormat":1},{"version":"cd960c347c006ace9a821d0a3cffb1d3fbc2518a4630fb3d77fe95f7fd0758b8","impliedFormat":1},{"version":"fe1f3b21a6cc1a6bc37276453bd2ac85910a8bdc16842dc49b711588e89b1b77","impliedFormat":1},{"version":"1a6a21ff41d509ab631dbe1ea14397c518b8551f040e78819f9718ef80f13975","impliedFormat":1},{"version":"0a55c554e9e858e243f714ce25caebb089e5cc7468d5fd022c1e8fa3d8e8173d","impliedFormat":1},{"version":"3a5e0fe9dcd4b1a9af657c487519a3c39b92a67b1b21073ff20e37f7d7852e32","impliedFormat":1},{"version":"977aeb024f773799d20985c6817a4c0db8fed3f601982a52d4093e0c60aba85f","impliedFormat":1},{"version":"d59cf5116848e162c7d3d954694f215b276ad10047c2854ed2ee6d14a481411f","impliedFormat":1},{"version":"50098be78e7cbfc324dfc04983571c80539e55e11a0428f83a090c13c41824a2","impliedFormat":1},{"version":"08e767d9d3a7e704a9ea5f057b0f020fd5880bc63fbb4aa6ffee73be36690014","impliedFormat":1},{"version":"dd6051c7b02af0d521857069c49897adb8595d1f0e94487d53ebc157294ef864","impliedFormat":1},{"version":"79c6a11f75a62151848da39f6098549af0dd13b22206244961048326f451b2a8","impliedFormat":1},"fca8faf900a0c1e0fbbf10e3e520e74aff6c17026c441862cdc80b62ab1b479a","d6423ec0080f5f9770474fd501a58ffd63b5aec89ee6a73bc00dc30c12655cad","57c7dac9afa14a5c6296c0ab24be137b87300a89d83e644b7a9c400d5c19672e","401eb02dc6058e0472cdc91b7fb8cb93062eca67fd4c791b60c23931a5a34d62","a2e07490c863b4b580a617160f7a5622eecd759aa70d673e647a7d08ad070c04","786e6f72488540b757292c69df7620d3815e93fffcd4e2b9709dd68ad8de01bd","947969993cb5a7f47b1834a775bcc9ef910cfebec11cc12831cf686b5ddf606c","c93af1dda945716bf5a47657bd0fe51fc0d3d1524341a34c0f1dff79f6e00a5a","efbd9ee2970fae9651117b43e0227d86e26ce8a2a70b0ff7e69ec8ae190eb486","4c4cc6da0c61dcdf9d482f39a8a75bd96e64defca3ffdd433c7e852e895918f9","cfd81388bfd6ddfa29e06c9ba1da3816cc5c1f636a774ef90654d1061c824af6","a813fe906a1638d00956f3f1720602e4854d3b433f6b35ebef9ea07c1b37b72d","6ae77d99c43ba771d7fcfc6c956378e5be362965451b2396e751c47fab82369c","e7ea98388dc56ac4b78342eefb9f4052979eef826422c97a330bd485d1f66090","890b7ef3bf0cfb385a7ec1f10c9ea02f8baadb309a9e82d86484990cde74e0ee","94d7bf0a53ca95e2f69ce8f0cc23bed58b998263edc88d3d2ac8d0b68423f092","610cf1c06bd2eeb4a40f1714340d387609cf075fe8efc792df5519d72ac4d9ef","3849c6dfa4e0231b234c72d30b70fe56f25a6c2beb9c9983212dd45c5b4e485c","1a6ba9d868c85cb6ce2694d839a5efb87ada439db340f6904b4483e577afa572","2644bf454f39fbd587e0452feff3cd7c74317b6218a3c798b8953de2f8995124","9570099588643b91681b6ff53b91383ce4040d8615f69fa992aa30eb4810c554","dae27c4d2a0424b56fa7ba3aeadf59a0fc6325b9c66c782336814b8089190ef9","05ec525d686f6485d8a38230fcbbe6da69e06d9183aa5c09702f5087b1384006","06a7f4c384cf1459c377a1dcd13636a65f6353c6447e6d45d62f2e20da45e99a","0cf88280fcd4a1c4e8b05832e86b71499dd1d584ef627a1495793d29770947c2","f6a5a48b5942b8d6979dcf0abe0f9cc0111aa8c96992e41600956b650c2de454","b427bc02b876d82278f25dd9f42d9d589a7f220dde5664e1915e6f278491c9ea","3db7cbd7b574c21eeadd96353715b35d56ad51e6375e461ae09f7b611a2843a9","0ba09f2e0a628dd8c5a99301cbba7815ce31811767549ffc868daeb4b638f53e","265351b9637e8ab770d21b48e712e02d62dfebf57ae071db3ffb3298035f1d17","96bdd618e34d7eaf1b9988469df8f3216615ef3512eeab23d3b11991c16df838","5c475282fb08f26b905cd074b4e1715e8cebad28893facaac66d90d0fc6f288f","d549677d7b520cf8a8dabbecae2da011130ad4cb00d3cef316fddce1afb126cb","595898ec952a86f2fc265e81d16f45cd1f5e48116db27fc6ed64cb8971213263","23414195f55b925fcb311097f2c7a7654c23ecef715661204d94af72f63a283f","860c8c12b83797eb898333f392a353343fefdc1a9046d3c2f967466dc137819c","dd48e5307e81bed8eaae5b8518325cd64e5354d39c43f0706b3eee2eff9cb4fc","7f168c678ce05ae9877964a940872a32c57fc587a3da49542d58b0ec57644a88","6113ee77c87fee19b31792c18d7ed0a6925647dbae53189c64e2087d990d9b76","7cac5f7a7552d383f5d5e915ecb8be98be78a5b97f2ba617690bfb879796152b","51dcdff71ae8a00305abd364c94b43d5ad63588a5fd76107c6c6f7740e0d4aa0","3d310221b18afbf10761719a5b31f12774f26b1dc9056068dfc9b12f0535d45c","ba4b32c992f3c26c386563afa6bd76d16e2ce5a22f5119be37a459b95b47ce6f","c4e376b71fa00bf0ccc227d583a0812db7e045a7146f403fb567fa8d236bda8e","0a6bf1f101fbe3774ffa2fa326edab3c2f32095eb53a3739e57ca71ddd8e239c","d6b325d9cc43748a4da453136d19bd54fee48872e8eed31471919c65c8c3c17d","79ae0ea59a0f28b306e50b1220e88b5545c2395d40725295439acc3996e34580","fc596974a8add8425683efd333a7678cb39c046064812c16288766c9f8debedd","6248005b18541b4cd574761506bfb1cb521dd7dd9ebf85027fccba94c53d9da2","c51584d140d3935b1fa8cf716f2b166aa6009ef8c8f66fdf1938ceffae541b92","b7fe30dc120f45e39de7f76afbe6710e6999cd04b79115d6c39971b757cad8f9","08671738acf6ce0f47e52d454edb9e713f7e07a6a8848bd4074d5605a5f075ff","12684d12241b63ff0c06c46102e8effceab7b0d0a8a5e479a0cd91c13b9ffd4f","c14491ba1bf818ec60ca02318525cc88ef5657ea0948ef147b9e9e320be5df77","27ede915128a8ea5777f4e408c6b50b6b4d2d2a406351e96925319815f5bde3e","1dc2cb573fcc22d40ad33f577c975adcf02096790359cda93e0f38ada3cfb803","344712edc4561b63b8f6e9875d643d57e11dd6a0392783b0c6084bae79ed6003","62cde40171c90bb043a741e280fbaae4cc3cdf383694db387a3aab7de0a0789d","382df1c67a1a1706da63c2c8a34cea34afb5a05e58fb64712d3dec2b187e0abc","55a414814c36b3bac144bffa7553785a9be7269f8991159b1987420677417678","275636e79373772f7ceb53b93704b82035f2d7b9d03522b4ee7ffded3c795ec3","ce846d7f0c05b716fc9e14f2b41ec3f4ce778b2f32f5a55a1d3971a1a5828ada","f5d9270557a9cf2486df3e205e589e754727d841388ddeb7a0adefb6709dd60b","5f8c4a84d453a52318cf83aa77554490807d98735252f51c29fe1d64882633d7","401de57ea282b13aac4fdf47170807447fb26fb57253a317856e0d0406c76357","5b145e5e0fd594fe29fe5db4406eedda9955e2efff3805b6bb28582dae445b7f","715f86129afc26c5db1f5263112f1c9fb0c33bf8049f69c46d1cad2de60e4dc8","a05e559ea258422df4d0454beeea6134e930f24ca1aca9978a6ab41c4311697d","761008110216f4af9fe02ed055c20956d5dbf82eb3672f8e8e04673f9b749d59","355857f448e3b700d340903ef45a3d2bf25f67ec175ea0dd0fcaae1a54b4bf37","92ea59e3e4f5340b89cb70473ea2083643c27ccd4fe80dbea5cd0d77730356b1","2b04ac4af831556b0818411dadc6010e35c0f5d8e54d5b5d3e61ec0a70af0241","452527d5a5f98166808bb40a3a2c5e5d30b62e913812922e8cde30da32ea3a59","fc8cbd357f7779d83f0d97b285705564535ca241cbb9c892d6f0406ef95ba319","714c9e8b8d2d4501a1857115cfac0678331f410dd6076895d93af930ed966636","05454bc27fa28601d346ec9ff19bae85fb14136e21b732cdf3c291bbc84840c7","cf219348f5d0353c35e23c26cdabab591d03418724f4019e7e4baf4ffb7932b6","b4811ecf01f70a0fe08941b6649ad58fed2574c171f7a8f495d29cfc4394158f","7fb0ba8bb5be3447291736485d6749bb78d37046376d2a7f18d08fd87ee799b5","156574ab8338a6ba765e5ec1c535ddfe3fbbfe1171fc421d730ce13d5f6bd110",{"version":"b1538a92b9bae8d230267210c5db38c2eb6bdb352128a3ce3aa8c6acf9fc9622","impliedFormat":1},{"version":"6fc1a4f64372593767a9b7b774e9b3b92bf04e8785c3f9ea98973aa9f4bbe490","impliedFormat":1},{"version":"ff09b6fbdcf74d8af4e131b8866925c5e18d225540b9b19ce9485ca93e574d84","impliedFormat":1},{"version":"d5895252efa27a50f134a9b580aa61f7def5ab73d0a8071f9b5bf9a317c01c2d","impliedFormat":1},{"version":"1f366bde16e0513fa7b64f87f86689c4d36efd85afce7eb24753e9c99b91c319","impliedFormat":1},{"version":"96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","impliedFormat":1}],"root":[[404,406],431,436,[444,467],[470,475],[478,483],502,[507,553],557,[562,578],[649,728]],"options":{"allowJs":true,"downlevelIteration":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"strictNullChecks":true},"referencedMap":[[725,1],[726,2],[724,3],[728,4],[727,5],[722,6],[723,7],[445,8],[446,8],[447,8],[448,8],[449,9],[450,8],[451,8],[452,8],[453,8],[454,8],[455,8],[436,10],[444,9],[456,10],[457,10],[458,10],[459,10],[460,10],[461,10],[462,10],[463,10],[464,10],[465,10],[466,10],[467,11],[470,12],[471,13],[472,11],[473,11],[474,11],[475,13],[478,14],[479,13],[480,14],[481,13],[482,11],[483,15],[476,13],[469,16],[477,17],[468,11],[484,11],[442,18],[441,19],[503,19],[435,20],[443,21],[437,11],[506,22],[438,11],[505,11],[433,11],[434,11],[432,23],[440,24],[439,25],[504,26],[501,27],[486,11],[487,11],[488,11],[489,11],[485,11],[490,28],[491,11],[493,29],[492,28],[494,28],[495,29],[496,28],[497,11],[498,28],[499,11],[500,11],[502,30],[516,31],[535,32],[510,33],[519,34],[522,35],[529,36],[513,37],[541,38],[526,39],[532,40],[538,41],[543,42],[507,43],[517,44],[536,45],[511,46],[520,47],[523,48],[530,49],[514,50],[542,51],[527,52],[533,53],[539,54],[515,14],[534,55],[509,56],[518,57],[521,14],[528,57],[512,55],[540,58],[525,59],[531,60],[537,59],[524,57],[508,14],[405,11],[404,61],[357,11],[729,11],[730,11],[731,11],[732,62],[599,11],[582,63],[600,64],[581,11],[733,11],[734,11],[142,65],[143,65],[144,66],[99,67],[145,68],[146,69],[147,70],[94,11],[97,71],[95,11],[96,11],[148,72],[149,73],[150,74],[151,75],[152,76],[153,77],[154,77],[155,78],[156,79],[157,80],[158,81],[100,11],[98,11],[159,82],[160,83],[161,84],[193,85],[162,86],[163,87],[164,88],[165,89],[166,90],[167,91],[168,92],[169,93],[170,94],[171,95],[172,95],[173,96],[174,11],[175,97],[177,98],[176,99],[178,100],[179,101],[180,102],[181,103],[182,104],[183,105],[184,106],[185,107],[186,108],[187,109],[188,110],[189,111],[190,112],[101,11],[102,11],[103,11],[141,113],[191,114],[192,115],[86,11],[198,116],[199,117],[197,118],[195,119],[196,120],[84,11],[87,121],[283,118],[85,11],[560,122],[561,118],[558,11],[559,11],[93,123],[360,124],[364,125],[366,126],[219,127],[233,128],[331,129],[264,11],[334,130],[297,131],[305,132],[332,133],[220,134],[263,11],[265,135],[333,136],[240,137],[221,138],[244,137],[234,137],[204,137],[289,139],[209,11],[286,140],[290,141],[375,142],[284,141],[376,143],[270,11],[287,144],[388,145],[387,146],[292,141],[386,11],[384,11],[385,147],[288,118],[276,148],[285,149],[300,150],[301,151],[291,152],[272,153],[379,154],[382,155],[251,156],[250,157],[249,158],[391,118],[248,159],[225,11],[394,11],[555,160],[554,11],[397,11],[396,118],[398,161],[200,11],[325,11],[232,162],[202,163],[348,11],[349,11],[351,11],[354,164],[350,11],[352,165],[353,165],[218,11],[231,11],[359,166],[367,167],[371,168],[214,169],[278,170],[277,11],[271,171],[296,172],[294,173],[293,11],[295,11],[299,174],[274,175],[213,176],[238,177],[322,178],[205,179],[212,180],[201,129],[336,181],[346,182],[335,11],[345,183],[239,11],[223,184],[314,185],[313,11],[321,186],[315,187],[319,188],[320,189],[318,187],[317,189],[316,187],[260,190],[245,190],[308,191],[246,191],[207,192],[206,11],[312,193],[311,194],[310,195],[309,196],[208,197],[282,198],[298,199],[281,200],[304,201],[306,202],[303,200],[241,197],[194,11],[323,203],[266,204],[344,205],[269,206],[339,207],[211,11],[340,208],[342,209],[343,210],[326,11],[338,179],[242,211],[324,212],[347,213],[215,11],[217,11],[222,214],[307,215],[210,216],[216,11],[268,217],[267,218],[224,219],[275,19],[273,220],[226,221],[228,222],[395,11],[227,223],[229,224],[362,11],[361,11],[363,11],[393,11],[230,225],[280,118],[92,11],[302,226],[252,11],[262,227],[369,118],[378,228],[259,118],[373,141],[258,229],[356,230],[257,228],[203,11],[380,231],[255,118],[256,118],[247,11],[261,11],[254,232],[253,233],[243,234],[237,152],[341,11],[236,235],[235,11],[365,11],[279,118],[358,236],[83,11],[91,237],[88,118],[89,11],[90,11],[337,238],[330,239],[329,11],[328,240],[327,11],[368,241],[370,242],[372,243],[556,244],[374,245],[377,246],[403,247],[381,247],[402,248],[383,249],[389,250],[390,251],[392,252],[399,253],[401,11],[400,254],[355,255],[423,256],[421,257],[422,258],[410,259],[411,257],[418,260],[409,261],[414,262],[424,11],[415,263],[420,264],[426,265],[425,266],[408,267],[416,268],[417,269],[412,270],[419,256],[413,271],[622,272],[624,273],[614,274],[619,275],[620,276],[626,277],[621,278],[618,279],[617,280],[616,281],[627,282],[584,275],[585,275],[625,275],[630,283],[640,284],[634,284],[642,284],[646,284],[632,285],[633,284],[635,284],[638,284],[641,284],[637,286],[639,284],[643,118],[636,275],[631,287],[593,118],[597,118],[587,275],[590,118],[595,275],[596,288],[589,289],[592,118],[594,118],[591,290],[580,118],[579,118],[648,291],[645,292],[611,293],[610,275],[608,118],[609,275],[612,294],[613,295],[606,118],[602,296],[605,275],[604,275],[603,275],[598,275],[607,296],[644,275],[623,297],[629,298],[628,299],[647,11],[615,11],[588,11],[586,300],[407,11],[429,301],[428,11],[427,11],[430,302],[81,11],[82,11],[13,11],[14,11],[16,11],[15,11],[2,11],[17,11],[18,11],[19,11],[20,11],[21,11],[22,11],[23,11],[24,11],[3,11],[25,11],[26,11],[4,11],[27,11],[31,11],[28,11],[29,11],[30,11],[32,11],[33,11],[34,11],[5,11],[35,11],[36,11],[37,11],[38,11],[6,11],[42,11],[39,11],[40,11],[41,11],[43,11],[7,11],[44,11],[49,11],[50,11],[45,11],[46,11],[47,11],[48,11],[8,11],[54,11],[51,11],[52,11],[53,11],[55,11],[9,11],[56,11],[57,11],[58,11],[60,11],[59,11],[61,11],[62,11],[10,11],[63,11],[64,11],[65,11],[11,11],[66,11],[67,11],[68,11],[69,11],[70,11],[1,11],[71,11],[72,11],[12,11],[76,11],[74,11],[79,11],[78,11],[73,11],[77,11],[75,11],[80,11],[119,303],[129,304],[118,303],[139,305],[110,306],[109,307],[138,254],[132,308],[137,309],[112,310],[126,311],[111,312],[135,313],[107,314],[106,254],[136,315],[108,316],[113,317],[114,11],[117,317],[104,11],[140,318],[130,319],[121,320],[122,321],[124,322],[120,323],[123,324],[133,254],[115,325],[116,326],[125,327],[105,328],[128,319],[127,317],[131,11],[134,329],[583,330],[601,331],[650,332],[578,333],[658,334],[660,335],[669,336],[676,337],[682,338],[569,339],[685,340],[690,341],[694,342],[699,343],[703,344],[710,345],[704,346],[717,347],[546,348],[547,348],[548,348],[549,348],[563,349],[564,350],[720,351],[565,352],[566,353],[568,354],[567,355],[571,118],[572,354],[570,356],[673,118],[663,354],[711,118],[573,354],[664,357],[562,357],[649,358],[721,359],[577,360],[574,361],[576,362],[575,363],[718,364],[719,364],[653,365],[651,366],[654,367],[656,368],[655,368],[652,369],[665,370],[666,370],[662,118],[667,370],[661,371],[672,372],[671,373],[670,374],[675,375],[674,376],[679,377],[677,378],[680,359],[678,379],[700,374],[701,366],[714,373],[713,354],[712,380],[715,381],[708,382],[706,354],[707,373],[705,373],[683,368],[687,383],[688,370],[686,384],[692,385],[691,366],[697,118],[695,386],[696,387],[557,388],[544,348],[552,389],[550,390],[551,391],[545,390],[553,11],[657,392],[659,393],[668,394],[681,395],[709,396],[684,397],[689,398],[693,399],[698,400],[716,401],[702,402],[431,403],[406,11]],"semanticDiagnosticsPerFile":[[551,[{"start":9217,"length":8,"code":2353,"category":1,"messageText":"Object literal may only specify known properties, and 'getStats' does not exist in type 'IExamService'."},{"start":9228,"length":2,"messageText":"Parameter 'id' implicitly has an 'any' type.","category":1,"code":7006},{"start":11132,"length":21,"code":2741,"category":1,"messageText":"Property 'getSubmissionResultByAssignment' is missing in type '{ getStudentPaper: (id: string) => Promise; submitAnswers: (assignmentId: string, answers: { examNodeId: string; studentAnswer: any; }[], timeSpent: number | undefined) => Promise<...>; saveProgress: (assignmentId: string, answers: { ...; }[]) => Promise<...>; getSubmissionResult: (submissionId:...' but required in type 'ISubmissionService'.","relatedInformation":[{"file":"./src/services/interfaces.ts","start":5312,"length":81,"messageText":"'getSubmissionResultByAssignment' is declared here.","category":3,"code":2728}],"canonicalHead":{"code":2322,"messageText":"Type '{ getStudentPaper: (id: string) => Promise; submitAnswers: (assignmentId: string, answers: { examNodeId: string; studentAnswer: any; }[], timeSpent: number | undefined) => Promise<...>; saveProgress: (assignmentId: string, answers: { ...; }[]) => Promise<...>; getSubmissionResult: (submissionId:...' is not assignable to type 'ISubmissionService'."}}]],[677,[{"start":17593,"length":12,"code":2322,"category":1,"messageText":{"messageText":"Type '{ children: string; variant: \"ghost\"; disabled: boolean; onClick: () => void; icon: Element; iconPosition: string; }' is not assignable to type 'IntrinsicAttributes & ButtonProps'.","category":1,"code":2322,"next":[{"messageText":"Property 'iconPosition' does not exist on type 'IntrinsicAttributes & ButtonProps'.","category":1,"code":2339}]}}]],[680,[{"start":748,"length":8,"code":2339,"category":1,"messageText":"Property 'getStats' does not exist on type 'IExamService'."}]],[716,[{"start":2503,"length":13,"code":2339,"category":1,"messageText":"Property 'studentAnswer' does not exist on type 'ExamNodeDto'."},{"start":2580,"length":13,"code":2339,"category":1,"messageText":"Property 'studentAnswer' does not exist on type 'ExamNodeDto'."},{"start":5317,"length":10,"code":2339,"category":1,"messageText":"Property 'submitExam' does not exist on type 'ISubmissionService'."}]]],"affectedFilesPendingEmit":[725,726,724,728,727,722,723,502,516,535,510,519,522,529,513,541,526,532,538,543,507,517,536,511,520,523,530,514,542,527,533,539,515,534,509,518,521,528,512,540,525,531,537,524,508,405,650,578,658,660,669,676,682,569,685,690,694,699,703,710,704,717,546,547,548,549,563,564,720,565,566,568,567,571,572,570,673,663,711,573,664,562,649,721,577,574,576,575,718,719,653,651,654,656,655,652,665,666,662,667,661,672,671,670,675,674,679,677,680,678,700,701,714,713,712,715,708,706,707,705,683,687,688,686,692,691,697,695,696,557,544,552,550,551,545,553,657,659,668,681,709,684,689,693,698,716,702,431,406],"version":"5.9.3"} \ No newline at end of file