@octo-cyber/quiz-engine 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/controllers/attempt.controller.d.ts +3 -0
  2. package/dist/controllers/attempt.controller.d.ts.map +1 -0
  3. package/dist/controllers/attempt.controller.js +83 -0
  4. package/dist/controllers/attempt.controller.js.map +1 -0
  5. package/dist/controllers/category.controller.d.ts +3 -0
  6. package/dist/controllers/category.controller.d.ts.map +1 -0
  7. package/dist/controllers/category.controller.js +38 -0
  8. package/dist/controllers/category.controller.js.map +1 -0
  9. package/dist/controllers/exam.controller.d.ts +3 -0
  10. package/dist/controllers/exam.controller.d.ts.map +1 -0
  11. package/dist/controllers/exam.controller.js +48 -0
  12. package/dist/controllers/exam.controller.js.map +1 -0
  13. package/dist/controllers/index.d.ts +7 -0
  14. package/dist/controllers/index.d.ts.map +1 -0
  15. package/dist/controllers/index.js +16 -0
  16. package/dist/controllers/index.js.map +1 -0
  17. package/dist/controllers/paper.controller.d.ts +3 -0
  18. package/dist/controllers/paper.controller.d.ts.map +1 -0
  19. package/dist/controllers/paper.controller.js +70 -0
  20. package/dist/controllers/paper.controller.js.map +1 -0
  21. package/dist/controllers/question.controller.d.ts +3 -0
  22. package/dist/controllers/question.controller.d.ts.map +1 -0
  23. package/dist/controllers/question.controller.js +59 -0
  24. package/dist/controllers/question.controller.js.map +1 -0
  25. package/dist/controllers/statistics.controller.d.ts +3 -0
  26. package/dist/controllers/statistics.controller.d.ts.map +1 -0
  27. package/dist/controllers/statistics.controller.js +37 -0
  28. package/dist/controllers/statistics.controller.js.map +1 -0
  29. package/dist/entities/index.d.ts +24 -0
  30. package/dist/entities/index.d.ts.map +1 -0
  31. package/dist/entities/index.js +43 -0
  32. package/dist/entities/index.js.map +1 -0
  33. package/dist/entities/quiz-answer.entity.d.ts +19 -0
  34. package/dist/entities/quiz-answer.entity.d.ts.map +1 -0
  35. package/dist/entities/quiz-answer.entity.js +81 -0
  36. package/dist/entities/quiz-answer.entity.js.map +1 -0
  37. package/dist/entities/quiz-attempt.entity.d.ts +17 -0
  38. package/dist/entities/quiz-attempt.entity.d.ts.map +1 -0
  39. package/dist/entities/quiz-attempt.entity.js +80 -0
  40. package/dist/entities/quiz-attempt.entity.js.map +1 -0
  41. package/dist/entities/quiz-category.entity.d.ts +10 -0
  42. package/dist/entities/quiz-category.entity.d.ts.map +1 -0
  43. package/dist/entities/quiz-category.entity.js +55 -0
  44. package/dist/entities/quiz-category.entity.js.map +1 -0
  45. package/dist/entities/quiz-exam.entity.d.ts +25 -0
  46. package/dist/entities/quiz-exam.entity.d.ts.map +1 -0
  47. package/dist/entities/quiz-exam.entity.js +99 -0
  48. package/dist/entities/quiz-exam.entity.js.map +1 -0
  49. package/dist/entities/quiz-paper-question.entity.d.ts +12 -0
  50. package/dist/entities/quiz-paper-question.entity.d.ts.map +1 -0
  51. package/dist/entities/quiz-paper-question.entity.js +58 -0
  52. package/dist/entities/quiz-paper-question.entity.js.map +1 -0
  53. package/dist/entities/quiz-paper.entity.d.ts +18 -0
  54. package/dist/entities/quiz-paper.entity.d.ts.map +1 -0
  55. package/dist/entities/quiz-paper.entity.js +75 -0
  56. package/dist/entities/quiz-paper.entity.js.map +1 -0
  57. package/dist/entities/quiz-question.entity.d.ts +28 -0
  58. package/dist/entities/quiz-question.entity.d.ts.map +1 -0
  59. package/dist/entities/quiz-question.entity.js +107 -0
  60. package/dist/entities/quiz-question.entity.js.map +1 -0
  61. package/dist/index.d.ts +27 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +66 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/quiz-engine.module.d.ts +8 -0
  66. package/dist/quiz-engine.module.d.ts.map +1 -0
  67. package/dist/quiz-engine.module.js +45 -0
  68. package/dist/quiz-engine.module.js.map +1 -0
  69. package/dist/schemas/attempt.schema.d.ts +47 -0
  70. package/dist/schemas/attempt.schema.d.ts.map +1 -0
  71. package/dist/schemas/attempt.schema.js +19 -0
  72. package/dist/schemas/attempt.schema.js.map +1 -0
  73. package/dist/schemas/category.schema.d.ts +36 -0
  74. package/dist/schemas/category.schema.d.ts.map +1 -0
  75. package/dist/schemas/category.schema.js +12 -0
  76. package/dist/schemas/category.schema.js.map +1 -0
  77. package/dist/schemas/exam.schema.d.ts +70 -0
  78. package/dist/schemas/exam.schema.d.ts.map +1 -0
  79. package/dist/schemas/exam.schema.js +20 -0
  80. package/dist/schemas/exam.schema.js.map +1 -0
  81. package/dist/schemas/paper.schema.d.ts +71 -0
  82. package/dist/schemas/paper.schema.d.ts.map +1 -0
  83. package/dist/schemas/paper.schema.js +26 -0
  84. package/dist/schemas/paper.schema.js.map +1 -0
  85. package/dist/schemas/question.schema.d.ts +147 -0
  86. package/dist/schemas/question.schema.d.ts.map +1 -0
  87. package/dist/schemas/question.schema.js +32 -0
  88. package/dist/schemas/question.schema.js.map +1 -0
  89. package/dist/services/attempt.service.d.ts +33 -0
  90. package/dist/services/attempt.service.d.ts.map +1 -0
  91. package/dist/services/attempt.service.js +197 -0
  92. package/dist/services/attempt.service.js.map +1 -0
  93. package/dist/services/category.service.d.ts +14 -0
  94. package/dist/services/category.service.d.ts.map +1 -0
  95. package/dist/services/category.service.js +74 -0
  96. package/dist/services/category.service.js.map +1 -0
  97. package/dist/services/exam.service.d.ts +17 -0
  98. package/dist/services/exam.service.d.ts.map +1 -0
  99. package/dist/services/exam.service.js +92 -0
  100. package/dist/services/exam.service.js.map +1 -0
  101. package/dist/services/grade.service.d.ts +16 -0
  102. package/dist/services/grade.service.d.ts.map +1 -0
  103. package/dist/services/grade.service.js +75 -0
  104. package/dist/services/grade.service.js.map +1 -0
  105. package/dist/services/index.d.ts +8 -0
  106. package/dist/services/index.d.ts.map +1 -0
  107. package/dist/services/index.js +18 -0
  108. package/dist/services/index.js.map +1 -0
  109. package/dist/services/paper.service.d.ts +32 -0
  110. package/dist/services/paper.service.d.ts.map +1 -0
  111. package/dist/services/paper.service.js +157 -0
  112. package/dist/services/paper.service.js.map +1 -0
  113. package/dist/services/question.service.d.ts +30 -0
  114. package/dist/services/question.service.d.ts.map +1 -0
  115. package/dist/services/question.service.js +155 -0
  116. package/dist/services/question.service.js.map +1 -0
  117. package/dist/services/statistics.service.d.ts +43 -0
  118. package/dist/services/statistics.service.d.ts.map +1 -0
  119. package/dist/services/statistics.service.js +134 -0
  120. package/dist/services/statistics.service.js.map +1 -0
  121. package/package.json +85 -0
  122. package/web/index.ts +51 -0
  123. package/web/manifest.ts +36 -0
  124. package/web/messages/en-US.json +143 -0
  125. package/web/messages/zh-CN.json +143 -0
  126. package/web/pages/ExamRoomPage.tsx +289 -0
  127. package/web/pages/ExamsPage.tsx +248 -0
  128. package/web/pages/PapersPage.tsx +202 -0
  129. package/web/pages/QuestionBankPage.tsx +263 -0
  130. package/web/pages/StatisticsPage.tsx +178 -0
  131. package/web/services/attempt-service.ts +53 -0
  132. package/web/services/category-service.ts +26 -0
  133. package/web/services/exam-service.ts +31 -0
  134. package/web/services/paper-service.ts +50 -0
  135. package/web/services/question-service.ts +36 -0
  136. package/web/services/statistics-service.ts +28 -0
  137. package/web/stores/quiz-store.ts +31 -0
  138. package/web/types/quiz.ts +166 -0
@@ -0,0 +1,53 @@
1
+ import { api } from '@octo-cyber/ui/services/api-client'
2
+ import type { ApiResponse } from '@octo-cyber/ui/types/common'
3
+ import type { QuizAttempt, QuizAnswer, AttemptDetail, PaginatedResponse } from '../types/quiz'
4
+
5
+ export const attemptApi = {
6
+ async start(examId: string): Promise<QuizAttempt> {
7
+ const res = await api.post<ApiResponse<QuizAttempt>>('/api/v1/quiz/attempts/start', { examId })
8
+ return res.data
9
+ },
10
+
11
+ async submit(
12
+ attemptId: string,
13
+ answers: Array<{ questionId: string; userAnswer: string | string[] | null }>,
14
+ timeUsedSeconds?: number,
15
+ ): Promise<QuizAttempt> {
16
+ const res = await api.post<ApiResponse<QuizAttempt>>(
17
+ `/api/v1/quiz/attempts/${attemptId}/submit`,
18
+ { answers, timeUsedSeconds },
19
+ )
20
+ return res.data
21
+ },
22
+
23
+ async getDetail(attemptId: string): Promise<AttemptDetail> {
24
+ const res = await api.get<ApiResponse<AttemptDetail>>(`/api/v1/quiz/attempts/${attemptId}`)
25
+ return res.data
26
+ },
27
+
28
+ async listByExam(examId: string, params: Record<string, string> = {}): Promise<PaginatedResponse<QuizAttempt>> {
29
+ const res = await api.get<ApiResponse<PaginatedResponse<QuizAttempt>>>(
30
+ `/api/v1/quiz/exams/${examId}/attempts`,
31
+ { params },
32
+ )
33
+ return res.data
34
+ },
35
+
36
+ async listMine(params: Record<string, string> = {}): Promise<PaginatedResponse<QuizAttempt>> {
37
+ const res = await api.get<ApiResponse<PaginatedResponse<QuizAttempt>>>('/api/v1/quiz/my/attempts', { params })
38
+ return res.data
39
+ },
40
+
41
+ async gradeAnswer(answerId: string, score: number, feedback?: string): Promise<QuizAnswer> {
42
+ const res = await api.put<ApiResponse<QuizAnswer>>(
43
+ `/api/v1/quiz/answers/${answerId}/grade`,
44
+ { score, feedback },
45
+ )
46
+ return res.data
47
+ },
48
+
49
+ async finalizeGrading(attemptId: string): Promise<QuizAttempt> {
50
+ const res = await api.post<ApiResponse<QuizAttempt>>(`/api/v1/quiz/attempts/${attemptId}/finalize`, {})
51
+ return res.data
52
+ },
53
+ }
@@ -0,0 +1,26 @@
1
+ import { api } from '@octo-cyber/ui/services/api-client'
2
+ import type { ApiResponse } from '@octo-cyber/ui/types/common'
3
+ import type { QuizCategory } from '../types/quiz'
4
+
5
+ const BASE = '/api/v1/quiz/categories'
6
+
7
+ export const categoryApi = {
8
+ async list(): Promise<QuizCategory[]> {
9
+ const res = await api.get<ApiResponse<QuizCategory[]>>(BASE)
10
+ return res.data
11
+ },
12
+
13
+ async create(data: Partial<QuizCategory>): Promise<QuizCategory> {
14
+ const res = await api.post<ApiResponse<QuizCategory>>(BASE, data)
15
+ return res.data
16
+ },
17
+
18
+ async update(id: string, data: Partial<QuizCategory>): Promise<QuizCategory> {
19
+ const res = await api.put<ApiResponse<QuizCategory>>(`${BASE}/${id}`, data)
20
+ return res.data
21
+ },
22
+
23
+ async delete(id: string): Promise<void> {
24
+ await api.delete(`${BASE}/${id}`)
25
+ },
26
+ }
@@ -0,0 +1,31 @@
1
+ import { api } from '@octo-cyber/ui/services/api-client'
2
+ import type { ApiResponse } from '@octo-cyber/ui/types/common'
3
+ import type { QuizExam, PaginatedResponse } from '../types/quiz'
4
+
5
+ const BASE = '/api/v1/quiz/exams'
6
+
7
+ export const examApi = {
8
+ async list(params: Record<string, string> = {}): Promise<PaginatedResponse<QuizExam>> {
9
+ const res = await api.get<ApiResponse<PaginatedResponse<QuizExam>>>(BASE, { params })
10
+ return res.data
11
+ },
12
+
13
+ async get(id: string): Promise<QuizExam> {
14
+ const res = await api.get<ApiResponse<QuizExam>>(`${BASE}/${id}`)
15
+ return res.data
16
+ },
17
+
18
+ async create(data: Partial<QuizExam>): Promise<QuizExam> {
19
+ const res = await api.post<ApiResponse<QuizExam>>(BASE, data)
20
+ return res.data
21
+ },
22
+
23
+ async update(id: string, data: Partial<QuizExam>): Promise<QuizExam> {
24
+ const res = await api.put<ApiResponse<QuizExam>>(`${BASE}/${id}`, data)
25
+ return res.data
26
+ },
27
+
28
+ async delete(id: string): Promise<void> {
29
+ await api.delete(`${BASE}/${id}`)
30
+ },
31
+ }
@@ -0,0 +1,50 @@
1
+ import { api } from '@octo-cyber/ui/services/api-client'
2
+ import type { ApiResponse } from '@octo-cyber/ui/types/common'
3
+ import type { QuizPaper, QuizPaperQuestion, PaperWithQuestions, PaginatedResponse } from '../types/quiz'
4
+
5
+ const BASE = '/api/v1/quiz/papers'
6
+
7
+ export const paperApi = {
8
+ async list(params: Record<string, string> = {}): Promise<PaginatedResponse<QuizPaper>> {
9
+ const res = await api.get<ApiResponse<PaginatedResponse<QuizPaper>>>(BASE, { params })
10
+ return res.data
11
+ },
12
+
13
+ async get(id: string): Promise<QuizPaper> {
14
+ const res = await api.get<ApiResponse<QuizPaper>>(`${BASE}/${id}`)
15
+ return res.data
16
+ },
17
+
18
+ async getWithQuestions(id: string): Promise<PaperWithQuestions> {
19
+ const res = await api.get<ApiResponse<PaperWithQuestions>>(`${BASE}/${id}/questions`)
20
+ return res.data
21
+ },
22
+
23
+ async create(data: Partial<QuizPaper>): Promise<QuizPaper> {
24
+ const res = await api.post<ApiResponse<QuizPaper>>(BASE, data)
25
+ return res.data
26
+ },
27
+
28
+ async update(id: string, data: Partial<QuizPaper>): Promise<QuizPaper> {
29
+ const res = await api.put<ApiResponse<QuizPaper>>(`${BASE}/${id}`, data)
30
+ return res.data
31
+ },
32
+
33
+ async delete(id: string): Promise<void> {
34
+ await api.delete(`${BASE}/${id}`)
35
+ },
36
+
37
+ async addQuestion(paperId: string, questionId: string, score?: number): Promise<QuizPaperQuestion> {
38
+ const res = await api.post<ApiResponse<QuizPaperQuestion>>(`${BASE}/${paperId}/questions`, { questionId, score })
39
+ return res.data
40
+ },
41
+
42
+ async updateQuestion(paperId: string, questionId: string, data: Partial<QuizPaperQuestion>): Promise<QuizPaperQuestion> {
43
+ const res = await api.put<ApiResponse<QuizPaperQuestion>>(`${BASE}/${paperId}/questions/${questionId}`, data)
44
+ return res.data
45
+ },
46
+
47
+ async removeQuestion(paperId: string, questionId: string): Promise<void> {
48
+ await api.delete(`${BASE}/${paperId}/questions/${questionId}`)
49
+ },
50
+ }
@@ -0,0 +1,36 @@
1
+ import { api } from '@octo-cyber/ui/services/api-client'
2
+ import type { ApiResponse } from '@octo-cyber/ui/types/common'
3
+ import type { QuizQuestion, PaginatedResponse, SmartGenerateRule } from '../types/quiz'
4
+
5
+ const BASE = '/api/v1/quiz/questions'
6
+
7
+ export const questionApi = {
8
+ async list(params: Record<string, string> = {}): Promise<PaginatedResponse<QuizQuestion>> {
9
+ const res = await api.get<ApiResponse<PaginatedResponse<QuizQuestion>>>(BASE, { params })
10
+ return res.data
11
+ },
12
+
13
+ async get(id: string): Promise<QuizQuestion> {
14
+ const res = await api.get<ApiResponse<QuizQuestion>>(`${BASE}/${id}`)
15
+ return res.data
16
+ },
17
+
18
+ async create(data: Partial<QuizQuestion>): Promise<QuizQuestion> {
19
+ const res = await api.post<ApiResponse<QuizQuestion>>(BASE, data)
20
+ return res.data
21
+ },
22
+
23
+ async update(id: string, data: Partial<QuizQuestion>): Promise<QuizQuestion> {
24
+ const res = await api.put<ApiResponse<QuizQuestion>>(`${BASE}/${id}`, data)
25
+ return res.data
26
+ },
27
+
28
+ async delete(id: string): Promise<void> {
29
+ await api.delete(`${BASE}/${id}`)
30
+ },
31
+
32
+ async smartGenerate(paperId: string, rules: SmartGenerateRule[]): Promise<{ added: number }> {
33
+ const res = await api.post<ApiResponse<{ added: number }>>(`${BASE}/smart-generate`, { paperId, rules })
34
+ return res.data
35
+ },
36
+ }
@@ -0,0 +1,28 @@
1
+ import { api } from '@octo-cyber/ui/services/api-client'
2
+ import type { ApiResponse } from '@octo-cyber/ui/types/common'
3
+ import type { ExamStats, UserStats, RankingEntry } from '../types/quiz'
4
+
5
+ export const statisticsApi = {
6
+ async getExamStats(examId: string): Promise<ExamStats> {
7
+ const res = await api.get<ApiResponse<ExamStats>>(`/api/v1/quiz/exams/${examId}/stats`)
8
+ return res.data
9
+ },
10
+
11
+ async getRanking(examId: string, limit = 20): Promise<RankingEntry[]> {
12
+ const res = await api.get<ApiResponse<RankingEntry[]>>(
13
+ `/api/v1/quiz/exams/${examId}/ranking`,
14
+ { params: { limit: String(limit) } },
15
+ )
16
+ return res.data
17
+ },
18
+
19
+ async getMyStats(): Promise<UserStats> {
20
+ const res = await api.get<ApiResponse<UserStats>>('/api/v1/quiz/my/stats')
21
+ return res.data
22
+ },
23
+
24
+ async getUserStats(userId: string): Promise<UserStats> {
25
+ const res = await api.get<ApiResponse<UserStats>>(`/api/v1/quiz/stats/user/${userId}`)
26
+ return res.data
27
+ },
28
+ }
@@ -0,0 +1,31 @@
1
+ import { create } from 'zustand'
2
+
3
+ interface QuizStoreState {
4
+ questionFilter: string
5
+ setQuestionFilter: (f: string) => void
6
+ paperFilter: string
7
+ setPaperFilter: (f: string) => void
8
+ examFilter: string
9
+ setExamFilter: (f: string) => void
10
+ currentAttemptId: string | null
11
+ setCurrentAttemptId: (id: string | null) => void
12
+ /** Saved in-progress answers keyed by questionId */
13
+ draftAnswers: Record<string, string | string[]>
14
+ setDraftAnswer: (questionId: string, answer: string | string[]) => void
15
+ clearDraftAnswers: () => void
16
+ }
17
+
18
+ export const useQuizStore = create<QuizStoreState>((set) => ({
19
+ questionFilter: 'all',
20
+ setQuestionFilter: (f) => set({ questionFilter: f }),
21
+ paperFilter: 'all',
22
+ setPaperFilter: (f) => set({ paperFilter: f }),
23
+ examFilter: 'all',
24
+ setExamFilter: (f) => set({ examFilter: f }),
25
+ currentAttemptId: null,
26
+ setCurrentAttemptId: (id) => set({ currentAttemptId: id }),
27
+ draftAnswers: {},
28
+ setDraftAnswer: (questionId, answer) =>
29
+ set((s) => ({ draftAnswers: { ...s.draftAnswers, [questionId]: answer } })),
30
+ clearDraftAnswers: () => set({ draftAnswers: {} }),
31
+ }))
@@ -0,0 +1,166 @@
1
+ /** @octo-cyber/quiz-engine — frontend types */
2
+
3
+ export interface QuizCategory {
4
+ id: string
5
+ name: string
6
+ parentId: string | null
7
+ description: string | null
8
+ sortOrder: number
9
+ createdAt: string
10
+ updatedAt: string
11
+ }
12
+
13
+ export interface QuizOption {
14
+ key: string
15
+ text: string
16
+ }
17
+
18
+ export type QuestionType = 'SINGLE_CHOICE' | 'MULTIPLE_CHOICE' | 'TRUE_FALSE' | 'FILL_BLANK' | 'ESSAY'
19
+
20
+ export interface QuizQuestion {
21
+ id: string
22
+ categoryId: string | null
23
+ categoryName?: string | null
24
+ type: QuestionType
25
+ content: string
26
+ options: QuizOption[] | null
27
+ correctAnswer: string | string[] | null
28
+ explanation: string | null
29
+ difficulty: number
30
+ tags: string[] | null
31
+ defaultScore: number
32
+ useCount: number
33
+ createdBy: string | null
34
+ createdAt: string
35
+ updatedAt: string
36
+ }
37
+
38
+ export type PaperStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED'
39
+
40
+ export interface QuizPaper {
41
+ id: string
42
+ title: string
43
+ description: string | null
44
+ totalScore: number
45
+ passingScore: number
46
+ timeLimit: number
47
+ status: PaperStatus
48
+ createdBy: string | null
49
+ createdAt: string
50
+ updatedAt: string
51
+ }
52
+
53
+ export interface QuizPaperQuestion {
54
+ id: string
55
+ paperId: string
56
+ questionId: string
57
+ score: number
58
+ sortOrder: number
59
+ sectionTitle: string | null
60
+ createdAt: string
61
+ questionDetail: QuizQuestion
62
+ }
63
+
64
+ export interface PaperWithQuestions {
65
+ paper: QuizPaper
66
+ questions: QuizPaperQuestion[]
67
+ }
68
+
69
+ export type ExamStatus = 'DRAFT' | 'SCHEDULED' | 'ONGOING' | 'ENDED' | 'CANCELLED'
70
+
71
+ export interface QuizExam {
72
+ id: string
73
+ title: string
74
+ paperId: string
75
+ description: string | null
76
+ startTime: string | null
77
+ endTime: string | null
78
+ maxAttempts: number
79
+ shuffleQuestions: boolean
80
+ shuffleOptions: boolean
81
+ showAnswerAfterSubmit: boolean
82
+ status: ExamStatus
83
+ createdBy: string | null
84
+ createdAt: string
85
+ updatedAt: string
86
+ }
87
+
88
+ export type AttemptStatus = 'IN_PROGRESS' | 'SUBMITTED' | 'GRADED'
89
+
90
+ export interface QuizAttempt {
91
+ id: string
92
+ examId: string
93
+ userId: string
94
+ startedAt: string
95
+ submittedAt: string | null
96
+ timeUsedSeconds: number
97
+ status: AttemptStatus
98
+ totalScore: number
99
+ isPassed: boolean
100
+ createdAt: string
101
+ updatedAt: string
102
+ }
103
+
104
+ export interface QuizAnswer {
105
+ id: string
106
+ attemptId: string
107
+ questionId: string
108
+ userAnswer: string | string[] | null
109
+ isCorrect: boolean | null
110
+ score: number
111
+ feedback: string | null
112
+ gradedBy: string | null
113
+ gradedAt: string | null
114
+ createdAt: string
115
+ updatedAt: string
116
+ questionDetail?: QuizQuestion
117
+ }
118
+
119
+ export interface AttemptDetail {
120
+ attempt: QuizAttempt
121
+ answers: Array<QuizAnswer & { questionDetail: QuizQuestion }>
122
+ }
123
+
124
+ export interface PaginatedResponse<T> {
125
+ items: T[]
126
+ total: number
127
+ page: number
128
+ pageSize: number
129
+ }
130
+
131
+ export interface ExamStats {
132
+ examId: string
133
+ title: string
134
+ totalAttempts: number
135
+ submittedAttempts: number
136
+ gradedAttempts: number
137
+ averageScore: number
138
+ highestScore: number
139
+ lowestScore: number
140
+ passRate: number
141
+ scoreDistribution: Array<{ range: string; count: number }>
142
+ }
143
+
144
+ export interface UserStats {
145
+ userId: string
146
+ totalAttempts: number
147
+ averageScore: number
148
+ highestScore: number
149
+ passCount: number
150
+ passRate: number
151
+ recentAttempts: QuizAttempt[]
152
+ }
153
+
154
+ export interface RankingEntry {
155
+ userId: string
156
+ score: number
157
+ rank: number
158
+ }
159
+
160
+ export interface SmartGenerateRule {
161
+ categoryId?: string | null
162
+ type?: QuestionType
163
+ difficulty?: number
164
+ count: number
165
+ scorePerQuestion?: number
166
+ }