quachtiensinh27 commited on
Commit
2ec553e
·
1 Parent(s): a09fc36

feat: implement TA dashboard recap workflow, AI analysis integration, and backend service modules

Browse files
src/components/ta/AiComposeModal.jsx CHANGED
@@ -3,8 +3,9 @@ import {
3
  FiX, FiSend, FiCpu, FiMessageCircle, FiSmile, FiUser,
4
  FiInfo, FiCopy, FiCheck, FiAlertCircle, FiTrendingUp
5
  } from 'react-icons/fi';
 
6
 
7
- const AiComposeModal = ({ isOpen, onClose, context, onSend }) => {
8
  const [draft, setDraft] = useState('');
9
  const [loading, setLoading] = useState(false);
10
  const [tone, setTone] = useState('helpful');
@@ -17,27 +18,23 @@ const AiComposeModal = ({ isOpen, onClose, context, onSend }) => {
17
  }
18
  }, [isOpen, context, tone]);
19
 
20
- const generateDraft = () => {
21
- // Đảm bảo signals luôn là một chuỗi an toàn
22
- const signalsArr = context?.at_risk_data?.signals;
23
- const signalsStr = Array.isArray(signalsArr) ? signalsArr.join(', ') : 'vắng mặt lâu ngày';
24
-
25
- // Giả lập gọi AI Agent
26
- setTimeout(() => {
27
- const studentName = context?.student_info?.name || 'Học viên';
28
-
29
- let aiContent = '';
30
- if (tone === 'helpful') {
31
- aiContent = `Chào ${studentName}, mình là Trợ lý AI từ lớp học đây. Thầy cô để ý thấy gần đây bạn có vẻ ${signalsStr}. Không biết bạn có gặp khó khăn gì ở phần nào không? Nếu cần hỗ trợ gì về bài tập hay nội dung bài giảng, đừng ngần ngại nhắn tin cho mình hoặc thầy cô nhé! Chúc bạn học tốt! ✨`;
32
- } else if (tone === 'urgent') {
33
- aiContent = `Chào ${studentName}, mình nhận thấy bạn đã không online khá lâu và đang gặp cảnh báo rủi ro. Việc vắng mặt lâu có thể khiến bạn mất gốc kiến thức quan trọng. Bạn vui lòng phản hồi tin nhắn này để thầy cô biết tình hình và hỗ trợ bạn kịp thời nhé!`;
34
- } else {
35
- aiContent = `Hey ${studentName}! Cố gắng lên nào! Thầy cô thấy bạn đang hơi "chững" lại một chút. Đừng để các rào cản làm bạn nản lòng. Hãy dành 15-20p mỗi ngày để duy trì thói quen học tập nhé. Bạn làm được mà! 💪`;
36
- }
37
-
38
- setDraft(aiContent);
39
  setLoading(false);
40
- }, 1200);
41
  };
42
 
43
  const handleCopy = () => {
@@ -169,10 +166,11 @@ const AiComposeModal = ({ isOpen, onClose, context, onSend }) => {
169
  <button
170
  onClick={() => onSend(draft)}
171
  className="vibrant-btn"
172
- style={{ padding: '10px 32px' }}
173
- disabled={loading || !draft}
174
  >
175
- <FiSend /> Gửi tin nhắn
 
176
  </button>
177
  </div>
178
  </div>
 
3
  FiX, FiSend, FiCpu, FiMessageCircle, FiSmile, FiUser,
4
  FiInfo, FiCopy, FiCheck, FiAlertCircle, FiTrendingUp
5
  } from 'react-icons/fi';
6
+ import taService from '../../services/ta.service';
7
 
8
+ const AiComposeModal = ({ isOpen, onClose, context, onSend, isSending }) => {
9
  const [draft, setDraft] = useState('');
10
  const [loading, setLoading] = useState(false);
11
  const [tone, setTone] = useState('helpful');
 
18
  }
19
  }, [isOpen, context, tone]);
20
 
21
+ const generateDraft = async () => {
22
+ if (!context?.id) return;
23
+
24
+ setLoading(true);
25
+ try {
26
+ const data = await taService.generateSmartMessage(context.id, tone);
27
+ // Đảm bảo dữ liệu là chuỗi văn bản
28
+ const content = typeof data.content === 'object'
29
+ ? (data.content.answer || data.content.content || JSON.stringify(data.content))
30
+ : data.content;
31
+ setDraft(content);
32
+ } catch (error) {
33
+ console.error('Failed to generate AI draft:', error);
34
+ setDraft('❌ Lỗi khi kết nối với AI Agent. Vui lòng thử lại sau.');
35
+ } finally {
 
 
 
 
36
  setLoading(false);
37
+ }
38
  };
39
 
40
  const handleCopy = () => {
 
166
  <button
167
  onClick={() => onSend(draft)}
168
  className="vibrant-btn"
169
+ style={{ padding: '10px 32px', display: 'flex', alignItems: 'center', gap: '8px' }}
170
+ disabled={loading || isSending || !draft}
171
  >
172
+ {isSending ? <FiCpu className="spin" /> : <FiSend />}
173
+ {isSending ? 'Đang gửi...' : 'Gửi tin nhắn'}
174
  </button>
175
  </div>
176
  </div>
src/components/ta/RecapWorkflow.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import React from 'react';
2
- import { FiUploadCloud, FiRefreshCw, FiCheckCircle, FiChevronRight, FiCpu, FiFileText, FiList, FiCheckSquare, FiSend, FiClock, FiLayers } from 'react-icons/fi';
3
 
4
  const RecapWorkflow = ({
5
  currentStep,
@@ -116,12 +116,27 @@ const RecapWorkflow = ({
116
  <div style={{ marginTop: '40px', textAlign: 'center' }}>
117
  <button
118
  className="vibrant-btn"
119
- style={{ minWidth: '280px', height: '54px', fontSize: '15px', borderRadius: '16px' }}
 
 
 
 
 
 
120
  onClick={startAiAnalysis}
121
- disabled={!uploadedFile || uploading}
122
  >
123
- Tiến hành phân tích bằng AI <FiChevronRight style={{ marginLeft: '10px' }} />
 
 
 
124
  </button>
 
 
 
 
 
 
125
  </div>
126
  </div>
127
  )}
 
1
  import React from 'react';
2
+ import { FiUploadCloud, FiRefreshCw, FiCheckCircle, FiChevronRight, FiCpu, FiFileText, FiList, FiCheckSquare, FiSend, FiClock, FiLayers, FiMessageSquare, FiInfo } from 'react-icons/fi';
3
 
4
  const RecapWorkflow = ({
5
  currentStep,
 
116
  <div style={{ marginTop: '40px', textAlign: 'center' }}>
117
  <button
118
  className="vibrant-btn"
119
+ style={{
120
+ minWidth: '320px',
121
+ height: '54px',
122
+ fontSize: '15px',
123
+ borderRadius: '16px',
124
+ background: !uploadedFile ? 'linear-gradient(135deg, var(--ta-blue), #3b82f6)' : 'linear-gradient(135deg, var(--ta-accent), #8b5cf6)'
125
+ }}
126
  onClick={startAiAnalysis}
127
+ disabled={uploading}
128
  >
129
+ {uploading ? <><FiRefreshCw className="spin" style={{ marginRight: '10px' }} /> Đang xử lý...</> :
130
+ uploadedFile ? <><FiCpu style={{ marginRight: '10px' }} /> Phân tích Slide & Hội thoại</> :
131
+ <><FiMessageSquare style={{ marginRight: '10px' }} /> Tóm tắt từ Hội thoại thực tế</>}
132
+ <FiChevronRight style={{ marginLeft: '10px' }} />
133
  </button>
134
+
135
+ {!uploadedFile && (
136
+ <p style={{ fontSize: '12px', color: 'var(--ta-text3)', marginTop: '16px' }}>
137
+ <FiInfo style={{ marginRight: '4px' }} /> Bạn có thể tải lên Slide để AI phân tích sâu hơn, hoặc tóm tắt ngay từ lịch sử chat.
138
+ </p>
139
+ )}
140
  </div>
141
  </div>
142
  )}
src/pages/TADashboard.jsx CHANGED
@@ -103,6 +103,49 @@ const TADashboard = () => {
103
  }
104
  };
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  useEffect(() => {
107
  fetchData();
108
  const interval = setInterval(fetchData, 60000);
@@ -120,8 +163,11 @@ const TADashboard = () => {
120
  setUploading(true);
121
  try {
122
  const res = await taService.uploadSlide(selectedSpaceFilter, file);
123
- if (res.success) setUploadedFile(res.data);
124
- else alert('❌ Tải lên thất bại.');
 
 
 
125
  } catch (error) {
126
  alert('❌ Lỗi kết nối server.');
127
  } finally {
@@ -129,79 +175,119 @@ const TADashboard = () => {
129
  }
130
  };
131
 
132
- const startAiAnalysis = () => {
 
 
 
 
 
133
  setCurrentStep(2);
134
- setTimeout(async () => {
135
- const mockContent = `### Tóm tắt buổi học hôm nay\n\n1. **React Hooks nâng cao**: Tìm hiểu useMemo và useCallback để tối ưu re-render.\n2. **Performance Audit**: Cách sử dụng Profiler trong React DevTools.\n3. **Atomic Design**: Phân rã Component thành các phần tử nguyên tử.\n\n📌 **Bài tập**: Hoàn thiện Dashboard cho dự án cá nhân và nộp trước 23h ngày mai.`;
136
- const mockMetadata = {
137
- decisions: [
138
- { content: "Thống nhất sử dụng Atomic Design cho dự án cuối khóa." },
139
- { content: "Dời lịch nộp bài tập sang thứ 4 tuần sau." }
140
- ],
141
- tasks: [
142
- { task: "Hoàn thiện UI Dashboard", assignee: "Quang" },
143
- { task: "Cài đặt Redis cho Backend", assignee: "Hoàng" }
144
- ]
145
- };
146
-
147
- try {
148
- // Lưu bản thảo vào DB để có ID thật
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  const res = await taService.createSummaryDraft({
150
  spaceId: selectedSpaceFilter,
151
- content: mockContent,
152
- metadata: mockMetadata,
 
 
 
 
153
  draft_type: 'lesson_recap'
154
  });
155
 
156
  if (res.success) {
157
- setAiPreview(res.data); // res.data đã bao gồm id và space_id từ DB
158
  setCurrentStep(3);
159
  } else {
160
- alert('❌ Không thể lưu bản thảo AI.');
161
  setCurrentStep(1);
162
  }
163
- } catch (error) {
164
- console.error('AI Analysis failed:', error);
165
- alert('❌ Lỗi kết nối AI Service.');
166
- setCurrentStep(1);
167
  }
168
- }, 2500);
 
 
 
 
169
  };
170
 
171
- const handleGenerateAnnouncement = (purpose, context) => {
172
  if (selectedSpaceFilter === 'all') {
173
  alert('⚠️ Vui lòng chọn một lớp học cụ thể ở thanh bộ lọc phía trên trước khi tạo thông báo!');
174
  return;
175
  }
176
  setUploading(true);
177
- setTimeout(async () => {
178
- const mockContent = `📢 **THÔNG BÁO ${purpose.toUpperCase()}** 📢\n\nChào cả lớp, thầy có thông báo về việc ${purpose === 'attendance' ? 'điểm danh' : purpose === 'homework' ? 'nộp bài tập' : 'lịch trình lớp học'}:\n\n- **Nội dung**: ${context}\n- **Thời gian áp dụng**: Từ hôm nay\n\nChúc các em học tốt!\n\n---\n*Thông báo tự động từ TA Assistant*`;
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- try {
181
  const res = await taService.createSummaryDraft({
182
  spaceId: selectedSpaceFilter,
183
- content: mockContent,
184
- metadata: { purpose, context },
185
  draft_type: 'announcement'
186
  });
187
 
188
  if (res.success) {
189
  setAiPreview(res.data);
190
- } else {
191
- alert('❌ Lỗi tạo thông báo.');
192
  }
193
- } catch (error) {
194
- console.error('Announcement generation failed:', error);
195
- } finally {
196
- setUploading(false);
197
  }
198
- }, 2000);
 
 
 
 
 
199
  };
200
 
201
  const handleResolveAlert = async (id, spaceId) => {
202
  try {
203
  await taService.resolveAlert(id, spaceId);
204
- await fetchData();
205
  } catch (error) {
206
  console.error('Resolve failed:', error);
207
  }
@@ -209,6 +295,15 @@ const TADashboard = () => {
209
 
210
  const handleApproveSummary = async (draftId, spaceId) => {
211
  try {
 
 
 
 
 
 
 
 
 
212
  await taService.approveSummary(draftId, spaceId);
213
  alert('✅ Bản tóm tắt đã được phê duyệt và đăng bài thành công!');
214
  fetchData();
@@ -219,13 +314,29 @@ const TADashboard = () => {
219
  }
220
  } catch (error) {
221
  console.error('Approval failed:', error);
222
- alert('❌ Lỗi khi phê duyệt bản tóm tắt.');
 
223
  }
224
  };
225
 
226
  const handleScheduleSummary = async (draftId, spaceId, scheduledAt) => {
 
 
 
 
 
227
  try {
228
- await taService.scheduleSummary(draftId, spaceId, scheduledAt);
 
 
 
 
 
 
 
 
 
 
229
  alert(`📅 Đã đặt lịch gửi bản tóm tắt vào: ${new Date(scheduledAt).toLocaleString()}`);
230
  fetchData();
231
  if (currentStep === 3) {
@@ -301,12 +412,25 @@ const TADashboard = () => {
301
  };
302
 
303
  const handleAiSend = async (message) => {
304
- // Logic gửi tin nhắn (thường là gọi dmsService)
305
- alert(`🤖 Tin nhắn AI đã được gửi: \n\n"${message}"`);
306
- if (currentContext) {
307
- handleResolveAlert(currentContext.id, currentContext.spaceId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  }
309
- setIsComposeOpen(false);
310
  };
311
 
312
  const formatOfflineTime = (student) => {
@@ -387,6 +511,7 @@ const TADashboard = () => {
387
  onClose={() => setIsComposeOpen(false)}
388
  context={currentContext}
389
  onSend={handleAiSend}
 
390
  />
391
 
392
  <aside className="ta-internal-sidebar">
@@ -499,14 +624,14 @@ const TADashboard = () => {
499
  </div>
500
  </div>
501
  <div className="ta-card-body" style={{ padding: 0 }}>
502
- {filteredAtRisk.length === 0 ? (
503
  <div className="empty-state" style={{ padding: '60px 0' }}>
504
  <FiCheckCircle size={48} color="var(--ta-accent)" style={{ marginBottom: '16px', opacity: 0.5 }} />
505
  <h3>Mọi thứ đang trong tầm kiểm soát</h3>
506
  <p style={{ color: 'var(--ta-text3)' }}>Tất cả học viên đều đang tương tác ổn định.</p>
507
  </div>
508
  ) : (
509
- filteredAtRisk.map(student => (
510
  <RiskCard
511
  key={student.id}
512
  student={student}
@@ -514,7 +639,7 @@ const TADashboard = () => {
514
  onResolve={handleResolveAlert}
515
  onGetContext={handleOpenCompose}
516
  formatOfflineTime={formatOfflineTime}
517
- getRiskColor={getRiskColor}
518
  getHomeworkBadge={getHomeworkBadge}
519
  />
520
  ))
 
103
  }
104
  };
105
 
106
+ // Logic phân loại và lọc Alert thông minh theo đặc tả rút gọn
107
+ const getCategorizedAlerts = () => {
108
+ // Lấy danh sách đã xử lý từ localStorage để ẩn đi
109
+ const processedIds = JSON.parse(localStorage.getItem('ta_processed_snapshots') || '[]');
110
+
111
+ const filtered = atRiskList.filter(item => {
112
+ // Ẩn nếu đã xử lý trong phiên này
113
+ if (processedIds.includes(item.id)) return false;
114
+ // Filter theo lớp học nếu có chọn lọc
115
+ if (selectedSpaceFilter !== 'all' && item.space_id !== selectedSpaceFilter) return false;
116
+ return true;
117
+ });
118
+
119
+ return filtered.map(item => {
120
+ const score = item.metadata?.score || 0;
121
+ const signals = item.metadata?.signals || [];
122
+ const signalsCount = signals.length;
123
+
124
+ let displayLevel = 'warning';
125
+ let color = 'var(--ta-warning)';
126
+
127
+ if (score >= 7 || (score >= 5 && signalsCount >= 2)) {
128
+ displayLevel = 'critical';
129
+ color = 'var(--ta-critical)';
130
+ } else if (score >= 5) {
131
+ displayLevel = 'high';
132
+ color = 'var(--ta-high)';
133
+ }
134
+
135
+ return { ...item, displayLevel, color };
136
+ }).sort((a, b) => (b.metadata?.score || 0) - (a.metadata?.score || 0));
137
+ };
138
+
139
+ const markAsProcessed = (snapshotId) => {
140
+ const processedIds = JSON.parse(localStorage.getItem('ta_processed_snapshots') || '[]');
141
+ if (!processedIds.includes(snapshotId)) {
142
+ processedIds.push(snapshotId);
143
+ localStorage.setItem('ta_processed_snapshots', JSON.stringify(processedIds));
144
+ }
145
+ // Update local state to trigger re-render
146
+ setAtRiskList(prev => prev.filter(item => item.id !== snapshotId));
147
+ };
148
+
149
  useEffect(() => {
150
  fetchData();
151
  const interval = setInterval(fetchData, 60000);
 
163
  setUploading(true);
164
  try {
165
  const res = await taService.uploadSlide(selectedSpaceFilter, file);
166
+ if (res.success) {
167
+ setUploadedFile({ ...res.data, rawFile: file });
168
+ } else {
169
+ alert('❌ Tải lên thất bại.');
170
+ }
171
  } catch (error) {
172
  alert('❌ Lỗi kết nối server.');
173
  } finally {
 
175
  }
176
  };
177
 
178
+ const startAiAnalysis = async () => {
179
+ if (selectedSpaceFilter === 'all') {
180
+ alert('⚠️ Vui lòng chọn một lớp học cụ thể để AI có thể phân tích dữ liệu.');
181
+ return;
182
+ }
183
+
184
  setCurrentStep(2);
185
+ try {
186
+ let agentResponse;
187
+ const senderId = user?.display_name || user?.id || 'TA';
188
+ const query = `Hãy đóng vai một Trợ giảng (TA) nhiệt huyết và chuyên nghiệp. Dựa trên nội dung buổi học, hãy soạn một bản Recap gửi cho các bạn học viên với cấu trúc sau:
189
+ 1. Lời chào: Thân thiện, truyền cảm hứng (VD: 'Chào các bạn, buổi học hôm nay thực sự bùng nổ!').
190
+ 2. Tóm tắt nội dung chính: Chia thành 3-5 ý chính rõ ràng, mỗi ý có tiêu đề đậm và mô tả súc tích.
191
+ 3. Các quyết định/Lưu ý quan trọng: Điểm lại những gì học viên cần chốt hoặc nhớ kỹ.
192
+ 4. Bài tập & Deadline: Liệt kê rõ ràng những việc cần hoàn thành.
193
+ 5. Lời kết: Động viên và nhắc nhở các bạn.
194
+
195
+ Giọng văn: Chuyên nghiệp nhưng gần gũi, sử dụng các từ như 'chúng mình', 'các bạn'.
196
+ Yêu cầu: Định dạng Markdown đẹp mắt, sử dụng emoji phù hợp để tăng tính sinh động.`;
197
+
198
+ if (uploadedFile && uploadedFile.rawFile) {
199
+ // Phân tích từ File PDF/Ảnh bài giảng
200
+ agentResponse = await taService.callAgentWithFile(
201
+ selectedSpaceFilter,
202
+ query,
203
+ senderId,
204
+ uploadedFile.rawFile
205
+ );
206
+ } else {
207
+ // Tóm tắt từ hội thoại trong phòng chat
208
+ agentResponse = await taService.callAgentChat(
209
+ selectedSpaceFilter,
210
+ query,
211
+ senderId
212
+ );
213
+ }
214
+ console.log('[DEBUG] Agent Response:', agentResponse);
215
+ if (agentResponse && agentResponse.answer) {
216
+ // Lưu bản thảo vào DB sau khi Agent trả về kết quả
217
  const res = await taService.createSummaryDraft({
218
  spaceId: selectedSpaceFilter,
219
+ content: agentResponse.answer,
220
+ metadata: {
221
+ processing_time: agentResponse.processing_time,
222
+ source: uploadedFile ? 'multimedia' : 'conversation',
223
+ created_by: user?.id
224
+ },
225
  draft_type: 'lesson_recap'
226
  });
227
 
228
  if (res.success) {
229
+ setAiPreview(res.data);
230
  setCurrentStep(3);
231
  } else {
232
+ alert('❌ Không thể lưu bản thảo Recap.');
233
  setCurrentStep(1);
234
  }
235
+ } else {
236
+ throw new Error('Agent returned invalid response');
 
 
237
  }
238
+ } catch (error) {
239
+ console.error('AI Analysis failed:', error);
240
+ alert('❌ Lỗi phân tích từ AI Agent: ' + (error.message || 'Dịch vụ không phản hồi'));
241
+ setCurrentStep(1);
242
+ }
243
  };
244
 
245
+ const handleGenerateAnnouncement = async (purpose, context) => {
246
  if (selectedSpaceFilter === 'all') {
247
  alert('⚠️ Vui lòng chọn một lớp học cụ thể ở thanh bộ lọc phía trên trước khi tạo thông báo!');
248
  return;
249
  }
250
  setUploading(true);
251
+ try {
252
+ const senderId = user?.display_name || user?.id || 'TA';
253
+ const query = `Hãy đóng vai một Trợ giảng chuyên nghiệp. Soạn một thông báo gửi tới lớp học.
254
+ Mục đích: ${purpose}
255
+ Ngữ cảnh cụ thể: ${context || 'Chưa có thông tin chi tiết'}
256
+
257
+ Yêu cầu:
258
+ - Phong cách: Gần gũi, khích lệ nhưng vẫn trang trọng.
259
+ - Định dạng: Markdown (dùng Bold, Bullet points để dễ đọc).
260
+ - Sử dụng Emoji phù hợp.
261
+ - Độ dài: Súc tích, tập trung vào thông tin cần truyền tải.`;
262
+
263
+ const agentResponse = await taService.callAgentChat(selectedSpaceFilter, query, senderId);
264
 
265
+ if (agentResponse && agentResponse.answer) {
266
  const res = await taService.createSummaryDraft({
267
  spaceId: selectedSpaceFilter,
268
+ content: agentResponse.answer,
269
+ metadata: { purpose, context, created_by: user?.id, processing_time: agentResponse.processing_time },
270
  draft_type: 'announcement'
271
  });
272
 
273
  if (res.success) {
274
  setAiPreview(res.data);
275
+ // Cập nhật hàng đợi hiển thị
276
+ setSummaryQueue([res.data, ...summaryQueue]);
277
  }
 
 
 
 
278
  }
279
+ } catch (error) {
280
+ console.error('Announcement generation failed:', error);
281
+ alert('❌ Không thể tạo thông báo bằng AI: ' + error.message);
282
+ } finally {
283
+ setUploading(false);
284
+ }
285
  };
286
 
287
  const handleResolveAlert = async (id, spaceId) => {
288
  try {
289
  await taService.resolveAlert(id, spaceId);
290
+ markAsProcessed(id); // Ẩn ngay lập tức khỏi Dashboard
291
  } catch (error) {
292
  console.error('Resolve failed:', error);
293
  }
 
295
 
296
  const handleApproveSummary = async (draftId, spaceId) => {
297
  try {
298
+ // 1. Cập nhật nội dung bản thảo lên server trước (để lưu những gì TA đã sửa như 'ABC')
299
+ if (aiPreview && aiPreview.id === draftId) {
300
+ await taService.updateSummaryDraft(draftId, spaceId, {
301
+ content: aiPreview.content,
302
+ metadata: aiPreview.metadata
303
+ });
304
+ }
305
+
306
+ // 2. Sau đó mới gọi phê duyệt để đăng bài
307
  await taService.approveSummary(draftId, spaceId);
308
  alert('✅ Bản tóm tắt đã được phê duyệt và đăng bài thành công!');
309
  fetchData();
 
314
  }
315
  } catch (error) {
316
  console.error('Approval failed:', error);
317
+ const errorMsg = error.response?.data?.message || error.message || 'Lỗi không xác định';
318
+ alert(`❌ Lỗi khi phê duyệt bản tóm tắt: ${errorMsg}`);
319
  }
320
  };
321
 
322
  const handleScheduleSummary = async (draftId, spaceId, scheduledAt) => {
323
+ console.log('[DEBUG] Scheduling summary:', { draftId, spaceId, scheduledAt });
324
+ if (!draftId || !spaceId || !scheduledAt) {
325
+ alert(`⚠️ Thiếu thông tin đặt lịch: ${!draftId ? 'ID bản thảo, ' : ''}${!spaceId ? 'ID lớp học, ' : ''}${!scheduledAt ? 'Thời gian' : ''}`);
326
+ return;
327
+ }
328
  try {
329
+ // 1. Lưu nội dung mới nhất (ví dụ 'ABC') trước khi đặt lịch
330
+ if (aiPreview && aiPreview.id === draftId) {
331
+ await taService.updateSummaryDraft(draftId, spaceId, {
332
+ content: aiPreview.content,
333
+ metadata: aiPreview.metadata
334
+ });
335
+ }
336
+
337
+ // 2. Chuyển đổi sang ISO String để tránh lệch múi giờ khi lưu vào DB
338
+ const isoDate = new Date(scheduledAt).toISOString();
339
+ await taService.scheduleSummary(draftId, spaceId, isoDate);
340
  alert(`📅 Đã đặt lịch gửi bản tóm tắt vào: ${new Date(scheduledAt).toLocaleString()}`);
341
  fetchData();
342
  if (currentStep === 3) {
 
412
  };
413
 
414
  const handleAiSend = async (message) => {
415
+ if (!currentContext) return;
416
+
417
+ try {
418
+ setLoading(true);
419
+ await taService.sendSmartMessage(currentContext.at_risk_data?.space_id || selectedSpaceFilter, {
420
+ studentId: currentContext.student_info?.id,
421
+ content: message,
422
+ snapshotId: currentContext.id
423
+ });
424
+
425
+ markAsProcessed(currentContext.id); // Ẩn ngay sau khi gửi tin nhắn hỗ trợ thành công
426
+ alert(`🤖 Tin nhắn AI đã được gửi thành công đến học viên!`);
427
+ setIsComposeOpen(false);
428
+ } catch (error) {
429
+ console.error('Failed to send smart message:', error);
430
+ alert('❌ Lỗi khi gửi tin nhắn AI: ' + (error.response?.data?.message || error.message));
431
+ } finally {
432
+ setLoading(false);
433
  }
 
434
  };
435
 
436
  const formatOfflineTime = (student) => {
 
511
  onClose={() => setIsComposeOpen(false)}
512
  context={currentContext}
513
  onSend={handleAiSend}
514
+ isSending={loading}
515
  />
516
 
517
  <aside className="ta-internal-sidebar">
 
624
  </div>
625
  </div>
626
  <div className="ta-card-body" style={{ padding: 0 }}>
627
+ {getCategorizedAlerts().length === 0 ? (
628
  <div className="empty-state" style={{ padding: '60px 0' }}>
629
  <FiCheckCircle size={48} color="var(--ta-accent)" style={{ marginBottom: '16px', opacity: 0.5 }} />
630
  <h3>Mọi thứ đang trong tầm kiểm soát</h3>
631
  <p style={{ color: 'var(--ta-text3)' }}>Tất cả học viên đều đang tương tác ổn định.</p>
632
  </div>
633
  ) : (
634
+ getCategorizedAlerts().map(student => (
635
  <RiskCard
636
  key={student.id}
637
  student={student}
 
639
  onResolve={handleResolveAlert}
640
  onGetContext={handleOpenCompose}
641
  formatOfflineTime={formatOfflineTime}
642
+ getRiskColor={() => student.color}
643
  getHomeworkBadge={getHomeworkBadge}
644
  />
645
  ))