import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, addDoc, query, orderBy, onSnapshot, serverTimestamp, doc, setDoc, getDocs, deleteDoc } from 'firebase/firestore'; // Global variables for Firebase config and app ID (provided by the environment) const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; // Utility function to copy text to clipboard const copyToClipboard = (text) => { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); console.log('Text copied to clipboard'); } catch (err) { console.error('Failed to copy text: ', err); } document.body.removeChild(textarea); }; // Main App Component const App = () => { const [db, setDb] = useState(null); const [auth, setAuth] = useState(null); const [userId, setUserId] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); const [uploadedFiles, setUploadedFiles] = useState([]); const [documentContent, setDocumentContent] = useState(''); // Combined text content from documents const [chatHistory, setChatHistory] = useState([]); const [userMessage, setUserMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [showUploadModal, setShowUploadModal] = useState(false); const [currentFileContent, setCurrentFileContent] = useState(''); const [currentFileName, setCurrentFileName] = useState(''); const chatContainerRef = useRef(null); // Initialize Firebase and set up auth listener useEffect(() => { try { const app = initializeApp(firebaseConfig); const firestore = getFirestore(app); const authentication = getAuth(app); setDb(firestore); setAuth(authentication); onAuthStateChanged(authentication, async (user) => { if (user) { setUserId(user.uid); } else { // Sign in anonymously if no initial token or user if (!initialAuthToken) { await signInAnonymously(authentication); } } setIsAuthReady(true); }); if (initialAuthToken) { signInWithCustomToken(authentication, initialAuthToken) .catch((error) => { console.error("Error signing in with custom token:", error); signInAnonymously(authentication); // Fallback to anonymous }); } else { signInAnonymously(authentication); } } catch (error) { console.error("Error initializing Firebase:", error); setErrorMessage("Lỗi khi khởi tạo Firebase."); } }, []); // Fetch documents and chat history when auth is ready and userId is available useEffect(() => { if (db && userId && isAuthReady) { const documentsRef = collection(db, `artifacts/${appId}/users/${userId}/documents`); const chatHistoryRef = collection(db, `artifacts/${appId}/users/${userId}/chatHistory`); // Listen for document changes const unsubscribeDocs = onSnapshot(documentsRef, (snapshot) => { const docsData = []; let combinedContent = ''; snapshot.forEach((doc) => { const data = doc.data(); docsData.push({ id: doc.id, ...data }); combinedContent += data.content + '\n\n'; // Concatenate document content }); setUploadedFiles(docsData); setDocumentContent(combinedContent.trim()); }, (error) => { console.error("Error fetching documents:", error); setErrorMessage("Lỗi khi tải tài liệu."); }); // Listen for chat history changes const unsubscribeChat = onSnapshot(query(chatHistoryRef, orderBy('timestamp')), (snapshot) => { const messages = []; snapshot.forEach((doc) => { messages.push(doc.data()); }); setChatHistory(messages); }, (error) => { console.error("Error fetching chat history:", error); setErrorMessage("Lỗi khi tải lịch sử trò chuyện."); }); // Clean up listeners on unmount return () => { unsubscribeDocs(); unsubscribeChat(); }; } }, [db, userId, isAuthReady]); // Scroll to bottom of chat history useEffect(() => { if (chatContainerRef.current) { chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; } }, [chatHistory]); // Handle file upload const handleFileChange = async (event) => { const files = Array.from(event.target.files); if (files.length === 0) return; setShowUploadModal(true); setCurrentFileContent(''); // Clear previous content setErrorMessage(''); for (const file of files) { setCurrentFileName(file.name); if (file.type === 'text/plain') { const reader = new FileReader(); reader.onload = async (e) => { const content = e.target.result; setCurrentFileContent(content); // Automatically save .txt content if (db && userId) { try { const documentsRef = collection(db, `artifacts/${appId}/users/${userId}/documents`); await addDoc(documentsRef, { fileName: file.name, content: content, timestamp: serverTimestamp() }); console.log("Tệp văn bản đã được tải lên và lưu trữ:", file.name); } catch (e) { console.error("Lỗi khi thêm tài liệu vào Firestore:", e); setErrorMessage("Không thể lưu tài liệu. Vui lòng thử lại."); } } }; reader.readAsText(file); } else { // For other file types, prompt user to manually paste content setCurrentFileContent(`Nội dung từ tệp '${file.name}' (loại: ${file.type}) không thể được trích xuất tự động. Vui lòng dán nội dung văn bản liên quan vào đây:`); } } // After processing all files, if not a .txt, the modal will remain open for manual input // If it was a .txt, the modal will close after content is set and saved. }; const handleManualContentSave = async () => { if (!currentFileName || !currentFileContent.trim()) { setErrorMessage("Vui lòng nhập nội dung hoặc chọn tệp."); return; } if (db && userId) { try { const documentsRef = collection(db, `artifacts/${appId}/users/${userId}/documents`); await addDoc(documentsRef, { fileName: currentFileName, content: currentFileContent, timestamp: serverTimestamp() }); console.log("Nội dung thủ công đã được lưu trữ cho:", currentFileName); setShowUploadModal(false); // Close modal after saving setCurrentFileContent(''); setCurrentFileName(''); } catch (e) { console.error("Lỗi khi thêm tài liệu thủ công vào Firestore:", e); setErrorMessage("Không thể lưu nội dung thủ công. Vui lòng thử lại."); } } }; const handleDeleteDocument = async (docId) => { if (db && userId) { try { await deleteDoc(doc(db, `artifacts/${appId}/users/${userId}/documents`, docId)); console.log("Tài liệu đã xóa:", docId); } catch (e) { console.error("Lỗi khi xóa tài liệu:", e); setErrorMessage("Không thể xóa tài liệu. Vui lòng thử lại."); } } }; // Send message to chatbot const sendMessage = async () => { if (!userMessage.trim() || isLoading || !db || !userId) return; const userMsg = userMessage.trim(); setUserMessage(''); setErrorMessage(''); setIsLoading(true); // Add user message to chat history in Firestore try { const chatHistoryRef = collection(db, `artifacts/${appId}/users/${userId}/chatHistory`); await addDoc(chatHistoryRef, { sender: 'user', message: userMsg, timestamp: serverTimestamp() }); } catch (e) { console.error("Lỗi khi lưu tin nhắn người dùng:", e); setErrorMessage("Không thể lưu tin nhắn của bạn."); setIsLoading(false); return; } try { // Construct prompt with document content const prompt = `Dựa trên các tài liệu sau:\n\n${documentContent}\n\nTrả lời câu hỏi: ${userMsg}\n\nLƯU Ý QUAN TRỌNG: CHỈ TRẢ LỜI DỰA TRÊN THÔNG TIN CÓ TRONG CÁC TÀI LIỆU ĐƯỢC CUNG CẤP. KHÔNG THÊM BẤT KỲ DỮ LIỆU NÀO NẰM NGOÀI PHẠM VI CỦA CÁC TÀI LIỆU ĐƯỢC HỌC. Nếu thông tin không có trong tài liệu, hãy nói rõ rằng bạn không tìm thấy thông tin đó.`; let chatHistoryForAPI = []; chatHistoryForAPI.push({ role: "user", parts: [{ text: prompt }] }); const payload = { contents: chatHistoryForAPI }; const apiKey = ""; // Canvas will provide this const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); let botResponse = "Xin lỗi, tôi không thể tạo phản hồi vào lúc này."; if (result.candidates && result.candidates.length > 0 && result.candidates[0].content && result.candidates[0].content.parts && result.candidates[0].content.parts.length > 0) { botResponse = result.candidates[0].content.parts[0].text; } else { console.error("Cấu trúc phản hồi API không mong muốn:", result); setErrorMessage("Lỗi từ API: Cấu trúc phản hồi không hợp lệ."); } // Add bot message to chat history in Firestore await addDoc(chatHistoryRef, { sender: 'bot', message: botResponse, timestamp: serverTimestamp() }); } catch (error) { console.error("Lỗi khi gọi API Gemini:", error); setErrorMessage("Lỗi khi giao tiếp với chatbot. Vui lòng thử lại."); // Add an error message to chat history if API call fails try { const chatHistoryRef = collection(db, `artifacts/${appId}/users/${userId}/chatHistory`); await addDoc(chatHistoryRef, { sender: 'bot', message: "Xin lỗi, đã xảy ra lỗi khi xử lý yêu cầu của bạn. Vui lòng thử lại sau.", timestamp: serverTimestamp() }); } catch (e) { console.error("Lỗi khi lưu tin nhắn lỗi của bot:", e); } } finally { setIsLoading(false); } }; if (!isAuthReady) { return (
Chúng tôi không thể tự động trích xuất văn bản từ loại tệp này. Vui lòng dán nội dung văn bản liên quan vào hộp bên dưới để chatbot có thể sử dụng.
{errorMessage && (