/* Student-aware assistant panel. Loaded after dcp-backend.js and before dechepro-app.jsx. */ (function () { const MAX_QUESTION_LENGTH = 1000; function DCPAssistant({ authed }) { const A = window.DCP.assistant; const [open, setOpen] = React.useState(false); const [messages, setMessages] = React.useState([{ from: "bot", text: A.hello }]); const [draft, setDraft] = React.useState(""); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(""); const bodyRef = React.useRef(null); const identityRef = React.useRef(window.DCPB.identity()); React.useEffect(() => { const resetIfChanged = (event) => { if (event.type === "storage" && event.key && event.key !== "ag_token" && event.key !== "token") return; const current = window.DCPB.identity(); if (current === identityRef.current) return; identityRef.current = current; setMessages([{ from: "bot", text: A.hello }]); setDraft(""); setError(""); setBusy(false); setOpen(false); }; for (const name of ["storage", "focus", "dcp-auth-updated"]) window.addEventListener(name, resetIfChanged); return () => { for (const name of ["storage", "focus", "dcp-auth-updated"]) window.removeEventListener(name, resetIfChanged); }; }, []); React.useEffect(() => { if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }, [messages, open, busy, error]); const addBotMessage = (text) => { setMessages((items) => [...items, { from: "bot", text: text }]); }; const ask = async (rawQuestion) => { const question = String(rawQuestion || "").trim().slice(0, MAX_QUESTION_LENGTH); if (!question || busy) return; const requestIdentity = window.DCPB.identity(); setMessages((items) => [...items, { from: "user", text: question }]); setDraft(""); setError(""); if (!authed) { addBotMessage(A.answers[question] || A.fallback); return; } setBusy(true); try { if (!window.DCPB || typeof window.DCPB.ask !== "function") { throw new Error("Trợ lý chưa sẵn sàng"); } const answer = await window.DCPB.ask(question); if (window.DCPB.identity() !== requestIdentity) return; if (window.DCPB.isAuthed && !window.DCPB.isAuthed()) { throw new Error("Phiên đăng nhập đã hết hạn"); } if (typeof answer !== "string" || !answer.trim()) { throw new Error("Không nhận được câu trả lời"); } addBotMessage(answer); } catch (requestError) { if (window.DCPB.identity() !== requestIdentity) return; const expired = /hết hạn|401|đăng nhập/i.test(String(requestError && requestError.message)); setError(expired ? "Phiên đăng nhập đã hết hạn. Hãy đăng nhập lại rồi gửi câu hỏi này." : "Không thể xác nhận đã nhận câu hỏi. Kiểm tra với hỗ trợ trước khi gửi lại."); } finally { if (window.DCPB.identity() === requestIdentity) setBusy(false); } }; if (!open) { return ( setOpen(true)} aria-label="Mở trợ lý học tập"> Hỗ trợ học tập ); } return ; } function AssistantChat({ A, authed, messages, busy, error, draft, setDraft, setOpen, bodyRef, ask }) { const submit = (event) => { event.preventDefault(); ask(draft); }; const onKeyDown = (event) => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") submit(event); }; return ( {A.name} Đang hoạt động setOpen(false)} aria-label="Đóng trợ lý">× Làm bài tập trong phần mềm Antigravity trên máy tính. Khung này chỉ hỏi đáp chung về khoá học. {messages.map((message, index) => {message.text})} {busy && Đang tìm câu trả lời…} {(!authed || messages.length === 1) && {A.suggestions.map((suggestion) => ( ask(suggestion)} disabled={busy}> {suggestion} ))} } {authed && ( Câu hỏi về khóa học setDraft(event.target.value)} onKeyDown={onKeyDown} disabled={busy} rows={2} placeholder="Hỏi về khóa học…" /> {busy ? "Đang gửi" : "Gửi"} )} {error && ( {error} )} ); } window.DCPAssistant = DCPAssistant; })();