Spaces:
Sleeping
Sleeping
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 +22 -24
- src/components/ta/RecapWorkflow.jsx +19 -4
- src/pages/TADashboard.jsx +176 -51
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 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
const
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 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 |
-
}
|
| 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 |
-
<
|
|
|
|
| 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={{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
onClick={startAiAnalysis}
|
| 121 |
-
disabled={
|
| 122 |
>
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
| 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)
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
const
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
const res = await taService.createSummaryDraft({
|
| 150 |
spaceId: selectedSpaceFilter,
|
| 151 |
-
content:
|
| 152 |
-
metadata:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
draft_type: 'lesson_recap'
|
| 154 |
});
|
| 155 |
|
| 156 |
if (res.success) {
|
| 157 |
-
setAiPreview(res.data);
|
| 158 |
setCurrentStep(3);
|
| 159 |
} else {
|
| 160 |
-
alert('❌ Không thể lưu bản thảo
|
| 161 |
setCurrentStep(1);
|
| 162 |
}
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
alert('❌ Lỗi kết nối AI Service.');
|
| 166 |
-
setCurrentStep(1);
|
| 167 |
}
|
| 168 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 178 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
|
| 181 |
const res = await taService.createSummaryDraft({
|
| 182 |
spaceId: selectedSpaceFilter,
|
| 183 |
-
content:
|
| 184 |
-
metadata: { purpose, context },
|
| 185 |
draft_type: 'announcement'
|
| 186 |
});
|
| 187 |
|
| 188 |
if (res.success) {
|
| 189 |
setAiPreview(res.data);
|
| 190 |
-
|
| 191 |
-
|
| 192 |
}
|
| 193 |
-
} catch (error) {
|
| 194 |
-
console.error('Announcement generation failed:', error);
|
| 195 |
-
} finally {
|
| 196 |
-
setUploading(false);
|
| 197 |
}
|
| 198 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
};
|
| 200 |
|
| 201 |
const handleResolveAlert = async (id, spaceId) => {
|
| 202 |
try {
|
| 203 |
await taService.resolveAlert(id, spaceId);
|
| 204 |
-
|
| 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 |
-
|
|
|
|
| 223 |
}
|
| 224 |
};
|
| 225 |
|
| 226 |
const handleScheduleSummary = async (draftId, spaceId, scheduledAt) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
try {
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
{
|
| 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 |
-
|
| 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={
|
| 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 |
))
|