092_user_interface / src /components /ta /RecapWorkflow.jsx
quachtiensinh27
feat: Implement PDF indexing to Qdrant and improve quiz send notification
42f9804
Raw
History Blame Contribute Delete
19.7 kB
import React, { useState } from 'react';
import * as Icons from 'react-icons/fi';
import { marked } from 'marked';
const RecapWorkflow = (props) => {
const {
currentStep = 1,
uploading = false,
uploadedFile = null,
handleFileUpload = () => {},
startAiAnalysis = () => {},
aiPreview = null,
scheduleDate = '',
setScheduleDate = () => {},
handleApproveSummary = () => {},
handleScheduleSummary = () => {},
setCurrentStep = () => {},
setAiPreview = () => {},
selectedSpaces = [],
setSelectedSpaces = () => {},
taSpaces = [],
isHitlEnabled = true,
handleRefineAi = () => {},
onGenerateQuiz = null,
generatingQuiz = false
} = props;
const [isEditing, setIsEditing] = useState(false);
const [refineQuery, setRefineQuery] = useState('');
const [selectedChips, setSelectedChips] = useState([]);
const [quizQuestionCount, setQuizQuestionCount] = useState(10);
// Use deadlines from aiPreview if available, otherwise fall back to empty array
const deadlines = (aiPreview?.deadlines && Array.isArray(aiPreview.deadlines) && aiPreview.deadlines.length > 0)
? aiPreview.deadlines
: [];
const renderMarkdown = (content) => {
return { __html: marked.parse(content || '') };
};
const refineOptions = [
{ id: 'shorter', label: '✨ Ngắn gọn', prompt: 'Làm ngắn gọn lại, súc tích hơn.' },
{ 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.' },
{ id: 'professional', label: '✨ Chuyên nghiệp', prompt: 'Viết lại chuyên nghiệp và trang trọng hơn.' },
{ id: 'emoji', label: '✨ Thêm Emoji', prompt: 'Bổ sung thêm các emoji phù hợp để bài viết sinh động.' },
{ id: 'structure', label: '✨ Chia mục rõ ràng', prompt: 'Sử dụng bullet points và tiêu đề để chia bố cục rõ ràng hơn.' },
];
const toggleChip = (chipId) => {
setSelectedChips(prev =>
prev.includes(chipId) ? prev.filter(id => id !== chipId) : [...prev, chipId]
);
};
const handleApplyRefine = () => {
const chipPrompts = selectedChips.map(id => refineOptions.find(opt => opt.id === id).prompt).join(' ');
const finalInstruction = `${chipPrompts} ${refineQuery}`.trim();
if (finalInstruction) {
handleRefineAi(finalInstruction);
setRefineQuery('');
setSelectedChips([]);
}
};
const toggleSpace = (id) => {
if (typeof setSelectedSpaces === 'function') {
setSelectedSpaces(prev => {
const current = Array.isArray(prev) ? prev : [];
return current.includes(id) ? current.filter(item => item !== id) : [...current, id];
});
}
};
return (
<div className="animate-fade">
<div style={{ display: 'grid', gridTemplateColumns: '320px 1fr', gap: '24px', alignItems: 'start' }}>
{/* Left Panel - Configuration */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="ta-card-premium" style={{ padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 700, marginBottom: '20px', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'inherit' }}>
<Icons.FiSettings color="var(--primary)" /> Cấu hình
</h3>
<div style={{ marginBottom: '24px' }}>
<label style={{ display: 'block', fontSize: '11px', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', marginBottom: '12px', fontFamily: 'inherit' }}>
1. Chọn lớp ({selectedSpaces?.length || 0})
</label>
<div style={{ maxHeight: '150px', overflowY: 'auto', background: 'var(--bg-surface-tertiary)', borderRadius: '12px', padding: '8px', border: '1px solid var(--border-primary)' }}>
{Array.isArray(taSpaces) && taSpaces.map(space => (
<label key={space.id} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', cursor: 'pointer', borderRadius: '8px', transition: '0.2s', fontFamily: 'inherit' }}>
<input
type="checkbox"
checked={selectedSpaces?.includes(space.id)}
onChange={() => toggleSpace(space.id)}
style={{ width: '16px', height: '16px' }}
/>
<span style={{ fontSize: '14px' }}>{space.name}</span>
</label>
))}
</div>
</div>
<div style={{ marginBottom: '24px' }}>
<label style={{ display: 'block', fontSize: '11px', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', marginBottom: '12px', fontFamily: 'inherit' }}>
2. Tài liệu (Tùy chọn)
</label>
<div
style={{ border: '2px dashed var(--border-primary)', borderRadius: '12px', padding: '20px', textAlign: 'center', background: 'var(--bg-surface)', cursor: 'pointer' }}
onClick={() => document.getElementById('slide-upload').click()}
>
<Icons.FiUploadCloud size={24} color="var(--primary)" style={{ margin: '0 auto 8px' }} />
<div style={{ fontSize: '14px', fontWeight: 600, fontFamily: 'inherit' }}>Tải Slide (PDF/Ảnh)</div>
<input type="file" style={{ display: 'none' }} id="slide-upload" accept=".pdf,image/*" onChange={handleFileUpload} />
</div>
{uploadedFile && (
<div style={{ marginTop: '8px', display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 12px', background: 'var(--ta-green-bg)', borderRadius: '8px', fontFamily: 'inherit' }}>
<Icons.FiFileText color="var(--ta-green)" size={14} />
<span style={{ fontSize: '13px' }}>{uploadedFile.filename}</span>
</div>
)}
</div>
<div style={{ marginBottom: '24px' }}>
<label style={{ display: 'block', fontSize: '11px', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', marginBottom: '12px', fontFamily: 'inherit' }}>
3. Lịch đăng (Để trống = gửi ngay)
</label>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<Icons.FiClock size={16} color="var(--text-muted)" style={{ position: 'absolute', left: '12px' }} />
<input
type="datetime-local"
className="ta-input"
style={{ paddingLeft: '40px', fontFamily: 'inherit' }}
value={scheduleDate || ''}
onChange={(e) => setScheduleDate(e.target.value)}
/>
</div>
</div>
<button
className="vibrant-btn"
style={{ width: '100%', fontFamily: 'inherit' }}
onClick={startAiAnalysis}
disabled={uploading || (selectedSpaces?.length || 0) === 0}
>
{uploading ? <Icons.FiRefreshCw className="spin" /> : <Icons.FiZap />}
<span>
{isHitlEnabled
? (scheduleDate ? 'Tạo phác thảo & Đặt lịch' : 'Tạo phác thảo')
: (scheduleDate ? 'Tạo phác thảo & Đặt lịch' : 'Tạo phác thảo rồi gửi ngay')
}
</span>
</button>
</div>
</div>
{/* Right Panel - Content Area */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="ta-card-premium" style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 140px)', overflow: 'hidden', position: 'relative' }}>
{/* Header */}
<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 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Icons.FiFileText color="var(--primary)" size={18} />
<h3 style={{ fontSize: '15px', fontWeight: 700, fontFamily: 'inherit' }}>Bản thảo Recap</h3>
</div>
{aiPreview && !uploading && (
<div style={{ display: 'flex', gap: '8px' }}>
<button className="ta-btn" style={{ padding: '4px 12px', fontFamily: 'inherit' }} onClick={() => setIsEditing(!isEditing)}>
{isEditing ? <><Icons.FiEye size={14} /> Xem</> : <><Icons.FiEdit3 size={14} /> Sửa</>}
</button>
</div>
)}
</div>
{/* Processing Overlay */}
{uploading && (
<div className="processing-overlay-card">
<div className="processing-card-content">
<Icons.FiCpu className="spin" size={48} color="var(--primary)" />
<div style={{ textAlign: 'center' }}>
<h4 className="processing-card-title">AI đang phân tích...</h4>
<p className="processing-card-text">Đang đọc file và tạo tóm tắt bài giảng</p>
</div>
</div>
</div>
)}
{/* Empty State */}
{!aiPreview && !uploading && (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)', fontFamily: 'inherit', padding: '40px' }}>
<Icons.FiFileText size={56} style={{ opacity: 0.3, marginBottom: '16px' }} />
<p style={{ fontSize: '15px' }}>Chọn lớp và nhấn tạo phác thảo để bắt đầu</p>
</div>
)}
{/* Scrollable Content Area */}
<div style={{ flex: 1, overflowY: 'auto', padding: '24px' }}>
{/* AI Preview Content - 2 Columns */}
{aiPreview && !uploading && (
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '20px', marginBottom: '24px' }}>
{/* Column 1: Tóm tắt bài giảng */}
<div style={{ display: 'flex', flexDirection: 'column', background: 'var(--bg-surface)', borderRadius: '12px', border: '1px solid var(--border-primary)', overflow: 'hidden' }}>
<div style={{ padding: '14px 18px', background: 'var(--bg-surface-tertiary)', borderBottom: '1px solid var(--border-primary)', display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
<Icons.FiFileText size={16} color="var(--primary)" />
<h4 style={{ margin: 0, fontSize: '13px', fontWeight: 700, fontFamily: 'inherit' }}>Tóm tắt bài giảng</h4>
</div>
<div style={{ flex: 1, padding: '18px', overflowY: 'auto', minHeight: '300px' }}>
{isEditing ? (
<textarea
className="ta-input"
style={{ width: '100%', height: '100%', minHeight: '300px', border: 'none', resize: 'vertical', outline: 'none', background: 'transparent', fontSize: '14px', lineHeight: '1.7', fontFamily: 'inherit', padding: '12px' }}
value={aiPreview?.content || ''}
onChange={(e) => { if (typeof setAiPreview === 'function') setAiPreview({ ...aiPreview, content: e.target.value }); }}
/>
) : (
<div
className="markdown-content"
dangerouslySetInnerHTML={renderMarkdown(aiPreview?.content)}
style={{ fontSize: '14px', lineHeight: '1.7' }}
/>
)}
</div>
</div>
{/* Column 2: Deadlines */}
<div style={{ display: 'flex', flexDirection: 'column', background: 'var(--bg-surface)', borderRadius: '12px', border: '1px solid var(--border-primary)', overflow: 'hidden' }}>
<div style={{ padding: '14px 18px', background: 'var(--bg-surface-tertiary)', borderBottom: '1px solid var(--border-primary)', display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
<Icons.FiCalendar size={16} color="var(--ta-amber)" />
<h4 style={{ margin: 0, fontSize: '13px', fontWeight: 700, fontFamily: 'inherit' }}>Deadlines</h4>
</div>
<div style={{ flex: 1, padding: '18px', overflowY: 'auto', minHeight: '300px' }}>
{deadlines && deadlines.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{deadlines.map((deadline, idx) => (
<div key={idx} style={{ padding: '12px', background: 'var(--bg-surface-tertiary)', borderRadius: '8px', borderLeft: '3px solid var(--ta-amber)', fontSize: '13px', lineHeight: '1.5', wordBreak: 'break-word' }}>
<div style={{ fontWeight: 600, marginBottom: '4px', wordBreak: 'break-word' }}>{deadline.title || deadline}</div>
{deadline.due_date && (
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '4px' }}>
📅 {deadline.due_date}
</div>
)}
{deadline.description && (
<div style={{ fontSize: '12px', marginTop: '4px', wordBreak: 'break-word' }}>
{deadline.description}
</div>
)}
</div>
))}
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', minHeight: '200px', color: 'var(--text-muted)', gap: '8px' }}>
<Icons.FiCalendar size={32} opacity={0.3} />
<span style={{ fontSize: '13px' }}>Không có deadline nào</span>
</div>
)}
</div>
</div>
</div>
)}
</div>
{/* Action Footer */}
{aiPreview && !uploading && (
<div style={{ padding: '16px 24px', borderTop: '1px solid var(--border-primary)', background: 'var(--bg-surface-tertiary)', flexShrink: 0 }}>
{/* Refine Section */}
<div style={{ marginBottom: '12px', padding: '12px 16px', background: 'var(--bg-surface)', borderRadius: '12px', border: '1px solid var(--border-primary)' }}>
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', marginBottom: '8px', fontFamily: 'inherit' }}>✨ Tinh chỉnh nhanh cùng AI</div>
<div className="refine-chips-container" style={{ marginBottom: '12px' }}>
{refineOptions.map(opt => (
<button
key={opt.id}
className={`refine-chip ${selectedChips.includes(opt.id) ? 'active' : ''}`}
onClick={() => toggleChip(opt.id)}
>
{opt.label}
</button>
))}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text" className="ta-input"
style={{ padding: '8px 12px', fontSize: '13px', flex: 1 }}
placeholder="Yêu cầu riêng (vd: Ngắn gọn hơn...)"
value={refineQuery} onChange={(e) => setRefineQuery(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleApplyRefine(); }}
/>
<button
className="vibrant-btn"
style={{ padding: '0 12px', minWidth: 'auto', height: '38px' }}
disabled={selectedChips.length === 0 && !refineQuery}
onClick={handleApplyRefine}
>
<Icons.FiCheck size={18} />
</button>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', flexWrap: 'wrap' }}>
<button className="ta-btn" style={{ padding: '8px 16px', fontFamily: 'inherit' }} onClick={() => { if (typeof setAiPreview === 'function') setAiPreview(null); }}>Hủy</button>
{typeof onGenerateQuiz === 'function' && (
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<label style={{ fontSize: '12px', fontWeight: 500, fontFamily: 'inherit' }}>Số câu:</label>
<select
className="ta-input"
value={quizQuestionCount}
onChange={(e) => setQuizQuestionCount(Number(e.target.value))}
disabled={generatingQuiz}
style={{ padding: '6px 10px', minWidth: '65px', fontSize: '13px', fontFamily: 'inherit', opacity: generatingQuiz ? 0.5 : 1 }}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
</div>
<button
className="ta-btn"
style={{ padding: '6px 14px', fontSize: '13px', fontFamily: 'inherit' }}
onClick={() => onGenerateQuiz(aiPreview, quizQuestionCount)}
disabled={generatingQuiz}
>
{generatingQuiz ? <Icons.FiRefreshCw className="spin" size={14} /> : <Icons.FiHelpCircle size={14} />}
<span style={{ marginLeft: '4px' }}>{generatingQuiz ? 'Đang tạo...' : 'Tạo quiz'}</span>
</button>
</div>
)}
<button className="vibrant-btn"
style={{ padding: '8px 20px', fontFamily: 'inherit' }}
onClick={() => {
if (aiPreview && aiPreview.id) {
if (!scheduleDate) handleApproveSummary(aiPreview.id, aiPreview.space_id);
else handleScheduleSummary(aiPreview.id, aiPreview.space_id, scheduleDate);
}
}}
>
<Icons.FiSend size={14} /> <span style={{ marginLeft: '4px' }}>{scheduleDate ? 'Đặt lịch' : 'Gửi ngay'}</span>
</button>
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default RecapWorkflow;