quachtiensinh27 commited on
Commit
3924714
·
1 Parent(s): bbd714b

feat: Add Quiz Interactive feature - Phase 1 (Database + Backend)

Browse files

- Database: quizzes, questions, quiz_attempts, quiz_summaries tables
- RLS policies for TA/student access control
- Backend API: generate, edit, send, submit, summary endpoints
- AI integration for quiz generation from recap content
- Fixed all security issues from code review
- Added checkSpaceMembership for student endpoints
- Prompt injection protection (topic validation)
- Quiz mutability check after attempts
- JSON parsing error handling
- API response format standardization
- Bulk insert optimization
- Added composite index for summaries

package-lock.json CHANGED
@@ -64,7 +64,6 @@
64
  "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
65
  "dev": true,
66
  "license": "MIT",
67
- "peer": true,
68
  "dependencies": {
69
  "@babel/code-frame": "^7.29.0",
70
  "@babel/generator": "^7.29.0",
@@ -274,6 +273,29 @@
274
  "node": ">=6.9.0"
275
  }
276
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  "node_modules/@emnapi/wasi-threads": {
278
  "version": "1.2.1",
279
  "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -1310,7 +1332,6 @@
1310
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
1311
  "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
1312
  "license": "MIT",
1313
- "peer": true,
1314
  "dependencies": {
1315
  "csstype": "^3.2.2"
1316
  }
@@ -1375,7 +1396,6 @@
1375
  "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
1376
  "dev": true,
1377
  "license": "MIT",
1378
- "peer": true,
1379
  "bin": {
1380
  "acorn": "bin/acorn"
1381
  },
@@ -1511,7 +1531,6 @@
1511
  }
1512
  ],
1513
  "license": "MIT",
1514
- "peer": true,
1515
  "dependencies": {
1516
  "baseline-browser-mapping": "^2.10.12",
1517
  "caniuse-lite": "^1.0.30001782",
@@ -2069,7 +2088,6 @@
2069
  "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
2070
  "dev": true,
2071
  "license": "MIT",
2072
- "peer": true,
2073
  "dependencies": {
2074
  "@eslint-community/eslint-utils": "^4.8.0",
2075
  "@eslint-community/regexpp": "^4.12.1",
@@ -4001,7 +4019,6 @@
4001
  "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
4002
  "dev": true,
4003
  "license": "MIT",
4004
- "peer": true,
4005
  "engines": {
4006
  "node": ">=12"
4007
  },
@@ -4082,7 +4099,6 @@
4082
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
4083
  "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
4084
  "license": "MIT",
4085
- "peer": true,
4086
  "engines": {
4087
  "node": ">=0.10.0"
4088
  }
@@ -4092,7 +4108,6 @@
4092
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
4093
  "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
4094
  "license": "MIT",
4095
- "peer": true,
4096
  "dependencies": {
4097
  "scheduler": "^0.27.0"
4098
  },
@@ -4148,7 +4163,6 @@
4148
  "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
4149
  "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
4150
  "license": "MIT",
4151
- "peer": true,
4152
  "dependencies": {
4153
  "@types/use-sync-external-store": "^0.0.6",
4154
  "use-sync-external-store": "^1.4.0"
@@ -4211,8 +4225,7 @@
4211
  "version": "5.0.1",
4212
  "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
4213
  "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
4214
- "license": "MIT",
4215
- "peer": true
4216
  },
4217
  "node_modules/redux-thunk": {
4218
  "version": "3.1.0",
@@ -4736,7 +4749,6 @@
4736
  "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
4737
  "dev": true,
4738
  "license": "MIT",
4739
- "peer": true,
4740
  "dependencies": {
4741
  "lightningcss": "^1.32.0",
4742
  "picomatch": "^4.0.4",
@@ -4890,7 +4902,6 @@
4890
  "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
4891
  "dev": true,
4892
  "license": "MIT",
4893
- "peer": true,
4894
  "funding": {
4895
  "url": "https://github.com/sponsors/colinhacks"
4896
  }
 
64
  "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
65
  "dev": true,
66
  "license": "MIT",
 
67
  "dependencies": {
68
  "@babel/code-frame": "^7.29.0",
69
  "@babel/generator": "^7.29.0",
 
273
  "node": ">=6.9.0"
274
  }
275
  },
276
+ "node_modules/@emnapi/core": {
277
+ "version": "1.10.0",
278
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
279
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
280
+ "dev": true,
281
+ "license": "MIT",
282
+ "optional": true,
283
+ "dependencies": {
284
+ "@emnapi/wasi-threads": "1.2.1",
285
+ "tslib": "^2.4.0"
286
+ }
287
+ },
288
+ "node_modules/@emnapi/runtime": {
289
+ "version": "1.10.0",
290
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
291
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
292
+ "dev": true,
293
+ "license": "MIT",
294
+ "optional": true,
295
+ "dependencies": {
296
+ "tslib": "^2.4.0"
297
+ }
298
+ },
299
  "node_modules/@emnapi/wasi-threads": {
300
  "version": "1.2.1",
301
  "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
 
1332
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
1333
  "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
1334
  "license": "MIT",
 
1335
  "dependencies": {
1336
  "csstype": "^3.2.2"
1337
  }
 
1396
  "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
1397
  "dev": true,
1398
  "license": "MIT",
 
1399
  "bin": {
1400
  "acorn": "bin/acorn"
1401
  },
 
1531
  }
1532
  ],
1533
  "license": "MIT",
 
1534
  "dependencies": {
1535
  "baseline-browser-mapping": "^2.10.12",
1536
  "caniuse-lite": "^1.0.30001782",
 
2088
  "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
2089
  "dev": true,
2090
  "license": "MIT",
 
2091
  "dependencies": {
2092
  "@eslint-community/eslint-utils": "^4.8.0",
2093
  "@eslint-community/regexpp": "^4.12.1",
 
4019
  "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
4020
  "dev": true,
4021
  "license": "MIT",
 
4022
  "engines": {
4023
  "node": ">=12"
4024
  },
 
4099
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
4100
  "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
4101
  "license": "MIT",
 
4102
  "engines": {
4103
  "node": ">=0.10.0"
4104
  }
 
4108
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
4109
  "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
4110
  "license": "MIT",
 
4111
  "dependencies": {
4112
  "scheduler": "^0.27.0"
4113
  },
 
4163
  "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
4164
  "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
4165
  "license": "MIT",
 
4166
  "dependencies": {
4167
  "@types/use-sync-external-store": "^0.0.6",
4168
  "use-sync-external-store": "^1.4.0"
 
4225
  "version": "5.0.1",
4226
  "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
4227
  "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
4228
+ "license": "MIT"
 
4229
  },
4230
  "node_modules/redux-thunk": {
4231
  "version": "3.1.0",
 
4749
  "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
4750
  "dev": true,
4751
  "license": "MIT",
 
4752
  "dependencies": {
4753
  "lightningcss": "^1.32.0",
4754
  "picomatch": "^4.0.4",
 
4902
  "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
4903
  "dev": true,
4904
  "license": "MIT",
 
4905
  "funding": {
4906
  "url": "https://github.com/sponsors/colinhacks"
4907
  }
scripts/log_hook.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Placeholder
src/components/ta/QuizMockupOptionA.html ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Quiz Mockup - Option A (Inline Quiz Generation)</title>
7
+ <style>
8
+ :root {
9
+ --primary: #3b82f6;
10
+ --primary-active: #2563eb;
11
+ --bg-surface: #ffffff;
12
+ --bg-surface-secondary: #f8fafc;
13
+ --bg-surface-tertiary: #f1f5f9;
14
+ --text-primary: #1e293b;
15
+ --text-secondary: #475569;
16
+ --text-muted: #94a3b8;
17
+ --border-primary: #e2e8f0;
18
+ --ta-red: #ef4444;
19
+ --ta-red-bg: rgba(239, 68, 68, 0.1);
20
+ --ta-amber: #f59e0b;
21
+ --ta-amber-bg: rgba(245, 158, 11, 0.1);
22
+ --ta-blue: #3b82f6;
23
+ --ta-blue-bg: rgba(59, 130, 246, 0.1);
24
+ --ta-green: #10b981;
25
+ --ta-green-bg: rgba(16, 185, 129, 0.1);
26
+ }
27
+
28
+ * {
29
+ margin: 0;
30
+ padding: 0;
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ body {
35
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
36
+ background: var(--bg-surface-secondary);
37
+ color: var(--text-primary);
38
+ line-height: 1.5;
39
+ }
40
+
41
+ .mockup-container {
42
+ max-width: 1400px;
43
+ margin: 0 auto;
44
+ padding: 40px 24px;
45
+ }
46
+
47
+ .mockup-header {
48
+ text-align: center;
49
+ margin-bottom: 40px;
50
+ }
51
+
52
+ .mockup-header h1 {
53
+ font-size: 28px;
54
+ font-weight: 700;
55
+ color: var(--primary);
56
+ margin-bottom: 8px;
57
+ }
58
+
59
+ .mockup-header p {
60
+ font-size: 14px;
61
+ color: var(--text-muted);
62
+ }
63
+
64
+ .option-badge {
65
+ display: inline-block;
66
+ padding: 6px 16px;
67
+ background: var(--primary);
68
+ color: white;
69
+ border-radius: 20px;
70
+ font-size: 12px;
71
+ font-weight: 600;
72
+ margin-bottom: 16px;
73
+ }
74
+
75
+ /* Main Card Container */
76
+ .quiz-container {
77
+ background: var(--bg-surface-secondary);
78
+ border: 1px solid var(--border-primary);
79
+ border-radius: 12px;
80
+ overflow: hidden;
81
+ height: 800px;
82
+ display: flex;
83
+ flex-direction: column;
84
+ }
85
+
86
+ /* Header */
87
+ .quiz-header {
88
+ padding: 16px 24px;
89
+ border-bottom: 1px solid var(--border-primary);
90
+ background: var(--bg-surface-tertiary);
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ flex-shrink: 0;
95
+ }
96
+
97
+ .quiz-header-left {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 12px;
101
+ }
102
+
103
+ .quiz-icon {
104
+ width: 32px;
105
+ height: 32px;
106
+ background: var(--primary);
107
+ border-radius: 8px;
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ color: white;
112
+ }
113
+
114
+ .quiz-title h3 {
115
+ font-size: 15px;
116
+ font-weight: 700;
117
+ margin: 0;
118
+ }
119
+
120
+ .quiz-title p {
121
+ font-size: 12px;
122
+ color: var(--text-muted);
123
+ margin-top: 2px;
124
+ }
125
+
126
+ .quiz-actions {
127
+ display: flex;
128
+ gap: 8px;
129
+ }
130
+
131
+ .quiz-btn {
132
+ padding: 6px 14px;
133
+ border-radius: 8px;
134
+ border: 1px solid var(--border-primary);
135
+ background: var(--bg-surface);
136
+ color: var(--text-secondary);
137
+ font-size: 12px;
138
+ font-weight: 600;
139
+ cursor: pointer;
140
+ transition: all 0.2s;
141
+ }
142
+
143
+ .quiz-btn:hover {
144
+ border-color: var(--primary);
145
+ color: var(--primary);
146
+ }
147
+
148
+ .quiz-btn.primary {
149
+ background: var(--primary);
150
+ color: white;
151
+ border-color: var(--primary);
152
+ }
153
+
154
+ /* Score Summary Bar */
155
+ .score-bar {
156
+ padding: 16px 24px;
157
+ background: linear-gradient(135deg, var(--ta-blue-bg) 0%, rgba(59, 130, 246, 0.05) 100%);
158
+ border-bottom: 1px solid var(--border-primary);
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: space-between;
162
+ flex-shrink: 0;
163
+ }
164
+
165
+ .score-left {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 16px;
169
+ }
170
+
171
+ .score-circle {
172
+ width: 56px;
173
+ height: 56px;
174
+ border-radius: 50%;
175
+ background: var(--primary);
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ justify-content: center;
180
+ color: white;
181
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
182
+ }
183
+
184
+ .score-number {
185
+ font-size: 20px;
186
+ font-weight: 700;
187
+ line-height: 1;
188
+ }
189
+
190
+ .score-label {
191
+ font-size: 10px;
192
+ font-weight: 600;
193
+ opacity: 0.9;
194
+ }
195
+
196
+ .score-stats {
197
+ display: flex;
198
+ gap: 24px;
199
+ }
200
+
201
+ .stat-item {
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 8px;
205
+ }
206
+
207
+ .stat-dot {
208
+ width: 12px;
209
+ height: 12px;
210
+ border-radius: 50%;
211
+ }
212
+
213
+ .stat-dot.correct { background: var(--ta-green); }
214
+ .stat-dot.wrong { background: var(--ta-red); }
215
+ .stat-dot.pending { background: var(--text-muted); }
216
+
217
+ .stat-text {
218
+ display: flex;
219
+ flex-direction: column;
220
+ }
221
+
222
+ .stat-value {
223
+ font-size: 16px;
224
+ font-weight: 700;
225
+ line-height: 1;
226
+ }
227
+
228
+ .stat-label {
229
+ font-size: 10px;
230
+ color: var(--text-muted);
231
+ text-transform: uppercase;
232
+ }
233
+
234
+ .progress-bar {
235
+ width: 200px;
236
+ height: 8px;
237
+ background: var(--bg-surface);
238
+ border-radius: 4px;
239
+ overflow: hidden;
240
+ }
241
+
242
+ .progress-fill {
243
+ height: 100%;
244
+ background: linear-gradient(90deg, var(--primary) 0%, var(--ta-green) 100%);
245
+ border-radius: 4px;
246
+ transition: width 0.3s;
247
+ }
248
+
249
+ .progress-text {
250
+ font-size: 11px;
251
+ color: var(--text-muted);
252
+ margin-left: 8px;
253
+ }
254
+
255
+ /* Quiz Content Area */
256
+ .quiz-content {
257
+ flex: 1;
258
+ overflow-y: auto;
259
+ padding: 24px;
260
+ }
261
+
262
+ .quiz-content::-webkit-scrollbar {
263
+ width: 6px;
264
+ }
265
+
266
+ .quiz-content::-webkit-scrollbar-track {
267
+ background: transparent;
268
+ }
269
+
270
+ .quiz-content::-webkit-scrollbar-thumb {
271
+ background: var(--border-primary);
272
+ border-radius: 10px;
273
+ }
274
+
275
+ /* Question Card */
276
+ .question-card {
277
+ background: var(--bg-surface);
278
+ border: 1px solid var(--border-primary);
279
+ border-radius: 12px;
280
+ padding: 20px 24px;
281
+ margin-bottom: 16px;
282
+ transition: all 0.2s;
283
+ }
284
+
285
+ .question-card:hover {
286
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
287
+ }
288
+
289
+ .question-header {
290
+ display: flex;
291
+ align-items: flex-start;
292
+ gap: 12px;
293
+ margin-bottom: 16px;
294
+ }
295
+
296
+ .question-number {
297
+ width: 28px;
298
+ height: 28px;
299
+ border-radius: 6px;
300
+ background: var(--bg-surface-tertiary);
301
+ color: var(--text-muted);
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ font-size: 12px;
306
+ font-weight: 700;
307
+ flex-shrink: 0;
308
+ }
309
+
310
+ .question-text {
311
+ font-size: 14px;
312
+ font-weight: 600;
313
+ line-height: 1.5;
314
+ flex: 1;
315
+ }
316
+
317
+ .question-card.correct .question-number {
318
+ background: var(--ta-green-bg);
319
+ color: var(--ta-green);
320
+ }
321
+
322
+ .question-card.wrong .question-number {
323
+ background: var(--ta-red-bg);
324
+ color: var(--ta-red);
325
+ }
326
+
327
+ .question-card.pending .question-number {
328
+ background: var(--bg-surface-tertiary);
329
+ color: var(--text-muted);
330
+ }
331
+
332
+ /* Options */
333
+ .options-list {
334
+ display: flex;
335
+ flex-direction: column;
336
+ gap: 8px;
337
+ }
338
+
339
+ .option-item {
340
+ display: flex;
341
+ align-items: center;
342
+ gap: 12px;
343
+ padding: 12px 16px;
344
+ border: 1px solid var(--border-primary);
345
+ border-radius: 8px;
346
+ cursor: pointer;
347
+ transition: all 0.2s;
348
+ background: var(--bg-surface-tertiary);
349
+ }
350
+
351
+ .option-item:hover {
352
+ border-color: var(--primary);
353
+ background: var(--ta-blue-bg);
354
+ }
355
+
356
+ .option-item.selected {
357
+ border-color: var(--primary);
358
+ background: var(--ta-blue-bg);
359
+ }
360
+
361
+ .option-item.correct-answer {
362
+ border-color: var(--ta-green);
363
+ background: var(--ta-green-bg);
364
+ }
365
+
366
+ .option-item.wrong-answer {
367
+ border-color: var(--ta-red);
368
+ background: var(--ta-red-bg);
369
+ }
370
+
371
+ .option-letter {
372
+ width: 24px;
373
+ height: 24px;
374
+ border-radius: 50%;
375
+ background: var(--bg-surface);
376
+ border: 2px solid var(--border-primary);
377
+ display: flex;
378
+ align-items: center;
379
+ justify-content: center;
380
+ font-size: 11px;
381
+ font-weight: 700;
382
+ flex-shrink: 0;
383
+ }
384
+
385
+ .option-item.correct-answer .option-letter {
386
+ background: var(--ta-green);
387
+ border-color: var(--ta-green);
388
+ color: white;
389
+ }
390
+
391
+ .option-item.wrong-answer .option-letter {
392
+ background: var(--ta-red);
393
+ border-color: var(--ta-red);
394
+ color: white;
395
+ }
396
+
397
+ .option-text {
398
+ flex: 1;
399
+ font-size: 13px;
400
+ color: var(--text-secondary);
401
+ }
402
+
403
+ .option-feedback {
404
+ display: flex;
405
+ align-items: center;
406
+ gap: 6px;
407
+ font-size: 12px;
408
+ font-weight: 600;
409
+ }
410
+
411
+ .feedback-icon {
412
+ font-size: 16px;
413
+ }
414
+
415
+ .feedback-correct {
416
+ color: var(--ta-green);
417
+ }
418
+
419
+ .feedback-wrong {
420
+ color: var(--ta-red);
421
+ }
422
+
423
+ /* Topic Badge */
424
+ .topic-badge {
425
+ display: inline-flex;
426
+ align-items: center;
427
+ gap: 6px;
428
+ padding: 4px 12px;
429
+ background: var(--ta-amber-bg);
430
+ border-radius: 12px;
431
+ font-size: 11px;
432
+ color: var(--ta-amber);
433
+ font-weight: 600;
434
+ margin-bottom: 12px;
435
+ }
436
+
437
+ /* Footer */
438
+ .quiz-footer {
439
+ padding: 16px 24px;
440
+ border-top: 1px solid var(--border-primary);
441
+ background: var(--bg-surface-tertiary);
442
+ display: flex;
443
+ justify-content: space-between;
444
+ align-items: center;
445
+ flex-shrink: 0;
446
+ }
447
+
448
+ .footer-info {
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 16px;
452
+ }
453
+
454
+ .question-count {
455
+ font-size: 13px;
456
+ color: var(--text-muted);
457
+ }
458
+
459
+ .question-count strong {
460
+ color: var(--text-primary);
461
+ }
462
+
463
+ .footer-actions {
464
+ display: flex;
465
+ gap: 12px;
466
+ }
467
+
468
+ .action-btn {
469
+ padding: 10px 20px;
470
+ border-radius: 8px;
471
+ border: none;
472
+ font-size: 13px;
473
+ font-weight: 600;
474
+ cursor: pointer;
475
+ transition: all 0.2s;
476
+ font-family: inherit;
477
+ }
478
+
479
+ .action-btn.secondary {
480
+ background: var(--bg-surface);
481
+ color: var(--text-secondary);
482
+ border: 1px solid var(--border-primary);
483
+ }
484
+
485
+ .action-btn.secondary:hover {
486
+ border-color: var(--primary);
487
+ color: var(--primary);
488
+ }
489
+
490
+ .action-btn.primary {
491
+ background: var(--primary);
492
+ color: white;
493
+ }
494
+
495
+ .action-btn.primary:hover {
496
+ background: var(--primary-active);
497
+ }
498
+
499
+ /* Explanation Box */
500
+ .explanation-box {
501
+ margin-top: 12px;
502
+ padding: 12px 16px;
503
+ background: var(--bg-surface-tertiary);
504
+ border-left: 3px solid var(--primary);
505
+ border-radius: 0 8px 8px 0;
506
+ font-size: 12px;
507
+ color: var(--text-secondary);
508
+ line-height: 1.6;
509
+ }
510
+
511
+ .explanation-title {
512
+ font-weight: 700;
513
+ color: var(--primary);
514
+ margin-bottom: 4px;
515
+ font-size: 11px;
516
+ text-transform: uppercase;
517
+ }
518
+
519
+ /* Responsive */
520
+ @media (max-width: 1024px) {
521
+ .mockup-container {
522
+ padding: 20px;
523
+ }
524
+
525
+ .score-bar {
526
+ flex-wrap: wrap;
527
+ gap: 16px;
528
+ }
529
+
530
+ .progress-bar {
531
+ width: 100%;
532
+ }
533
+ }
534
+ </style>
535
+ </head>
536
+ <body>
537
+ <div class="mockup-container">
538
+ <div class="mockup-header">
539
+ <span class="option-badge">OPTION A</span>
540
+ <h1>Inline Quiz Generation</h1>
541
+ <p>Quiz questions are generated immediately when recap is created. Users answer and see results instantly.</p>
542
+ </div>
543
+
544
+ <div class="quiz-container">
545
+ <!-- Header -->
546
+ <div class="quiz-header">
547
+ <div class="quiz-header-left">
548
+ <div class="quiz-icon">
549
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
550
+ <path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
551
+ </svg>
552
+ </div>
553
+ <div class="quiz-title">
554
+ <h3>Quiz - Bài giảng hoy</h3>
555
+ <p>Machine Learning Basics</p>
556
+ </div>
557
+ </div>
558
+ <div class="quiz-actions">
559
+ <button class="quiz-btn">
560
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
561
+ <path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
562
+ <path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
563
+ </svg>
564
+ Xem lại
565
+ </button>
566
+ <button class="quiz-btn primary">
567
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
568
+ <path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
569
+ </svg>
570
+ Xuất kết quả
571
+ </button>
572
+ </div>
573
+ </div>
574
+
575
+ <!-- Score Summary Bar -->
576
+ <div class="score-bar">
577
+ <div class="score-left">
578
+ <div class="score-circle">
579
+ <span class="score-number">85</span>
580
+ <span class="score-label">/100</span>
581
+ </div>
582
+ <div class="score-stats">
583
+ <div class="stat-item">
584
+ <div class="stat-dot correct"></div>
585
+ <div class="stat-text">
586
+ <div class="stat-value">17</div>
587
+ <div class="stat-label">Đúng</div>
588
+ </div>
589
+ </div>
590
+ <div class="stat-item">
591
+ <div class="stat-dot wrong"></div>
592
+ <div class="stat-text">
593
+ <div class="stat-value">3</div>
594
+ <div class="stat-label">Sai</div>
595
+ </div>
596
+ </div>
597
+ <div class="stat-item">
598
+ <div class="stat-dot pending"></div>
599
+ <div class="stat-text">
600
+ <div class="stat-value">0</div>
601
+ <div class="stat-label">Chưa làm</div>
602
+ </div>
603
+ </div>
604
+ </div>
605
+ </div>
606
+ <div style="display: flex; align-items: center;">
607
+ <div class="progress-bar">
608
+ <div class="progress-fill" style="width: 100%;"></div>
609
+ </div>
610
+ <span class="progress-text">20/20 câu</span>
611
+ </div>
612
+ </div>
613
+
614
+ <!-- Quiz Content -->
615
+ <div class="quiz-content">
616
+ <!-- Question 1 - Correct -->
617
+ <div class="question-card correct">
618
+ <div class="topic-badge">
619
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
620
+ <path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
621
+ </svg>
622
+ Giới thiệu Machine Learning
623
+ </div>
624
+ <div class="question-header">
625
+ <div class="question-number">1</div>
626
+ <div class="question-text">Machine Learning là gì?</div>
627
+ </div>
628
+ <div class="options-list">
629
+ <div class="option-item">
630
+ <div class="option-letter">A</div>
631
+ <span class="option-text">Lập trình máy tính truyền thống</span>
632
+ </div>
633
+ <div class="option-item correct-answer">
634
+ <div class="option-letter">B</div>
635
+ <span class="option-text">Lĩnh vực nghiên cứu giúp máy tính học từ dữ liệu</span>
636
+ <div class="option-feedback feedback-correct">
637
+ <span class="feedback-icon">✓</span>
638
+ <span>Đúng</span>
639
+ </div>
640
+ </div>
641
+ <div class="option-item">
642
+ <div class="option-letter">C</div>
643
+ <span class="option-text">Viết code tự động hoàn chỉnh</span>
644
+ </div>
645
+ <div class="option-item">
646
+ <div class="option-letter">D</div>
647
+ <span class="option-text">Sử dụng database để lưu trữ thông tin</span>
648
+ </div>
649
+ </div>
650
+ <div class="explanation-box">
651
+ <div class="explanation-title">Giải thích</div>
652
+ Machine Learning (Học máy) là một nhánh của AI tập trung vào việc phát triển thuật toán cho phép máy tính học hỏi từ dữ liệu mà không cần được lập trình tường minh.
653
+ </div>
654
+ </div>
655
+
656
+ <!-- Question 2 - Wrong -->
657
+ <div class="question-card wrong">
658
+ <div class="topic-badge">
659
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
660
+ <path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
661
+ </svg>
662
+ Types of Learning
663
+ </div>
664
+ <div class="question-header">
665
+ <div class="question-number">2</div>
666
+ <div class="question-text">Loại nào KHÔNG phải là một dạng của Machine Learning?</div>
667
+ </div>
668
+ <div class="options-list">
669
+ <div class="option-item">
670
+ <div class="option-letter">A</div>
671
+ <span class="option-text">Supervised Learning</span>
672
+ </div>
673
+ <div class="option-item">
674
+ <div class="option-letter">B</div>
675
+ <span class="option-text">Unsupervised Learning</span>
676
+ </div>
677
+ <div class="option-item wrong-answer">
678
+ <div class="option-letter">C</div>
679
+ <span class="option-text">Manual Learning</span>
680
+ <div class="option-feedback feedback-wrong">
681
+ <span class="feedback-icon">✗</span>
682
+ <span>Sai</span>
683
+ </div>
684
+ </div>
685
+ <div class="option-item correct-answer">
686
+ <div class="option-letter">D</div>
687
+ <span class="option-text">Reinforcement Learning</span>
688
+ <div class="option-feedback feedback-correct">
689
+ <span class="feedback-icon">✓</span>
690
+ <span>Đáp án đúng</span>
691
+ </div>
692
+ </div>
693
+ </div>
694
+ <div class="explanation-box">
695
+ <div class="explanation-title">Giải thích</div>
696
+ "Manual Learning" không phải là một dạng của Machine Learning. Các dạng chính bao gồm: Supervised, Unsupervised, Semi-supervised, và Reinforcement Learning.
697
+ </div>
698
+ </div>
699
+
700
+ <!-- Question 3 - Pending -->
701
+ <div class="question-card pending">
702
+ <div class="topic-badge">
703
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
704
+ <path d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/>
705
+ </svg>
706
+ Data Preprocessing
707
+ </div>
708
+ <div class="question-header">
709
+ <div class="question-number">3</div>
710
+ <div class="question-text">Bước nào KHÔNG thuộc quá trình tiền xử lý dữ liệu?</div>
711
+ </div>
712
+ <div class="options-list">
713
+ <div class="option-item">
714
+ <div class="option-letter">A</div>
715
+ <span class="option-text">Xử lý giá trị thiếu (Missing values)</span>
716
+ </div>
717
+ <div class="option-item">
718
+ <div class="option-letter">B</div>
719
+ <span class="option-text">Chuẩn hóa dữ liệu (Normalization)</span>
720
+ </div>
721
+ <div class="option-item">
722
+ <div class="option-letter">C</div>
723
+ <span class="option-text">Mã hóa biến phân loại (Encoding)</span>
724
+ </div>
725
+ <div class="option-item">
726
+ <div class="option-letter">D</div>
727
+ <span class="option-text">Huấn luyện mô hình (Model Training)</span>
728
+ </div>
729
+ </div>
730
+ </div>
731
+
732
+ </div>
733
+
734
+ <!-- Footer -->
735
+ <div class="quiz-footer">
736
+ <div class="footer-info">
737
+ <span class="question-count">Tổng <strong>20</strong> câu hỏi</span>
738
+ <span style="color: var(--ta-green); font-weight: 600;">✨ Hoàn thành 100%</span>
739
+ </div>
740
+ <div class="footer-actions">
741
+ <button class="action-btn secondary">
742
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
743
+ <path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
744
+ </svg>
745
+ Làm lại
746
+ </button>
747
+ <button class="action-btn primary">
748
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
749
+ <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
750
+ </svg>
751
+ Xem chi tiết
752
+ </button>
753
+ </div>
754
+ </div>
755
+ </div>
756
+ </div>
757
+ </body>
758
+ </html>
src/components/ta/QuizMockupOptionB.html ADDED
@@ -0,0 +1,797 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Quiz Mockup - Option B (Interactive Quiz Mode)</title>
7
+ <style>
8
+ :root {
9
+ --primary: #3b82f6;
10
+ --primary-active: #2563eb;
11
+ --bg-surface: #ffffff;
12
+ --bg-surface-secondary: #f8fafc;
13
+ --bg-surface-tertiary: #f1f5f9;
14
+ --text-primary: #1e293b;
15
+ --text-secondary: #475569;
16
+ --text-muted: #94a3b8;
17
+ --border-primary: #e2e8f0;
18
+ --ta-red: #ef4444;
19
+ --ta-red-bg: rgba(239, 68, 68, 0.1);
20
+ --ta-amber: #f59e0b;
21
+ --ta-amber-bg: rgba(245, 158, 11, 0.1);
22
+ --ta-blue: #3b82f6;
23
+ --ta-blue-bg: rgba(59, 130, 246, 0.1);
24
+ --ta-green: #10b981;
25
+ --ta-green-bg: rgba(16, 185, 129, 0.1);
26
+ }
27
+
28
+ * {
29
+ margin: 0;
30
+ padding: 0;
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ body {
35
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
36
+ background: var(--bg-surface-secondary);
37
+ color: var(--text-primary);
38
+ line-height: 1.5;
39
+ }
40
+
41
+ .mockup-container {
42
+ max-width: 1400px;
43
+ margin: 0 auto;
44
+ padding: 40px 24px;
45
+ }
46
+
47
+ .mockup-header {
48
+ text-align: center;
49
+ margin-bottom: 40px;
50
+ }
51
+
52
+ .mockup-header h1 {
53
+ font-size: 28px;
54
+ font-weight: 700;
55
+ color: var(--primary);
56
+ margin-bottom: 8px;
57
+ }
58
+
59
+ .mockup-header p {
60
+ font-size: 14px;
61
+ color: var(--text-muted);
62
+ }
63
+
64
+ .option-badge {
65
+ display: inline-block;
66
+ padding: 6px 16px;
67
+ background: linear-gradient(135deg, var(--ta-amber) 0%, #f97316 100%);
68
+ color: white;
69
+ border-radius: 20px;
70
+ font-size: 12px;
71
+ font-weight: 600;
72
+ margin-bottom: 16px;
73
+ }
74
+
75
+ /* Main Card Container */
76
+ .quiz-container {
77
+ background: var(--bg-surface-secondary);
78
+ border: 1px solid var(--border-primary);
79
+ border-radius: 12px;
80
+ overflow: hidden;
81
+ height: 800px;
82
+ display: flex;
83
+ flex-direction: column;
84
+ }
85
+
86
+ /* Header */
87
+ .quiz-header {
88
+ padding: 16px 24px;
89
+ border-bottom: 1px solid var(--border-primary);
90
+ background: var(--bg-surface-tertiary);
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ flex-shrink: 0;
95
+ }
96
+
97
+ .quiz-header-left {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 12px;
101
+ }
102
+
103
+ .quiz-icon {
104
+ width: 32px;
105
+ height: 32px;
106
+ background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
107
+ border-radius: 8px;
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ color: white;
112
+ }
113
+
114
+ .quiz-title h3 {
115
+ font-size: 15px;
116
+ font-weight: 700;
117
+ margin: 0;
118
+ }
119
+
120
+ .quiz-title p {
121
+ font-size: 12px;
122
+ color: var(--text-muted);
123
+ margin-top: 2px;
124
+ }
125
+
126
+ .quiz-timer {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 6px;
130
+ padding: 6px 12px;
131
+ background: var(--ta-red-bg);
132
+ border-radius: 8px;
133
+ color: var(--ta-red);
134
+ font-size: 12px;
135
+ font-weight: 600;
136
+ }
137
+
138
+ /* Empty State / Start Screen */
139
+ .quiz-start-screen {
140
+ flex: 1;
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ justify-content: center;
145
+ padding: 40px;
146
+ text-align: center;
147
+ }
148
+
149
+ .start-icon {
150
+ width: 120px;
151
+ height: 120px;
152
+ background: linear-gradient(135deg, var(--ta-blue-bg) 0%, rgba(59, 130, 246, 0.2) 100%);
153
+ border-radius: 50%;
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ margin-bottom: 24px;
158
+ }
159
+
160
+ .start-icon svg {
161
+ width: 56px;
162
+ height: 56px;
163
+ color: var(--primary);
164
+ }
165
+
166
+ .start-title {
167
+ font-size: 24px;
168
+ font-weight: 700;
169
+ color: var(--text-primary);
170
+ margin-bottom: 12px;
171
+ }
172
+
173
+ .start-description {
174
+ font-size: 14px;
175
+ color: var(--text-muted);
176
+ max-width: 400px;
177
+ margin-bottom: 32px;
178
+ line-height: 1.6;
179
+ }
180
+
181
+ .start-info-grid {
182
+ display: grid;
183
+ grid-template-columns: repeat(3, 1fr);
184
+ gap: 16px;
185
+ margin-bottom: 32px;
186
+ width: 100%;
187
+ max-width: 500px;
188
+ }
189
+
190
+ .info-card {
191
+ padding: 20px 16px;
192
+ background: var(--bg-surface);
193
+ border: 1px solid var(--border-primary);
194
+ border-radius: 12px;
195
+ text-align: center;
196
+ }
197
+
198
+ .info-value {
199
+ font-size: 24px;
200
+ font-weight: 700;
201
+ color: var(--primary);
202
+ margin-bottom: 4px;
203
+ }
204
+
205
+ .info-label {
206
+ font-size: 11px;
207
+ color: var(--text-muted);
208
+ text-transform: uppercase;
209
+ font-weight: 600;
210
+ }
211
+
212
+ .start-btn {
213
+ padding: 16px 48px;
214
+ background: linear-gradient(135deg, var(--primary) 0%, #6366f1 100%);
215
+ color: white;
216
+ border: none;
217
+ border-radius: 12px;
218
+ font-size: 16px;
219
+ font-weight: 700;
220
+ cursor: pointer;
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 12px;
224
+ transition: all 0.3s;
225
+ box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
226
+ font-family: inherit;
227
+ }
228
+
229
+ .start-btn:hover {
230
+ transform: translateY(-2px);
231
+ box-shadow: 0 12px 32px rgba(59, 130, 246, 0.4);
232
+ }
233
+
234
+ /* Active Quiz Screen */
235
+ .quiz-active-screen {
236
+ flex: 1;
237
+ display: flex;
238
+ flex-direction: column;
239
+ overflow: hidden;
240
+ }
241
+
242
+ /* Progress Bar */
243
+ .quiz-progress-bar {
244
+ height: 4px;
245
+ background: var(--bg-surface-tertiary);
246
+ display: flex;
247
+ flex-shrink: 0;
248
+ }
249
+
250
+ .progress-segment {
251
+ flex: 1;
252
+ height: 100%;
253
+ background: var(--border-primary);
254
+ transition: background 0.3s;
255
+ }
256
+
257
+ .progress-segment.completed {
258
+ background: var(--ta-green);
259
+ }
260
+
261
+ .progress-segment.current {
262
+ background: var(--primary);
263
+ }
264
+
265
+ .progress-segment.pending {
266
+ background: var(--bg-surface-tertiary);
267
+ }
268
+
269
+ /* Question Card Area */
270
+ .question-area {
271
+ flex: 1;
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: center;
275
+ padding: 32px 48px;
276
+ overflow-y: auto;
277
+ }
278
+
279
+ .question-card {
280
+ background: var(--bg-surface);
281
+ border: 1px solid var(--border-primary);
282
+ border-radius: 16px;
283
+ padding: 40px;
284
+ max-width: 600px;
285
+ width: 100%;
286
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
287
+ }
288
+
289
+ .question-card-header {
290
+ display: flex;
291
+ justify-content: space-between;
292
+ align-items: center;
293
+ margin-bottom: 24px;
294
+ }
295
+
296
+ .question-badge {
297
+ padding: 6px 14px;
298
+ background: var(--ta-amber-bg);
299
+ color: var(--ta-amber);
300
+ border-radius: 20px;
301
+ font-size: 11px;
302
+ font-weight: 700;
303
+ text-transform: uppercase;
304
+ }
305
+
306
+ .question-number-badge {
307
+ padding: 6px 14px;
308
+ background: var(--bg-surface-tertiary);
309
+ color: var(--text-muted);
310
+ border-radius: 8px;
311
+ font-size: 13px;
312
+ font-weight: 600;
313
+ }
314
+
315
+ .question-text {
316
+ font-size: 18px;
317
+ font-weight: 600;
318
+ line-height: 1.5;
319
+ margin-bottom: 32px;
320
+ color: var(--text-primary);
321
+ }
322
+
323
+ /* Options */
324
+ .options-grid {
325
+ display: flex;
326
+ flex-direction: column;
327
+ gap: 12px;
328
+ }
329
+
330
+ .option-card {
331
+ display: flex;
332
+ align-items: center;
333
+ gap: 16px;
334
+ padding: 18px 22px;
335
+ border: 2px solid var(--border-primary);
336
+ border-radius: 12px;
337
+ cursor: pointer;
338
+ transition: all 0.2s;
339
+ background: var(--bg-surface);
340
+ }
341
+
342
+ .option-card:hover {
343
+ border-color: var(--primary);
344
+ background: var(--ta-blue-bg);
345
+ transform: translateX(4px);
346
+ }
347
+
348
+ .option-card.selected {
349
+ border-color: var(--primary);
350
+ background: var(--ta-blue-bg);
351
+ }
352
+
353
+ .option-letter {
354
+ width: 36px;
355
+ height: 36px;
356
+ border-radius: 10px;
357
+ background: var(--bg-surface-tertiary);
358
+ border: 2px solid var(--border-primary);
359
+ display: flex;
360
+ align-items: center;
361
+ justify-content: center;
362
+ font-size: 14px;
363
+ font-weight: 700;
364
+ color: var(--text-muted);
365
+ flex-shrink: 0;
366
+ transition: all 0.2s;
367
+ }
368
+
369
+ .option-card.selected .option-letter {
370
+ background: var(--primary);
371
+ border-color: var(--primary);
372
+ color: white;
373
+ }
374
+
375
+ .option-text {
376
+ flex: 1;
377
+ font-size: 14px;
378
+ color: var(--text-secondary);
379
+ line-height: 1.5;
380
+ }
381
+
382
+ /* Quiz Footer */
383
+ .quiz-footer {
384
+ padding: 20px 32px;
385
+ border-top: 1px solid var(--border-primary);
386
+ background: var(--bg-surface);
387
+ display: flex;
388
+ justify-content: space-between;
389
+ align-items: center;
390
+ flex-shrink: 0;
391
+ }
392
+
393
+ .navigation-btn {
394
+ padding: 12px 24px;
395
+ border-radius: 10px;
396
+ border: 1px solid var(--border-primary);
397
+ background: var(--bg-surface);
398
+ color: var(--text-secondary);
399
+ font-size: 13px;
400
+ font-weight: 600;
401
+ cursor: pointer;
402
+ display: flex;
403
+ align-items: center;
404
+ gap: 8px;
405
+ transition: all 0.2s;
406
+ font-family: inherit;
407
+ }
408
+
409
+ .navigation-btn:hover:not(:disabled) {
410
+ border-color: var(--primary);
411
+ color: var(--primary);
412
+ }
413
+
414
+ .navigation-btn:disabled {
415
+ opacity: 0.5;
416
+ cursor: not-allowed;
417
+ }
418
+
419
+ .submit-btn {
420
+ padding: 12px 32px;
421
+ border-radius: 10px;
422
+ border: none;
423
+ background: linear-gradient(135deg, var(--ta-green) 0%, #059669 100%);
424
+ color: white;
425
+ font-size: 14px;
426
+ font-weight: 700;
427
+ cursor: pointer;
428
+ display: flex;
429
+ align-items: center;
430
+ gap: 10px;
431
+ transition: all 0.3s;
432
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
433
+ font-family: inherit;
434
+ }
435
+
436
+ .submit-btn:hover {
437
+ transform: translateY(-2px);
438
+ box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4);
439
+ }
440
+
441
+ /* Results Screen */
442
+ .results-screen {
443
+ flex: 1;
444
+ display: flex;
445
+ flex-direction: column;
446
+ align-items: center;
447
+ justify-content: center;
448
+ padding: 40px;
449
+ text-align: center;
450
+ }
451
+
452
+ .results-icon {
453
+ width: 100px;
454
+ height: 100px;
455
+ background: linear-gradient(135deg, var(--ta-green) 0%, #059669 100%);
456
+ border-radius: 50%;
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+ margin-bottom: 24px;
461
+ animation: pulse 2s infinite;
462
+ }
463
+
464
+ @keyframes pulse {
465
+ 0%, 100% { transform: scale(1); }
466
+ 50% { transform: scale(1.05); }
467
+ }
468
+
469
+ .results-icon svg {
470
+ width: 48px;
471
+ height: 48px;
472
+ color: white;
473
+ }
474
+
475
+ .results-title {
476
+ font-size: 28px;
477
+ font-weight: 700;
478
+ color: var(--text-primary);
479
+ margin-bottom: 8px;
480
+ }
481
+
482
+ .results-subtitle {
483
+ font-size: 14px;
484
+ color: var(--text-muted);
485
+ margin-bottom: 32px;
486
+ }
487
+
488
+ .score-display {
489
+ width: 180px;
490
+ height: 180px;
491
+ border-radius: 50%;
492
+ background: conic-gradient(var(--primary) 0deg, var(--bg-surface-tertiary) 0deg);
493
+ display: flex;
494
+ align-items: center;
495
+ justify-content: center;
496
+ margin-bottom: 32px;
497
+ position: relative;
498
+ }
499
+
500
+ .score-display::before {
501
+ content: '';
502
+ position: absolute;
503
+ width: 150px;
504
+ height: 150px;
505
+ background: var(--bg-surface);
506
+ border-radius: 50%;
507
+ }
508
+
509
+ .score-content {
510
+ position: relative;
511
+ z-index: 1;
512
+ }
513
+
514
+ .score-number {
515
+ font-size: 48px;
516
+ font-weight: 800;
517
+ color: var(--primary);
518
+ line-height: 1;
519
+ }
520
+
521
+ .score-text {
522
+ font-size: 12px;
523
+ color: var(--text-muted);
524
+ margin-top: 4px;
525
+ }
526
+
527
+ .results-stats {
528
+ display: grid;
529
+ grid-template-columns: repeat(3, 1fr);
530
+ gap: 16px;
531
+ margin-bottom: 32px;
532
+ width: 100%;
533
+ max-width: 500px;
534
+ }
535
+
536
+ .result-stat {
537
+ padding: 20px 16px;
538
+ background: var(--bg-surface);
539
+ border: 1px solid var(--border-primary);
540
+ border-radius: 12px;
541
+ text-align: center;
542
+ }
543
+
544
+ .result-stat-icon {
545
+ font-size: 24px;
546
+ margin-bottom: 8px;
547
+ }
548
+
549
+ .result-stat-value {
550
+ font-size: 24px;
551
+ font-weight: 700;
552
+ color: var(--text-primary);
553
+ }
554
+
555
+ .result-stat-label {
556
+ font-size: 11px;
557
+ color: var(--text-muted);
558
+ text-transform: uppercase;
559
+ font-weight: 600;
560
+ margin-top: 2px;
561
+ }
562
+
563
+ .results-actions {
564
+ display: flex;
565
+ gap: 12px;
566
+ }
567
+
568
+ .action-btn {
569
+ padding: 14px 28px;
570
+ border-radius: 10px;
571
+ font-size: 14px;
572
+ font-weight: 600;
573
+ cursor: pointer;
574
+ transition: all 0.2s;
575
+ font-family: inherit;
576
+ }
577
+
578
+ .action-btn.secondary {
579
+ background: var(--bg-surface);
580
+ color: var(--text-secondary);
581
+ border: 1px solid var(--border-primary);
582
+ }
583
+
584
+ .action-btn.secondary:hover {
585
+ border-color: var(--primary);
586
+ color: var(--primary);
587
+ }
588
+
589
+ .action-btn.primary {
590
+ background: var(--primary);
591
+ color: white;
592
+ border: none;
593
+ }
594
+
595
+ .action-btn.primary:hover {
596
+ background: var(--primary-active);
597
+ }
598
+
599
+ /* Performance Badge */
600
+ .performance-badge {
601
+ display: inline-flex;
602
+ align-items: center;
603
+ gap: 6px;
604
+ padding: 8px 16px;
605
+ background: var(--ta-green-bg);
606
+ border-radius: 20px;
607
+ color: var(--ta-green);
608
+ font-size: 13px;
609
+ font-weight: 600;
610
+ margin-bottom: 24px;
611
+ }
612
+
613
+ /* Responsive */
614
+ @media (max-width: 1024px) {
615
+ .mockup-container {
616
+ padding: 20px;
617
+ }
618
+ }
619
+ </style>
620
+ </head>
621
+ <body>
622
+ <div class="mockup-container">
623
+ <div class="mockup-header">
624
+ <span class="option-badge">OPTION B</span>
625
+ <h1>Interactive Quiz Mode</h1>
626
+ <p>Game-like experience with one question at a time, navigation controls, and final score submission.</p>
627
+ </div>
628
+
629
+ <div class="quiz-container">
630
+ <!-- Header -->
631
+ <div class="quiz-header">
632
+ <div class="quiz-header-left">
633
+ <div class="quiz-icon">
634
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
635
+ <path d="M13 10V3L4 14h7v7l9-11h-7z"/>
636
+ </svg>
637
+ </div>
638
+ <div class="quiz-title">
639
+ <h3>Quiz Challenge</h3>
640
+ <p>Machine Learning Basics</p>
641
+ </div>
642
+ </div>
643
+ <div class="quiz-timer">
644
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
645
+ <circle cx="12" cy="12" r="10"/>
646
+ <path d="M12 6v6l4 2"/>
647
+ </svg>
648
+ 15:00
649
+ </div>
650
+ </div>
651
+
652
+ <!-- Progress Bar -->
653
+ <div class="quiz-progress-bar">
654
+ <div class="progress-segment completed"></div>
655
+ <div class="progress-segment completed"></div>
656
+ <div class="progress-segment current"></div>
657
+ <div class="progress-segment pending"></div>
658
+ <div class="progress-segment pending"></div>
659
+ <div class="progress-segment pending"></div>
660
+ <div class="progress-segment pending"></div>
661
+ <div class="progress-segment pending"></div>
662
+ <div class="progress-segment pending"></div>
663
+ <div class="progress-segment pending"></div>
664
+ </div>
665
+
666
+ <!-- Active Quiz Screen -->
667
+ <div class="quiz-active-screen">
668
+ <!-- Question Area -->
669
+ <div class="question-area">
670
+ <div class="question-card">
671
+ <div class="question-card-header">
672
+ <span class="question-badge">Kiến thức cơ bản</span>
673
+ <span class="question-number-badge">Câu 3/10</span>
674
+ </div>
675
+
676
+ <div class="question-text">
677
+ Trong Supervised Learning, dữ liệu huấn luyện cần có đặc điểm gì quan trọng nhất?
678
+ </div>
679
+
680
+ <div class="options-grid">
681
+ <div class="option-card">
682
+ <div class="option-letter">A</div>
683
+ <span class="option-text">Phải có kích thước lớn hơn 1GB</span>
684
+ </div>
685
+ <div class="option-card selected">
686
+ <div class="option-letter">B</div>
687
+ <span class="option-text">Có nhãn (label) hoặc target variable</span>
688
+ </div>
689
+ <div class="option-card">
690
+ <div class="option-letter">C</div>
691
+ <span class="option-text">Chỉ chứa dữ liệu số</span>
692
+ </div>
693
+ <div class="option-card">
694
+ <div class="option-letter">D</div>
695
+ <span class="option-text">Phải được chuẩn hóa trước</span>
696
+ </div>
697
+ </div>
698
+ </div>
699
+ </div>
700
+
701
+ <!-- Footer Navigation -->
702
+ <div class="quiz-footer">
703
+ <button class="navigation-btn" disabled>
704
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
705
+ <path d="M15 19l-7-7 7-7"/>
706
+ </svg>
707
+ Previous
708
+ </button>
709
+ <div style="display: flex; gap: 12px;">
710
+ <button class="navigation-btn">
711
+ Next
712
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
713
+ <path d="M9 5l7 7-7 7"/>
714
+ </svg>
715
+ </button>
716
+ <button class="submit-btn">
717
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
718
+ <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
719
+ </svg>
720
+ Nộp bài
721
+ </button>
722
+ </div>
723
+ </div>
724
+ </div>
725
+ </div>
726
+
727
+ <!-- Results Screen Section -->
728
+ <div style="margin-top: 60px;">
729
+ <div class="mockup-header" style="margin-bottom: 24px;">
730
+ <h2 style="font-size: 20px; color: var(--text-primary); margin-bottom: 8px;">Results Screen</h2>
731
+ <p style="font-size: 13px;">Hiển thị kết quả sau khi nộp bài</p>
732
+ </div>
733
+
734
+ <div class="quiz-container">
735
+ <div class="results-screen">
736
+ <div class="performance-badge">
737
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
738
+ <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
739
+ </svg>
740
+ Xuất sắc!
741
+ </div>
742
+
743
+ <div class="score-display" style="background: conic-gradient(var(--ta-green) 306deg, var(--bg-surface-tertiary) 306deg);">
744
+ <div class="score-content">
745
+ <div class="score-number">85</div>
746
+ <div class="score-text">/ 100 điểm</div>
747
+ </div>
748
+ </div>
749
+
750
+ <h3 class="results-title">Hoàn thành tốt!</h3>
751
+ <p class="results-subtitle">Bạn đã làm rất tốt bài quiz về Machine Learning Basics</p>
752
+
753
+ <div class="results-stats">
754
+ <div class="result-stat">
755
+ <div class="result-stat-icon">✓</div>
756
+ <div class="result-stat-value">8.5</div>
757
+ <div class="result-stat-label">Đúng</div>
758
+ </div>
759
+ <div class="result-stat">
760
+ <div class="result-stat-icon">✗</div>
761
+ <div class="result-stat-value">1.5</div>
762
+ <div class="result-stat-label">Sai</div>
763
+ </div>
764
+ <div class="result-stat">
765
+ <div class="result-stat-icon">⏱️</div>
766
+ <div class="result-stat-value">12:34</div>
767
+ <div class="result-stat-label">Thời gian</div>
768
+ </div>
769
+ </div>
770
+
771
+ <div class="results-actions">
772
+ <button class="action-btn secondary">
773
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
774
+ <path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
775
+ </svg>
776
+ Làm lại
777
+ </button>
778
+ <button class="action-btn secondary">
779
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
780
+ <path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
781
+ <path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
782
+ </svg>
783
+ Xem chi tiết
784
+ </button>
785
+ <button class="action-btn primary">
786
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
787
+ <path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
788
+ </svg>
789
+ Xuất kết quả
790
+ </button>
791
+ </div>
792
+ </div>
793
+ </div>
794
+ </div>
795
+ </div>
796
+ </body>
797
+ </html>
src/components/ta/RecapWorkflow.jsx CHANGED
@@ -27,6 +27,15 @@ const RecapWorkflow = (props) => {
27
  const [refineQuery, setRefineQuery] = useState('');
28
  const [selectedChips, setSelectedChips] = useState([]);
29
 
 
 
 
 
 
 
 
 
 
30
  const refineOptions = [
31
  { id: 'shorter', label: '✨ Ngắn gọn', prompt: 'Làm ngắn gọn lại, súc tích hơn.' },
32
  { id: 'funny', label: '✨ Hài hước', prompt: 'Viết lại với giọng văn hài hước, năng lượng hơn.' },
@@ -60,43 +69,6 @@ const RecapWorkflow = (props) => {
60
  }
61
  };
62
 
63
- const renderMarkdown = (content) => {
64
- return { __html: marked.parse(content || '') };
65
- };
66
-
67
- // Extract deadlines from summary content (look for homework/bài tập patterns)
68
- const extractDeadlines = (content) => {
69
- if (!content) return [];
70
-
71
- // Tìm các dòng có từ khóa: bài tập, homework, deadline, nộp, due, assignment
72
- const patterns = [
73
- /[-•*]\s*.*?(?:bài tập|homework|assignment|deadline|hạn|nộp|due date|làm|chuẩn bị).*?(?:\n|$)/gi,
74
- /[-•*]\s*(?:đóng|gửi|submit).*?(?:bài|file|nội dung).*?(?:\n|$)/gi,
75
- /\d{1,2}[\/-]\d{1,2}(?:\/\d{2,4})?\s*[.:]\s*.+?[\n]/gi
76
- ];
77
-
78
- const deadlines = new Set();
79
-
80
- for (const pattern of patterns) {
81
- const matches = content.match(pattern);
82
- if (matches) {
83
- matches.forEach(match => {
84
- const cleanMatch = match.trim()
85
- .replace(/^[-•*]\s*/, '')
86
- .replace(/\s+/g, ' ')
87
- .slice(0, 150);
88
- if (cleanMatch.length > 8 && cleanMatch.length < 151) {
89
- deadlines.add(cleanMatch);
90
- }
91
- });
92
- }
93
- }
94
-
95
- return Array.from(deadlines).slice(0, 5);
96
- };
97
-
98
- const deadlines = extractDeadlines(aiPreview?.content || '');
99
-
100
  return (
101
  <div className="animate-fade">
102
  <div style={{ display: 'grid', gridTemplateColumns: '320px 1fr', gap: '24px', alignItems: 'start' }}>
@@ -182,7 +154,7 @@ const RecapWorkflow = (props) => {
182
 
183
  {/* Right Panel - Content Area */}
184
  <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
185
- <div className="ta-card-premium" style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 140px)', overflow: 'hidden' }}>
186
 
187
  {/* Header */}
188
  <div style={{ padding: '16px 24px', borderBottom: '1px solid var(--border-primary)', background: 'var(--bg-surface-tertiary)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
@@ -201,10 +173,14 @@ const RecapWorkflow = (props) => {
201
 
202
  {/* Processing Overlay */}
203
  {uploading && (
204
- <div style={{ position: 'absolute', inset: 0, background: 'rgba(15, 23, 42, 0.9)', zIndex: 30, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', color: 'white' }}>
205
- <Icons.FiCpu className="spin" size={56} color="var(--primary)" />
206
- <h4 style={{ marginTop: '20px', fontWeight: 700, fontFamily: 'inherit', fontSize: '18px' }}>AI đang phân tích...</h4>
207
- <p style={{ fontSize: '14px', color: 'rgba(255,255,255,0.85)', marginTop: '8px' }}>Đang đọc file và tạo tóm tắt bài giảng</p>
 
 
 
 
208
  </div>
209
  )}
210
 
@@ -258,7 +234,17 @@ const RecapWorkflow = (props) => {
258
  <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
259
  {deadlines.map((deadline, idx) => (
260
  <div key={idx} style={{ padding: '12px', background: 'var(--bg-surface-tertiary)', borderRadius: '8px', borderLeft: '3px solid var(--ta-amber)', fontSize: '13px', lineHeight: '1.5' }}>
261
- {deadline}
 
 
 
 
 
 
 
 
 
 
262
  </div>
263
  ))}
264
  </div>
 
27
  const [refineQuery, setRefineQuery] = useState('');
28
  const [selectedChips, setSelectedChips] = useState([]);
29
 
30
+ // Use deadlines from aiPreview if available, otherwise fall back to empty array
31
+ const deadlines = (aiPreview?.deadlines && Array.isArray(aiPreview.deadlines) && aiPreview.deadlines.length > 0)
32
+ ? aiPreview.deadlines
33
+ : [];
34
+
35
+ const renderMarkdown = (content) => {
36
+ return { __html: marked.parse(content || '') };
37
+ };
38
+
39
  const refineOptions = [
40
  { id: 'shorter', label: '✨ Ngắn gọn', prompt: 'Làm ngắn gọn lại, súc tích hơn.' },
41
  { id: 'funny', label: '✨ Hài hước', prompt: 'Viết lại với giọng văn hài hước, năng lượng hơn.' },
 
69
  }
70
  };
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  return (
73
  <div className="animate-fade">
74
  <div style={{ display: 'grid', gridTemplateColumns: '320px 1fr', gap: '24px', alignItems: 'start' }}>
 
154
 
155
  {/* Right Panel - Content Area */}
156
  <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
157
+ <div className="ta-card-premium" style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 140px)', overflow: 'hidden', position: 'relative' }}>
158
 
159
  {/* Header */}
160
  <div style={{ padding: '16px 24px', borderBottom: '1px solid var(--border-primary)', background: 'var(--bg-surface-tertiary)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
 
173
 
174
  {/* Processing Overlay */}
175
  {uploading && (
176
+ <div className="processing-overlay-card">
177
+ <div className="processing-card-content">
178
+ <Icons.FiCpu className="spin" size={48} color="var(--primary)" />
179
+ <div style={{ textAlign: 'center' }}>
180
+ <h4 className="processing-card-title">AI đang phân tích...</h4>
181
+ <p className="processing-card-text">Đang đọc file và tạo tóm tắt bài giảng</p>
182
+ </div>
183
+ </div>
184
  </div>
185
  )}
186
 
 
234
  <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
235
  {deadlines.map((deadline, idx) => (
236
  <div key={idx} style={{ padding: '12px', background: 'var(--bg-surface-tertiary)', borderRadius: '8px', borderLeft: '3px solid var(--ta-amber)', fontSize: '13px', lineHeight: '1.5' }}>
237
+ <div style={{ fontWeight: 600, marginBottom: '4px' }}>{deadline.title || deadline}</div>
238
+ {deadline.due_date && (
239
+ <div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '4px' }}>
240
+ 📅 {deadline.due_date}
241
+ </div>
242
+ )}
243
+ {deadline.description && (
244
+ <div style={{ fontSize: '12px', marginTop: '4px' }}>
245
+ {deadline.description}
246
+ </div>
247
+ )}
248
  </div>
249
  ))}
250
  </div>
src/pages/TADashboard.css CHANGED
@@ -355,6 +355,49 @@
355
  border-radius: 12px;
356
  }
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  /* Toast Notification */
359
  .toast-container {
360
  position: fixed;
 
355
  border-radius: 12px;
356
  }
357
 
358
+ /* Processing overlay với card trắng (dành cho Recap Workflow) */
359
+ .processing-overlay-card {
360
+ position: absolute;
361
+ top: 0;
362
+ left: 0;
363
+ right: 0;
364
+ bottom: 0;
365
+ display: flex;
366
+ flex-direction: column;
367
+ align-items: center;
368
+ justify-content: center;
369
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(147, 197, 253, 0.25) 100%);
370
+ backdrop-filter: blur(4px);
371
+ z-index: 100;
372
+ border-radius: 12px;
373
+ }
374
+
375
+ .processing-card-content {
376
+ display: flex;
377
+ flex-direction: column;
378
+ align-items: center;
379
+ gap: 16px;
380
+ padding: 32px;
381
+ background: rgba(255, 255, 255, 0.95);
382
+ border-radius: 16px;
383
+ box-shadow: 0 8px 32px rgba(59, 130, 246, 0.2);
384
+ }
385
+
386
+ .processing-card-title {
387
+ margin: 0;
388
+ font-weight: 700;
389
+ font-size: 16px;
390
+ color: #1e3a8a;
391
+ font-family: inherit;
392
+ }
393
+
394
+ .processing-card-text {
395
+ margin: 4px 0 0;
396
+ font-size: 13px;
397
+ color: #4b5563;
398
+ font-family: inherit;
399
+ }
400
+
401
  /* Toast Notification */
402
  .toast-container {
403
  position: fixed;
src/pages/TADashboard.jsx CHANGED
@@ -144,14 +144,89 @@ const TADashboard = () => {
144
  const r = aiConfig.recap;
145
  const pronouns = r.pronouns || g.pronouns;
146
  return `[ROLE] Bạn là một Trợ Giảng (Teaching Assistant) xuất sắc.
147
- [OBJECTIVE] Phân tích nội dung buổi học và viết bài Recap chất lượng cao.
148
  [RULES]
149
  - Xưng hô bắt buộc: ${pronouns}.
150
  - Tone giọng: ${r.tone}.
151
  - Trọng tâm (Focus): ${r.highlight}.
152
  - Quy tắc chung:\\n${compileRules(g.rules)}\\n${g.instruction}
153
  - Quy tắc Recap:\\n${compileRules(r.rules)}\\n${r.instruction}
154
- [OUTPUT FORMAT] CHỈ xuất ra nội dung bài viết định dạng Markdown. TUYỆT ĐỐI KHÔNG kèm theo các câu giao tiếp của AI (như 'Đây là...', 'Vâng, tôi hiểu').`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  };
156
 
157
  const buildAnnouncementPrompt = (p, c) => {
@@ -222,7 +297,10 @@ const TADashboard = () => {
222
  addToast('Đã giải quyết');
223
  fetchData();
224
  }
225
- } catch (error) {}
 
 
 
226
  };
227
 
228
  const handleApproveSummary = async (draftId, spaceId, autoPilotData = null) => {
@@ -230,18 +308,26 @@ const TADashboard = () => {
230
  try {
231
  const activeContent = autoPilotData?.content || aiPreview?.content || '';
232
  const activeType = autoPilotData?.draft_type || aiPreview?.draft_type || '';
 
233
 
234
  if (aiPreview?.id === draftId) await taService.updateSummaryDraft(draftId, spaceId, { content: activeContent });
235
  const res = await taService.approveSummary(draftId, spaceId);
236
- if (res.success && selectedSpaces.length > 1 && (!aiPreview || aiPreview.status === 'pending')) {
 
 
 
 
237
  const otherSpaces = selectedSpaces.filter(id => id !== spaceId);
238
  await Promise.all(otherSpaces.map(async (sid) => {
239
- const newDraftRes = await taService.createSummaryDraft({ spaceId: sid, content: activeContent, draft_type: activeType });
240
  if (newDraftRes?.success) await taService.approveSummary(newDraftRes.data.id, sid);
241
  }));
242
  }
243
  addToast('Đã đăng bài!'); fetchData(); setAiPreview(null); setCurrentStep(1);
244
- } catch (error) { addToast('Lỗi gửi bài', 'error'); } finally { setIsSending(false); }
 
 
 
245
  };
246
 
247
  const handleScheduleSummary = async (draftId, spaceId, scheduledAt, autoPilotData = null) => {
@@ -249,6 +335,7 @@ const TADashboard = () => {
249
  try {
250
  const activeContent = autoPilotData?.content || aiPreview?.content || '';
251
  const activeType = autoPilotData?.draft_type || aiPreview?.draft_type || '';
 
252
 
253
  if (aiPreview?.id === draftId) await taService.updateSummaryDraft(draftId, spaceId, { content: activeContent });
254
  const isoDate = new Date(scheduledAt).toISOString();
@@ -257,17 +344,25 @@ const TADashboard = () => {
257
  if (res.success && selectedSpaces.length > 1 && (!aiPreview || aiPreview.status === 'pending')) {
258
  const otherSpaces = selectedSpaces.filter(id => id !== spaceId);
259
  await Promise.all(otherSpaces.map(async (sid) => {
260
- const newDraftRes = await taService.createSummaryDraft({ spaceId: sid, content: activeContent, draft_type: activeType });
261
  if (newDraftRes?.success) await taService.scheduleSummary(newDraftRes.data.id, sid, isoDate);
262
  }));
263
  }
264
 
265
  addToast('Đã đặt lịch!'); fetchData(); setAiPreview(null); setCurrentStep(1);
266
- } catch (error) {} finally { setIsSending(false); }
 
 
 
267
  };
268
 
269
  const handleEditScheduled = (item) => {
270
- setAiPreview(item);
 
 
 
 
 
271
  setSelectedSpaces([item.space_id]);
272
  if (item.draft_type === 'lesson_recap') { setActiveTab('summary'); setCurrentStep(3); }
273
  else { setActiveTab('announcements'); }
@@ -283,7 +378,10 @@ const TADashboard = () => {
283
  }));
284
  addToast('Đã hủy lịch hàng loạt');
285
  fetchData();
286
- } catch (e) {} finally { setIsSending(false); }
 
 
 
287
  };
288
 
289
  const handleBulkSendNow = async (ids) => {
@@ -295,7 +393,10 @@ const TADashboard = () => {
295
  }));
296
  addToast('Đã gửi hàng loạt');
297
  fetchData();
298
- } catch (e) {} finally { setIsSending(false); }
 
 
 
299
  };
300
 
301
  const handleCancelSchedule = async (draftId, spaceId) => {
@@ -303,7 +404,10 @@ const TADashboard = () => {
303
  setIsSending(true);
304
  const res = await taService.cancelSchedule(draftId, spaceId);
305
  if (res.success) { addToast('Đã hủy lịch'); fetchData(); }
306
- } catch (error) {} finally { setIsSending(false); }
 
 
 
307
  };
308
 
309
  const handleRefineAi = async (refineInstruction) => {
@@ -333,7 +437,10 @@ ${compileRules(g.rules)}
333
  addToast('Đã cập nhật bản thảo!');
334
  }
335
  } else { addToast('AI không phản hồi lệnh sửa', 'error'); }
336
- } catch (error) { addToast('Lỗi hiệu chỉnh AI', 'error'); } finally { setIsAnalyzing(false); }
 
 
 
337
  };
338
 
339
  const handleOpenCompose = async (snapshotId, spaceId) => {
@@ -344,7 +451,10 @@ ${compileRules(g.rules)}
344
  setCurrentContext({ id: snapshotId, space_id: spaceId, ...res.data, aiConfig, taName: user?.display_name });
345
  setIsComposeOpen(true);
346
  }
347
- } catch (error) {} finally { setLoading(false); }
 
 
 
348
  };
349
 
350
  const handleAiSend = async (message) => {
@@ -356,7 +466,10 @@ ${compileRules(g.rules)}
356
  content: message, snapshotId: currentContext.id
357
  });
358
  if (res.success) { setIsComposeOpen(false); handleResolveAlert(currentContext.id, currentContext.space_id); addToast('Đã gửi tin nhắn!'); }
359
- } catch (error) {} finally { setLoading(false); }
 
 
 
360
  };
361
 
362
  const RuleCheckbox = ({ label, checked, onChange }) => (
@@ -512,7 +625,10 @@ ${compileRules(g.rules)}
512
  try {
513
  const res = await taService.uploadSlide(selectedSpaces[0], file);
514
  if (res.success) setUploadedFile({ ...res.data, rawFile: file });
515
- } catch (error) {} finally { setUploading(false); }
 
 
 
516
  }}
517
  startAiAnalysis={async () => {
518
  if (selectedSpaces.length === 0) return;
@@ -530,10 +646,14 @@ ${compileRules(g.rules)}
530
 
531
  const aiContent = resAgent?.answer || resAgent?.content || resAgent?.response;
532
  if (resAgent?.success && aiContent) {
 
 
 
533
  const res = await taService.createSummaryDraft({
534
  spaceId: selectedSpaces[0],
535
- content: aiContent,
536
  draft_type: 'lesson_recap',
 
537
  });
538
  if (res?.success) {
539
  if (scheduleDate) handleScheduleSummary(res.data.id, res.data.space_id, scheduleDate, res.data);
@@ -554,13 +674,17 @@ ${compileRules(g.rules)}
554
 
555
  const aiContent = resAgent?.answer || resAgent?.content || resAgent?.response;
556
  if (resAgent?.success && aiContent) {
 
 
 
557
  const res = await taService.createSummaryDraft({
558
  spaceId: selectedSpaces[0],
559
- content: aiContent,
560
  draft_type: 'lesson_recap',
 
561
  });
562
  if (res?.success) {
563
- setAiPreview(res.data);
564
  setCurrentStep(3);
565
  }
566
  } else { addToast('AI không phản hồi', 'error'); }
@@ -612,7 +736,10 @@ ${compileRules(g.rules)}
612
  setAiPreview(res.data);
613
  }
614
  } else { addToast('AI không phản hồi', 'error'); }
615
- } catch (error) { addToast('Lỗi khi soạn thông báo', 'error'); } finally { setIsAnalyzing(false); }
 
 
 
616
  }}
617
  loading={isAnalyzing} aiPreview={aiPreview} setAiPreview={setAiPreview} handleApprove={handleApproveSummary} handleSchedule={handleScheduleSummary}
618
  scheduleDate={scheduleDate} setScheduleDate={setScheduleDate} selectedSpaces={selectedSpaces} setSelectedSpaces={setSelectedSpaces}
 
144
  const r = aiConfig.recap;
145
  const pronouns = r.pronouns || g.pronouns;
146
  return `[ROLE] Bạn là một Trợ Giảng (Teaching Assistant) xuất sắc.
147
+ [OBJECTIVE] Phân tích nội dung buổi học và trả về dữ liệu có cấu trúc gồm: tóm tắt bài giảng + danh sách deadline.
148
  [RULES]
149
  - Xưng hô bắt buộc: ${pronouns}.
150
  - Tone giọng: ${r.tone}.
151
  - Trọng tâm (Focus): ${r.highlight}.
152
  - Quy tắc chung:\\n${compileRules(g.rules)}\\n${g.instruction}
153
  - Quy tắc Recap:\\n${compileRules(r.rules)}\\n${r.instruction}
154
+ [OUTPUT FORMAT] Bắt buộc trả về valid JSON với cấu trúc sau:
155
+ {
156
+ "summary": "Nội dung tóm tắt bài giảng định dạng Markdown",
157
+ "deadlines": [
158
+ { "title": "Tên bài tập", "due_date": "YYYY-MM-DD hoặc mô tả ngày", "description": "Mô tả ngắn về yêu cầu" }
159
+ ]
160
+ }
161
+ TUYỆT ĐỐI KHÔNG thêm bất kỳ nội dung nào khác ngoài JSON (không có markdown code blocks, không có lời giải thích).`;
162
+ };
163
+
164
+ // Parse AI JSON response with fallback to text extraction
165
+ const parseAiRecapResponse = (aiContent) => {
166
+ if (!aiContent) return { summary: '', deadlines: [] };
167
+
168
+ // Extract deadlines from text using regex (for fallback)
169
+ const extractDeadlinesFromText = (content) => {
170
+ const patterns = [
171
+ /[-•*]\s*.*?(?:bài tập|homework|assignment|deadline|hạn|nộp|due date|làm|chuẩn bị).*?(?:\n|$)/gi,
172
+ /[-•*]\s*(?:đóng|gửi|submit).*?(?:bài|file|nội dung).*?(?:\n|$)/gi,
173
+ /\d{1,2}[\/-]\d{1,2}(?:\/\d{2,4})?\s*[.:]\s*.+?[\n]/gi
174
+ ];
175
+ const deadlines = new Set();
176
+ for (const pattern of patterns) {
177
+ const matches = content.match(pattern);
178
+ if (matches) {
179
+ matches.forEach(match => {
180
+ const cleanMatch = match.trim()
181
+ .replace(/^[-•*]\s*/, '')
182
+ .replace(/\s+/g, ' ')
183
+ .slice(0, 150);
184
+ if (cleanMatch.length > 8 && cleanMatch.length < 151) {
185
+ deadlines.add(cleanMatch);
186
+ }
187
+ });
188
+ }
189
+ }
190
+ return Array.from(deadlines).slice(0, 5);
191
+ };
192
+
193
+ try {
194
+ // Extract JSON from code blocks first (handle ```json ... ```)
195
+ let jsonStr = aiContent.trim();
196
+ const codeBlockMatch = aiContent.match(/```(?:json)?\s*([\s\S]*?)```/);
197
+ if (codeBlockMatch && codeBlockMatch[1]) {
198
+ jsonStr = codeBlockMatch[1].trim();
199
+ }
200
+
201
+ const parsed = JSON.parse(jsonStr);
202
+
203
+ // Validate structure
204
+ if (parsed.summary && Array.isArray(parsed.deadlines)) {
205
+ return {
206
+ summary: parsed.summary,
207
+ deadlines: parsed.deadlines
208
+ .filter(d => d?.title && d?.due_date)
209
+ .map(d => ({
210
+ title: d.title,
211
+ due_date: d.due_date,
212
+ description: d.description || ''
213
+ }))
214
+ };
215
+ }
216
+ } catch (e) {
217
+ // JSON parse failed, fall back to text extraction
218
+ console.warn('Failed to parse AI JSON response, using fallback extraction');
219
+ }
220
+
221
+ // Fallback: return raw content with extracted deadlines from text
222
+ return {
223
+ summary: aiContent,
224
+ deadlines: extractDeadlinesFromText(aiContent).map(text => ({
225
+ title: text,
226
+ due_date: '',
227
+ description: ''
228
+ }))
229
+ };
230
  };
231
 
232
  const buildAnnouncementPrompt = (p, c) => {
 
297
  addToast('Đã giải quyết');
298
  fetchData();
299
  }
300
+ } catch (error) {
301
+ console.error('handleResolveAlert error:', error);
302
+ addToast('Lỗi giải quyết cảnh báo', 'error');
303
+ }
304
  };
305
 
306
  const handleApproveSummary = async (draftId, spaceId, autoPilotData = null) => {
 
308
  try {
309
  const activeContent = autoPilotData?.content || aiPreview?.content || '';
310
  const activeType = autoPilotData?.draft_type || aiPreview?.draft_type || '';
311
+ const activeDeadlines = autoPilotData?.metadata?.deadlines || aiPreview?.deadlines || [];
312
 
313
  if (aiPreview?.id === draftId) await taService.updateSummaryDraft(draftId, spaceId, { content: activeContent });
314
  const res = await taService.approveSummary(draftId, spaceId);
315
+ if (!res.success) {
316
+ console.error('Approve summary failed:', res);
317
+ throw new Error('API returned failure');
318
+ }
319
+ if (selectedSpaces.length > 1 && (!aiPreview || aiPreview.status === 'pending')) {
320
  const otherSpaces = selectedSpaces.filter(id => id !== spaceId);
321
  await Promise.all(otherSpaces.map(async (sid) => {
322
+ const newDraftRes = await taService.createSummaryDraft({ spaceId: sid, content: activeContent, draft_type: activeType, metadata: { deadlines: activeDeadlines } });
323
  if (newDraftRes?.success) await taService.approveSummary(newDraftRes.data.id, sid);
324
  }));
325
  }
326
  addToast('Đã đăng bài!'); fetchData(); setAiPreview(null); setCurrentStep(1);
327
+ } catch (error) {
328
+ console.error('handleApproveSummary error:', error);
329
+ addToast(error.message || 'Lỗi gửi bài', 'error');
330
+ } finally { setIsSending(false); }
331
  };
332
 
333
  const handleScheduleSummary = async (draftId, spaceId, scheduledAt, autoPilotData = null) => {
 
335
  try {
336
  const activeContent = autoPilotData?.content || aiPreview?.content || '';
337
  const activeType = autoPilotData?.draft_type || aiPreview?.draft_type || '';
338
+ const activeDeadlines = autoPilotData?.metadata?.deadlines || aiPreview?.deadlines || [];
339
 
340
  if (aiPreview?.id === draftId) await taService.updateSummaryDraft(draftId, spaceId, { content: activeContent });
341
  const isoDate = new Date(scheduledAt).toISOString();
 
344
  if (res.success && selectedSpaces.length > 1 && (!aiPreview || aiPreview.status === 'pending')) {
345
  const otherSpaces = selectedSpaces.filter(id => id !== spaceId);
346
  await Promise.all(otherSpaces.map(async (sid) => {
347
+ const newDraftRes = await taService.createSummaryDraft({ spaceId: sid, content: activeContent, draft_type: activeType, metadata: { deadlines: activeDeadlines } });
348
  if (newDraftRes?.success) await taService.scheduleSummary(newDraftRes.data.id, sid, isoDate);
349
  }));
350
  }
351
 
352
  addToast('Đã đặt lịch!'); fetchData(); setAiPreview(null); setCurrentStep(1);
353
+ } catch (error) {
354
+ console.error('handleScheduleSummary error:', error);
355
+ addToast(error.message || 'Lỗi đặt lịch', 'error');
356
+ } finally { setIsSending(false); }
357
  };
358
 
359
  const handleEditScheduled = (item) => {
360
+ // Normalize: add top-level deadlines from metadata for consistent extraction
361
+ const normalizedItem = {
362
+ ...item,
363
+ deadlines: item.metadata?.deadlines || []
364
+ };
365
+ setAiPreview(normalizedItem);
366
  setSelectedSpaces([item.space_id]);
367
  if (item.draft_type === 'lesson_recap') { setActiveTab('summary'); setCurrentStep(3); }
368
  else { setActiveTab('announcements'); }
 
378
  }));
379
  addToast('Đã hủy lịch hàng loạt');
380
  fetchData();
381
+ } catch (e) {
382
+ console.error('handleBulkCancel error:', e);
383
+ addToast('Lỗi hủy lịch hàng loạt', 'error');
384
+ } finally { setIsSending(false); }
385
  };
386
 
387
  const handleBulkSendNow = async (ids) => {
 
393
  }));
394
  addToast('Đã gửi hàng loạt');
395
  fetchData();
396
+ } catch (e) {
397
+ console.error('handleBulkSendNow error:', e);
398
+ addToast('Lỗi gửi hàng loạt', 'error');
399
+ } finally { setIsSending(false); }
400
  };
401
 
402
  const handleCancelSchedule = async (draftId, spaceId) => {
 
404
  setIsSending(true);
405
  const res = await taService.cancelSchedule(draftId, spaceId);
406
  if (res.success) { addToast('Đã hủy lịch'); fetchData(); }
407
+ } catch (error) {
408
+ console.error('handleCancelSchedule error:', error);
409
+ addToast('Lỗi hủy lịch', 'error');
410
+ } finally { setIsSending(false); }
411
  };
412
 
413
  const handleRefineAi = async (refineInstruction) => {
 
437
  addToast('Đã cập nhật bản thảo!');
438
  }
439
  } else { addToast('AI không phản hồi lệnh sửa', 'error'); }
440
+ } catch (error) {
441
+ console.error('handleRefineAi error:', error);
442
+ addToast('Lỗi hiệu chỉnh AI', 'error');
443
+ } finally { setIsAnalyzing(false); }
444
  };
445
 
446
  const handleOpenCompose = async (snapshotId, spaceId) => {
 
451
  setCurrentContext({ id: snapshotId, space_id: spaceId, ...res.data, aiConfig, taName: user?.display_name });
452
  setIsComposeOpen(true);
453
  }
454
+ } catch (error) {
455
+ console.error('handleOpenCompose error:', error);
456
+ addToast('Lỗi mở compose', 'error');
457
+ } finally { setLoading(false); }
458
  };
459
 
460
  const handleAiSend = async (message) => {
 
466
  content: message, snapshotId: currentContext.id
467
  });
468
  if (res.success) { setIsComposeOpen(false); handleResolveAlert(currentContext.id, currentContext.space_id); addToast('Đã gửi tin nhắn!'); }
469
+ } catch (error) {
470
+ console.error('handleAiSend error:', error);
471
+ addToast('Lỗi gửi tin nhắn', 'error');
472
+ } finally { setLoading(false); }
473
  };
474
 
475
  const RuleCheckbox = ({ label, checked, onChange }) => (
 
625
  try {
626
  const res = await taService.uploadSlide(selectedSpaces[0], file);
627
  if (res.success) setUploadedFile({ ...res.data, rawFile: file });
628
+ } catch (error) {
629
+ console.error('handleFileUpload error:', error);
630
+ addToast('Lỗi tải file lên', 'error');
631
+ } finally { setUploading(false); }
632
  }}
633
  startAiAnalysis={async () => {
634
  if (selectedSpaces.length === 0) return;
 
646
 
647
  const aiContent = resAgent?.answer || resAgent?.content || resAgent?.response;
648
  if (resAgent?.success && aiContent) {
649
+ // Parse structured JSON response
650
+ const parsed = parseAiRecapResponse(aiContent);
651
+
652
  const res = await taService.createSummaryDraft({
653
  spaceId: selectedSpaces[0],
654
+ content: parsed.summary,
655
  draft_type: 'lesson_recap',
656
+ metadata: { deadlines: parsed.deadlines }
657
  });
658
  if (res?.success) {
659
  if (scheduleDate) handleScheduleSummary(res.data.id, res.data.space_id, scheduleDate, res.data);
 
674
 
675
  const aiContent = resAgent?.answer || resAgent?.content || resAgent?.response;
676
  if (resAgent?.success && aiContent) {
677
+ // Parse structured JSON response
678
+ const parsed = parseAiRecapResponse(aiContent);
679
+
680
  const res = await taService.createSummaryDraft({
681
  spaceId: selectedSpaces[0],
682
+ content: parsed.summary,
683
  draft_type: 'lesson_recap',
684
+ metadata: { deadlines: parsed.deadlines }
685
  });
686
  if (res?.success) {
687
+ setAiPreview({ ...res.data, deadlines: parsed.deadlines });
688
  setCurrentStep(3);
689
  }
690
  } else { addToast('AI không phản hồi', 'error'); }
 
736
  setAiPreview(res.data);
737
  }
738
  } else { addToast('AI không phản hồi', 'error'); }
739
+ } catch (error) {
740
+ console.error('Announcement AI error:', error);
741
+ addToast('Lỗi khi soạn thông báo', 'error');
742
+ } finally { setIsAnalyzing(false); }
743
  }}
744
  loading={isAnalyzing} aiPreview={aiPreview} setAiPreview={setAiPreview} handleApprove={handleApproveSummary} handleSchedule={handleScheduleSummary}
745
  scheduleDate={scheduleDate} setScheduleDate={setScheduleDate} selectedSpaces={selectedSpaces} setSelectedSpaces={setSelectedSpaces}
src/services/ta.service.js CHANGED
@@ -37,6 +37,15 @@ const taService = {
37
  * Duyệt bản tóm tắt
38
  */
39
  approveSummary: async (draftId, spaceId) => {
 
 
 
 
 
 
 
 
 
40
  const response = await api.post(`/ta/summary-queue/${draftId}/approve?spaceId=${spaceId}`);
41
  return response.data;
42
  },
 
37
  * Duyệt bản tóm tắt
38
  */
39
  approveSummary: async (draftId, spaceId) => {
40
+ console.log('=== approveSummary DEBUG ===');
41
+ console.log('draftId:', draftId, 'type:', typeof draftId);
42
+ console.log('spaceId:', spaceId, 'type:', typeof spaceId);
43
+ console.log('URL:', `/ta/summary-queue/${draftId}/approve?spaceId=${spaceId}`);
44
+
45
+ if (!draftId || !spaceId) {
46
+ throw new Error(`Invalid IDs: draftId=${draftId}, spaceId=${spaceId}`);
47
+ }
48
+
49
  const response = await api.post(`/ta/summary-queue/${draftId}/approve?spaceId=${spaceId}`);
50
  return response.data;
51
  },